"Se um trabalhador quiser fazer bem o seu trabalho, ele deve primeiro afiar suas ferramentas." - Confúcio, "Os Analectos de Confúcio. Lu Linggong"
Primeira página > Programação > Acelerando seu site usando Fastify e Redis Cache

Acelerando seu site usando Fastify e Redis Cache

Publicado em 2024-08-27
Navegar:949

Speeding Up Your Website Using Fastify and Redis Cache

Há menos de 24 horas, escrevi um post sobre como acelerar seu site usando o cache Cloudflare. No entanto, desde então mudei a maior parte da lógica para um middleware Fastify usando Redis. Aqui está por que e como você pode fazer isso sozinho.

Problemas de cache Cloudflare

Tive dois problemas com o cache da Cloudflare:

  • A navegação da página foi interrompida após ativar o cache das respostas. Levantei uma questão sobre isso no fórum Remix há algum tempo, mas no momento em que escrevo isto, ainda não foi resolvido. Não está claro por que o cache da resposta está causando a interrupção da navegação da página, mas isso só acontece quando a resposta é armazenada em cache pelo Cloudflare.
  • Não consegui fazer com que o Cloudflare executasse Servir conteúdo obsoleto durante a revalidação, conforme descrito na postagem original. Parece que não é um recurso disponível.

Encontrei alguns outros problemas (como não conseguir limpar o cache usando correspondência de padrões), mas eles não foram críticos para meu caso de uso.

Portanto, decidi migrar a lógica para um middleware Fastify usando Redis.

[!OBSERVAÇÃO]
Deixei o cache do Cloudflare para armazenamento em cache de imagens. Nesse caso, o cache da Cloudflare funciona efetivamente como um CDN.

Fastify Middleware

O que se segue é uma versão anotada do middleware que escrevi para armazenar respostas em cache usando Fastify.

const isCacheableRequest = (request: FastifyRequest): boolean => {
  // Do not attempt to use cache for authenticated visitors.
  if (request.visitor?.userAccount) {
    return false;
  }

  if (request.method !== 'GET') {
    return false;
  }

  // We only want to cache responses under /supplements/.
  if (!request.url.includes('/supplements/')) {
    return false;
  }

  // We provide a mechanism to bypass the cache.
  // This is necessary for implementing the "Serve Stale Content While Revalidating" feature.
  if (request.headers['cache-control'] === 'no-cache') {
    return false;
  }

  return true;
};

const isCacheableResponse = (reply: FastifyReply): boolean => {
  if (reply.statusCode !== 200) {
    return false;
  }

  // We don't want to cache responses that are served from the cache.
  if (reply.getHeader('x-pillser-cache') === 'HIT') {
    return false;
  }

  // We only want to cache responses that are HTML.
  if (!reply.getHeader('content-type')?.toString().includes('text/html')) {
    return false;
  }

  return true;
};

const generateRequestCacheKey = (request: FastifyRequest): string => {
  // We need to namespace the cache key to allow an easy purging of all the cache entries.
  return 'request:'   generateHash({
    algorithm: 'sha256',
    buffer: stringifyJson({
      method: request.method,
      url: request.url,
      // This is used to cache viewport specific responses.
      viewportWidth: request.viewportWidth,
    }),
    encoding: 'hex',
  });
};

type CachedResponse = {
  body: string;
  headers: Record;
  statusCode: number;
};

const refreshRequestCache = async (request: FastifyRequest) => {
  await got({
    headers: {
      'cache-control': 'no-cache',
      'sec-ch-viewport-width': String(request.viewportWidth),
      'user-agent': request.headers['user-agent'],
    },
    method: 'GET',
    url: pathToAbsoluteUrl(request.originalUrl),
  });
};

app.addHook('onRequest', async (request, reply) => {
  if (!isCacheableRequest(request)) {
    return;
  }

  const cachedResponse = await redis.get(generateRequestCacheKey(request));

  if (!cachedResponse) {
    return;
  }

  reply.header('x-pillser-cache', 'HIT');

  const response: CachedResponse = parseJson(cachedResponse);

  reply.status(response.statusCode);
  reply.headers(response.headers);
  reply.send(response.body);
  reply.hijack();

  setImmediate(() => {
    // After the response is sent, we send a request to refresh the cache in the background.
    // This effectively serves stale content while revalidating.
    // Therefore, this cache does not reduce the number of requests to the origin;
    // The goal is to reduce the response time for the user.
    refreshRequestCache(request);
  });
});

const readableToString = (readable: Readable): Promise => {
  const chunks: Uint8Array[] = [];

  return new Promise((resolve, reject) => {
    readable.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
    readable.on('error', (err) => reject(err));
    readable.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
  });
};

