[Kubernetes] Next.js ISR 문제 해결 (분산 환경)

문린이·2024년 10월 8일
0

문제 상황

next.js 깃허브 페이지 Discussions에 나와 비슷한 이슈가 있다.
링크

Kubernetes에서 여러 개의 Pod로 Next.js 앱을 실행할 때 ISR이 제대로 작동하지 않는 문제

각 요청이 다른 Pod에 도달할 때 발생하는 불일치

예시 시나리오

재검증 주기: 5초
기존 콘텐츠: X
새로운 콘텐츠: Y

발생하는 현상

  1. 첫 번째 요청 -> Pod 1

    • X 콘텐츠 제공
    • Y 콘텐츠로 새 정적 페이지 빌드
  2. 5초 후, 두 번째 요청 -> Pod 1

    • Y 콘텐츠 제공
  3. 5초 후, 세 번째 요청 -> Pod 2

    • X 콘텐츠 제공 (여전히 옛 버전)
    • Y 콘텐츠로 새 정적 페이지 빌드

결과

사용자는 X -> Y -> X 순서로 다른 콘텐츠를 보게 됨

문제 해결 1

PersistentVolumeClaim(PVC)를 사용하여 여러 Pod 간에 폴더를 공유

next-pvc.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: next-pvc
spec:
  storageClassName: next-sc
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 5Gi

next-sc.yaml (EFS)

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: next-sc
provisioner: efs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
parameters:
  provisioningMode: efs-ap
  fileSystemId: fs-abcde12345

next-deploy.yaml (중간 생략)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: next-deploy
spec:
  #
  template:
	#
    spec:
	  #
      containers:
        - volumeMounts:
       	  - name: my-volume
            mountPath: /app/.next
        #
      volumes:
      	- name: my-volume
          persistentVolumeClaim:
            claimName: next-pvc

문제 해결 2

ISR 메모리 캐시 비활성화

기본적으로 Next.js는 메모리(50MB)와 디스크에 캐시를 저장한다.

Kubernetes에서 각 Pod는 자체 캐시를 가지게 되어 Pod 간 캐시가 공유되지 않는다.

링크

next.config.js

module.exports = {
  cacheMaxMemorySize: 0, // disable default in-memory caching (defaults to 50mb)
}

밑에 코드를 사용하여 중앙 집중식 캐시(Redis 등)를 사용할 수도 있다.

next.config.js

module.exports = {
  cacheHandler: require.resolve('./cache-handler.js'),
  cacheMaxMemorySize: 0, 
}

cache-handler.js

const cache = new Map()
 
module.exports = class CacheHandler {
  constructor(options) {
    this.options = options
  }
 
  async get(key) {
    // This could be stored anywhere, like durable storage
    return cache.get(key)
  }
 
  async set(key, data, ctx) {
    // This could be stored anywhere, like durable storage
    cache.set(key, {
      value: data,
      lastModified: Date.now(),
      tags: ctx.tags,
    })
  }
 
  async revalidateTag(tag) {
    // Iterate over all entries in the cache
    for (let [key, value] of cache) {
      // If the value's tags include the specified tag, delete this entry
      if (value.tags.includes(tag)) {
        cache.delete(key)
      }
    }
  }
}

문제 해결 3

초기 배포시 문제 (/app 비어있음) 및 동기화

위에 2개를 적용하고 배포를 해도 처음 volumes은 빈 값이므로 에러가 나온다.

Deployment에 initContainers를 추가해서 해결한다.

링크

next-deploy.yaml (중간 생략)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: next-deploy
spec:
  #
  template:
	#
    spec:
	  initContainers:
        - name: init-cache-sync
          command: [ "/bin/sh", "-c" ]
          args:
            - |
              BUILD_ID_NEXT=$(cat /app/.next/BUILD_ID)
              if [ -d "/app/shared/${BUILD_ID_NEXT}" ]; then
                  echo "Already exist cache ${BUILD_ID_NEXT}"
                  exit 0
              else
                echo "Create cache for ${BUILD_ID_NEXT}"
                rm -rf /app/shared/*
                mkdir -p /app/shared/${BUILD_ID_NEXT}
                cp -r /app/.next/* /app/shared/
                echo "Cache created"
                exit 0
              fi
          volumeMounts:
            - name: my-volume
              mountPath: /app/shared
      containers:
        - volumeMounts:
       	  - name: my-volume
            mountPath: /app/.next
        #
      volumes:
      	- name: my-volume
          persistentVolumeClaim:
            claimName: next-pvc

해당 코드는 다음과 같이 동작한다.

  1. Next.js의 BUILD_ID를 읽는다.

  2. 해당 BUILD_ID에 대한 캐시 디렉토리가 이미 존재하면 종료한다.

  3. 캐시 디렉토리가 없으면 기존 캐시를 모두 삭제하고 새 캐시 디렉토리를 생성한다.

  4. Next.js 빌드 결과를 새 캐시 디렉토리로 복사한다.

결론

쿠버네티스 환경에서 여러 개의 pod를 사용하는 경우, 캐시 일관성 문제는 중요한 과제이다.

각 pod가 독립적으로 메모리 캐시를 관리하게 되면, 캐시가 일관되게 공유되지 않아 서버 간의 데이터 불일치가 발생할 수 있다.

이를 방지하려면 PVC와 같은 공유 저장소를 사용하여 캐시를 디스크에 저장하고, 모든 pod가 동일한 데이터를 참조할 수 있도록 구성하는 것이 중요하다. (중앙 집중형 캐시(Redis, Memcached)를 사용하는 방법도 좋은 방법이다.)

profile
Software Developer

0개의 댓글