next.js 깃허브 페이지 Discussions에 나와 비슷한 이슈가 있다.
링크
Kubernetes에서 여러 개의 Pod로 Next.js 앱을 실행할 때 ISR이 제대로 작동하지 않는 문제
각 요청이 다른 Pod에 도달할 때 발생하는 불일치
재검증 주기: 5초
기존 콘텐츠: X
새로운 콘텐츠: Y
첫 번째 요청 -> Pod 1
5초 후, 두 번째 요청 -> Pod 1
5초 후, 세 번째 요청 -> Pod 2
사용자는 X -> Y -> X 순서로 다른 콘텐츠를 보게 됨
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
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)
}
}
}
}
초기 배포시 문제 (/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
해당 코드는 다음과 같이 동작한다.
Next.js의 BUILD_ID를 읽는다.
해당 BUILD_ID에 대한 캐시 디렉토리가 이미 존재하면 종료한다.
캐시 디렉토리가 없으면 기존 캐시를 모두 삭제하고 새 캐시 디렉토리를 생성한다.
Next.js 빌드 결과를 새 캐시 디렉토리로 복사한다.
쿠버네티스 환경에서 여러 개의 pod를 사용하는 경우, 캐시 일관성 문제는 중요한 과제이다.
각 pod가 독립적으로 메모리 캐시를 관리하게 되면, 캐시가 일관되게 공유되지 않아 서버 간의 데이터 불일치가 발생할 수 있다.
이를 방지하려면 PVC와 같은 공유 저장소를 사용하여 캐시를 디스크에 저장하고, 모든 pod가 동일한 데이터를 참조할 수 있도록 구성하는 것이 중요하다. (중앙 집중형 캐시(Redis, Memcached)를 사용하는 방법도 좋은 방법이다.)