app.addHook('onSend', async (request, reply, payload) => {
  if (reply.hasHeader('x-pillser-cache')) {
    return payload;
  }

  if (!isCacheableRequest(request) || !isCacheableResponse(reply) || !(payload instanceof Readable)) {
    // Indicate that the response is not cacheable.
    reply.header('x-pillser-cache', 'DYNAMIC');

    return payload;
  }

  const content = await readableToString(payload);

  const headers = omit(reply.getHeaders(), [
    'content-length',
    'set-cookie',
    'x-pillser-cache',
  ]) as Record;

  reply.header('x-pillser-cache', 'MISS');

  await redis.setex(
    generateRequestCacheKey(request),
    getDuration('1 day', 'seconds'),
    stringifyJson({
      body: content,
      headers,
      statusCode: reply.statusCode,
    } satisfies CachedResponse),
  );

  return content;
});

Os comentários percorrem o código, mas aqui estão alguns pontos-chave:

  • Critérios de cache:
    • Solicitações:
    • Não armazene respostas em cache para usuários autenticados.
    • Apenas solicitações GET em cache.
    • Apenas respostas em cache para URLs que incluem "/supplements/".
    • Ignore o cache se o cabeçalho da solicitação contiver cache-control: no-cache.
    • Respostas:
    • Apenas armazenar em cache as respostas bem-sucedidas (statusCode é 200).
    • Não armazene em cache as respostas já veiculadas do cache (x-pillser-cache: HIT).
    • Apenas respostas em cache com tipo de conteúdo: text/html.
  • Geração de chave de cache:
    • Use hash SHA-256 de uma representação JSON contendo método de solicitação, URL e largura da janela de visualização.
    • Prefixe a chave de cache com 'request:' para facilitar o namespace e a limpeza.
  • Tratamento de solicitação:
    • Conecte-se ao ciclo de vida onRequest para verificar se uma solicitação tem uma resposta em cache.
    • Serve a resposta em cache, se disponível, marcando-a com x-pillser-cache: HIT.
    • Inicie uma tarefa em segundo plano para atualizar o cache após enviar uma resposta em cache, implementando "Servir conteúdo obsoleto durante a revalidação".
  • Tratamento de respostas:
    • Conecte-se ao ciclo de vida onSend para processar e armazenar respostas em cache.
    • Converta fluxos legíveis em string para armazenamento em cache mais simples.
    • Exclua cabeçalhos específicos (comprimento do conteúdo, set-cookie, x-pillser-cache) do cache.
    • Marque respostas não armazenáveis ​​em cache como x-pillser-cache: DYNAMIC.
    • Respostas em cache com um TTL (Time To Live) de um dia, marcando novas entradas com x-pillser-cache: MISS.

Resultados

Executei testes de latência em vários locais e capturei o tempo de resposta mais lento para cada URL. Os resultados estão abaixo:

URL País Tempo de resposta da origem Tempo de resposta em cache do Cloudflare Agilize o tempo de resposta em cache
https://pillser.com/vitamins/vitamin-b1 nós-oeste1 240ms 16ms 40ms
https://pillser.com/vitamins/vitamin-b1 europa-oeste3 320ms 10ms 110ms
https://pillser.com/vitamins/vitamin-b1 austrália-sudeste1 362ms 16ms 192ms
https://pillser.com/supplements/vitamin-b1-3254 nós-oeste1 280ms 10ms 38ms
https://pillser.com/supplements/vitamin-b1-3254 europa-oeste3 340ms 12ms 141ms
https://pillser.com/supplements/vitamin-b1-3254 austrália-sudeste1 362ms 14ms 183ms

Comparado ao cache Cloudflare, o cache Fastify é mais lento. Isso ocorre porque o conteúdo armazenado em cache ainda é servido a partir da origem, enquanto o cache da Cloudflare é servido a partir de pontos de presença regionais. No entanto, descobri que esses tempos de resposta são suficientes para alcançar uma boa experiência do usuário.

Declaração de lançamento Este artigo foi reproduzido em: https://dev.to/lilouartz/speeding-up-your-website-using-fastify-and-redis-cache-4ck6?1 Se houver alguma violação, entre em contato com [email protected] para excluí-lo
Tutorial mais recente Mais>

Isenção de responsabilidade: Todos os recursos fornecidos são parcialmente provenientes da Internet. Se houver qualquer violação de seus direitos autorais ou outros direitos e interesses, explique os motivos detalhados e forneça prova de direitos autorais ou direitos e interesses e envie-a para o e-mail: [email protected]. Nós cuidaremos disso para você o mais rápido possível.

Copyright© 2022 湘ICP备2022001581号-3