>  기사  >  웹 프론트엔드  >  Fastify 및 Redis Cache를 사용하여 웹 사이트 속도 향상

Fastify 및 Redis Cache를 사용하여 웹 사이트 속도 향상

WBOY
WBOY원래의
2024-08-26 21:46:32364검색

Speeding Up Your Website Using Fastify and Redis Cache

24시간도 채 안 되어 Cloudflare 캐시를 사용하여 웹 사이트 속도를 높이는 방법에 대한 게시물을 작성했습니다. 그러나 이후 Redis를 사용하여 대부분의 논리를 Fastify 미들웨어로 옮겼습니다. 스스로 할 수 있는 이유와 방법은 다음과 같습니다.

Cloudflare 캐시 문제

Cloudflare 캐시와 관련하여 두 가지 문제가 발생했습니다.

  • 응답 캐싱을 활성화한 후 페이지 탐색이 중단되었습니다. 얼마 전 Remix 포럼에 이에 대한 문제를 제기한 적이 있는데, 이 글을 쓰는 현재까지도 여전히 해결되지 않은 상태입니다. 응답 캐싱으로 인해 페이지 탐색이 중단되는 이유는 명확하지 않지만 Cloudflare에서 응답을 캐시할 때만 발생합니다.
  • 원본 게시물에 설명된 대로 재검증하는 동안 Cloudflare가 오래된 콘텐츠 제공을 수행하도록 할 수 없습니다. 사용할 수 있는 기능은 아닌 것 같습니다.

몇 가지 다른 문제(예: 패턴 일치를 사용하여 캐시를 제거할 수 없는 문제)가 있었지만 이는 내 사용 사례에 중요하지 않았습니다.

그래서 Redis를 사용하여 로직을 Fastify 미들웨어로 옮기기로 결정했습니다.

[!참고]
이미지 캐싱을 위해 Cloudflare 캐시를 떠났습니다. 이 경우 Cloudflare 캐시는 효과적으로 CDN으로 작동합니다.

Fastify 미들웨어

다음은 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<string, string>;
  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<string> => {
  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<string, string>;

  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;
});

주석은 코드 전체를 안내하지만 다음은 몇 가지 핵심 사항입니다.

  • 캐싱 기준:
    • 요청:
    • 인증된 사용자에 대한 응답을 캐시하지 마세요.
    • GET 요청만 캐시합니다.
    • "/supplements/"가 포함된 URL에 대한 응답만 캐시합니다.
    • 요청 헤더에 캐시 제어: no-cache가 포함된 경우 캐시를 우회합니다.
    • 응답:
    • 성공적인 응답만 캐시합니다(statusCode는 200).
    • 캐시(x-pillser-cache: HIT)에서 이미 제공된 응답을 캐시하지 마세요.
    • 콘텐츠 유형이 text/html인 응답만 캐시합니다.
  • 캐시 키 생성:
    • 요청 방법, URL 및 표시 영역 너비가 포함된 JSON 표현의 SHA-256 해시를 사용하세요.
    • 네임스페이스를 쉽게 지정하고 삭제할 수 있도록 캐시 키 앞에 'request:'를 붙입니다.
  • 요청 처리:
    • onRequest 수명 주기를 연결하여 요청에 캐시된 응답이 있는지 확인하세요.
    • 가능한 경우 캐시된 응답을 제공하고 x-pilser-cache: HIT로 표시합니다.
    • "재검증하는 동안 오래된 콘텐츠 제공"을 구현하여 캐시된 응답을 보낸 후 캐시를 새로 고치는 백그라운드 작업을 시작합니다.
  • 응답 처리:
    • onSend 수명 주기에 연결하여 응답을 처리하고 캐시합니다.
    • 간단한 캐싱을 위해 읽을 수 있는 스트림을 문자열로 변환합니다.
    • 캐시에서 특정 헤더(content-length, set-cookie, x-pilser-cache)를 제외합니다.
    • 캐시할 수 없는 응답을 x-pilser-cache: DYNAMIC으로 표시합니다.
    • 1일의 TTL(Time To Live)로 캐시 응답하고 x-pilser-cache: MISS로 새 항목을 표시합니다.

결과

여러 위치에서 지연 시간 테스트를 실행하고 각 URL에 대해 가장 느린 응답 시간을 포착했습니다. 결과는 아래와 같습니다.

URL Country Origin Response Time Cloudflare Cached Response Time Fastify Cached Response Time
https://pillser.com/vitamins/vitamin-b1 us-west1 240ms 16ms 40ms
https://pillser.com/vitamins/vitamin-b1 europe-west3 320ms 10ms 110ms
https://pillser.com/vitamins/vitamin-b1 australia-southeast1 362ms 16ms 192ms
https://pillser.com/supplements/vitamin-b1-3254 us-west1 280ms 10ms 38ms
https://pillser.com/supplements/vitamin-b1-3254 europe-west3 340ms 12ms 141ms
https://pillser.com/supplements/vitamin-b1-3254 australia-southeast1 362ms 14ms 183ms

Cloudflare 캐시에 비해 Fastify 캐시는 속도가 느립니다. 그 이유는 캐시된 콘텐츠가 여전히 원본에서 제공되는 반면 Cloudflare 캐시는 지역 엣지 위치에서 제공되기 때문입니다. 그러나 이러한 응답 시간은 좋은 사용자 경험을 달성하는 데 충분하다는 것을 알았습니다.

위 내용은 Fastify 및 Redis Cache를 사용하여 웹 사이트 속도 향상의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.