Kubernetes와 headless service를 사용해 캐시 무효화를 해결한 방법

발행: (2025년 12월 29일 오후 03:45 GMT+9)
7 min read
원문: Dev.to

Source: Dev.to

위의 소스 링크 아래에 번역하고 싶은 전체 텍스트를 제공해 주시면, 요청하신 대로 한국어로 번역해 드리겠습니다. 코드 블록, URL 및 마크다운 형식은 그대로 유지됩니다.

🛑 문제: 빠른 캐싱 및 확장 가능한 무효화

We decided on in‑memory caching (using @nestjs/cache-manager) to slash DB load and boost response times. In a Kubernetes cluster, where the service runs across multiple, dynamic pods, this creates a major headache: cache inconsistency.

  • If an admin updates a package price in MySQL, only the pod that handled the write sees the change immediately.
  • All other pods continue serving stale data.

Additionally, pod IPs are temporary; they change during restarts, deployments, and scaling events. Hard‑coding IP lists is impossible, so we needed a reliable way to discover every running pod at the exact moment of a data update—a broadcasting mechanism.

🛠️ 솔루션: K8s 헤드리스 서비스

우리 전략은 하이브리드 접근 방식이었습니다:

  1. 성능을 위한 로컬 캐싱
  2. 무효화를 위한 타깃 내부 HTTP 브로드캐스트

Step 1 – 헤드리스 서비스로 Pod 탐색

표준 Kubernetes Service는 단일 Virtual IP를 가진 로드밸런서 역할을 합니다. 헤드리스 서비스(clusterIP: None)는 로드밸런서를 건너뛰고 현재 활성화된 각 Pod의 IP 주소에 대한 개별 DNS A 레코드 목록을 반환합니다.

apiVersion: v1
kind: Service
metadata:
  name: booking-service-headless
spec:
  clusterIP: None               # THE TRICK: makes it headless
  selector:
    app: booking-service
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000

이제 booking-service-headless.default.svc.cluster.local을 조회하면 현재 정상적인 모든 Pod IP가 배열 형태로 반환됩니다.

시각화 – 헤드리스 서비스 동작 예시
Kubernetes Headless Service Explained

Steps 2 & 3 – 브로드캐스트 구현

  1. Docker 이미지에 kubectl 추가(또는 @kubernetes/client-node 라이브러리 사용)하여 Pod가 Kubernetes API를 직접 조회할 수 있게 합니다.
  2. 최소 권한 원칙 적용: 전용 ServiceAccount를 만들고, 해당 네임스페이스 내 endpoints 리소스에 대해 listget만 허용하는 Role에 바인딩합니다. 이렇게 하면 Pod가 침해당했을 때 피해를 최소화할 수 있습니다.
  3. booking 서비스에 내부 /invalidate-cache 엔드포인트 노출. 이 엔드포인트는 캐시 키를 받아 메모리 캐시의 del() 메서드로 해당 키를 삭제합니다.

관리자 업데이트가 발생하면, 쓰기를 수행한 Pod가 다음 스크립트를 실행합니다:

  • 헤드리스 서비스 엔드포인트에서 현재 Pod IP 목록을 가져옵니다.
  • 각 Pod의 /invalidate-cache 엔드포인트에 HTTP POST 요청을 보냅니다.
# 업데이트 시 Node.js 프로세스가 실행
pod_ips=$(kubectl get endpoints booking-service-headless \
          -o jsonpath='{.subsets[0].addresses[*].ip}')

for ip in $pod_ips; do
  curl -X POST http://$ip:3000/v2/invalidate \
       -H "Content-Type: application/json" \
       -d '{"key":"package-123"}'   # 특정 키 삭제
done

왜 작동하는가

✅ FeatureDescription
Dynamic무효화 시점에 IP 목록이 최신이므로, 스케일 업/다운 및 재시작을 매끄럽게 처리합니다.
Targeted오래된 패키지 키만 삭제하여 캐시 보존을 최대화합니다.
Cost‑Effective추가 서비스나 외부 메시지 브로커가 필요 없습니다.
Reliablecurl 재시도와 로깅을 추가해 브로드캐스트 실패를 기록합니다.

대안 – 이미지에 kubectl/curl을 설치하고 싶지 않다면 공식 @kubernetes/client-node 라이브러리를 사용하세요. 이 라이브러리를 통해 Node.js 코드가 직접 Kubernetes API를 조회할 수 있어 로직을 더 테스트하기 쉽고, 오류 처리가 개선되며, 셸 프로세스를 스폰하는 오버헤드가 사라집니다.

📈 프로덕션 결과 및 시사점

롤아웃 후:

  • MySQL 부하가 지속적으로 낮게 유지되었습니다.
  • 캐시 적중률이 크게 상승했습니다 (패키지 쿼리에서 ≈ 95 %).
  • 응답 지연 시간이 캐시된 요청에 대해 ~200 ms에서 < 30 ms로 감소했습니다.
  • 운영 오버헤드는 최소 수준을 유지했습니다—새 서비스 없이, 추가 비용 없이, RBAC 제한 ServiceAccount가 보안 표면을 작게 유지했습니다.

주요 교훈

  1. Headless Service는 파드 수준 통신을 위한 간단하고 내장된 서비스 디스커버리 메커니즘입니다.
  2. 인메모리 캐시 + 명시적 무효화를 사용하면 운영 복잡성 없이 분산 캐시와 같은 성능을 얻을 수 있습니다.
  3. 최소 권한 ServiceAccount는 컨테이너 내부에 내부 도구를 노출하더라도 클러스터를 보호합니다.

대부분 정적 데이터이지만 가끔 업데이트가 필요한 경우, 이 패턴은 저비용의 Kubernetes 네이티브 방식으로 모든 파드의 캐시를 동기화할 수 있게 해줍니다.

결과 요약

  • 가동 시간: 95 %+ 이상을 유지했습니다.
  • 사고: 사용자가 오래된 패키지 데이터를 보는 사례가 보고되지 않았습니다.
  • Headless Service: 트릭은 트래픽이 많은 배포에서도 견고함을 입증했습니다.

Graph

시사점
이 솔루션은 가장 우아하고 비용 효율적인 답이 항상 비싼 관리형 서비스는 아니라는 것을 보여줍니다—이미 가지고 있는 도구를 창의적으로 활용할 수 있습니다. 문제에 돈을 쏟아붓는 것이 아니라 인프라를 현명하게 사용하는 것이 중요합니다.

Back to Blog

관련 글

더 보기 »