본문 바로가기
Redis

단일 VM Valkey 를 K8s Sentinel HA 로 옮긴 기록

by Ramos 2026. 5. 4.

솔루션 형태로 납품되는 사내 플랫폼 BE 가 캐시로 Valkey(Redis OSS fork)를 쓴다. 그동안은 VM 1대에 단일 Valkey 가 떠 있었고, 이게 한 대 죽으면 BE 전체가 뜨겁게 식는 구조였다. 납품 사이트엔 DBA 가 없을 거고, K8s 위에 같이 올라가는 플랫폼이니 캐시도 Pod 로 올리되 Sentinel HA 로 짜자 - 이게 출발이었다. 이 글은 그 마이그레이션을 왜 Sentinel 로 골랐고, 어떻게 짰고, 어떤 검증을 거쳤고, 운영 적용 후 어떤 회귀에 부딪혔고, 결과적으로 무엇이 달라졌는지 짧게 정리한 기록이다.

Intro

  • 단일 VM Valkey → K8s 위 StatefulSet(data 3) + Deployment(Sentinel 3) Helm chart 로 마이그레이션. 사내 플랫폼 alpha 환경에 적용 완료.
  • Sentinel 을 고른 이유: 캐시 워크로드가 샤딩이 필요한 양이 아니었다. Cluster 의 16384 슬롯은 운영 부담만 늘리고 얻는 게 없었다. "단일 master 견고화" 가 진짜 요구사항이었고 거기엔 Sentinel 이 fit.
  • 데이터와 Sentinel 을 다른 워크로드 리소스로 분리한 이유: data 는 stable identity (PVC + ordinal), Sentinel 은 무상태 quorum 멤버. 한 Pod 에 묶으면 한쪽 장애가 다른 쪽으로 전파된다.
  • 부하테스트 + 5종 failover 시나리오 (master pod kill / master 노드 drain / Sentinel 과반 손실 / helm rolling 중 절체 / network partition) 를 alpha 에서 모두 통과시킨 뒤 운영 적용.
  • 운영 적용 직후 두 건의 회귀가 잡혔다. (1) 단순 PING readiness 가 split-brain 을 ready 처리해 분리된 master 가 service endpoint 에 노출, (2) postStart hook 의 valkey-cli 가 graceful-shutdown 중인 master 에 무한 대기 → 8 분+ stuck. 둘 다 readiness 강화 + hard timeout 으로 fail-fast 화.
  • 결과: 단일장애점 제거 + helm 한 줄로 재현 가능한 캐시 인프라 + Prometheus 로 6 인스턴스 동시 가시화. DBA 없는 사이트로 납품 가능한 형태가 됐다.

시점 컨텍스트 - 본문에 등장하는 환경은 alpha 한 곳이다. beta · prod 는 진입 준비 중이고, 본 chart 는 alpha 검증을 마친 뒤 다른 환경에 그대로 끌고 갈 계획이다. "운영 적용 후 회귀" 라는 표현도 alpha 환경에서의 검증·운영 단계 이야기로 읽으면 된다.


1. 출발점 - 단일 VM Valkey 의 단일장애점

기존 구성은 단순했다 - VM 한 대, Valkey 한 프로세스, BE 가 직접 host:port 로 박는다. 솔루션을 새 사이트에 깔 때마다 그 VM 한 대가 단일장애점이 됐다. 사이트엔 DBA 가 없을 가능성이 높고, OS · Valkey 패치 · 백업 · 절체 시 수동 개입까지 전부 우리 운영팀이 들여다봐야 했다.

플랫폼 자체는 어차피 K8s 위에 올라가는데 캐시만 VM 에 뽑아두는 게 점점 어색해졌다. K8s 위에 캐시를 올리되, 단일 Pod 가 아니라 HA 로 - 이게 명확해진 다음 글로 옮겨갔다.


2. 왜 Cluster 가 아니라 Sentinel 인가

K8s 위 Valkey HA 옵션은 사실상 두 가지다. Sentinel 이냐 Cluster 냐. 비교를 해봤다.

Sentinel Cluster
용도 단일 master 견고화 (자동 failover) 데이터 샤딩 + HA
토폴로지 data 3 + sentinel 3 (or quorum) 최소 6 노드 (master 3 + replica 3)
클라이언트 Sentinel 프로토콜 인지 (Lettuce/Jedis/Redisson 모두 지원) Cluster 프로토콜 (MOVED/ASK 처리, 멀티키 제약)
운영 부담 슬롯/리샤딩 개념 없음 16384 슬롯 + 리샤딩 운영
멀티키 트랜잭션 자유 같은 슬롯(hash tag) 으로 강제

내 워크로드는 샤딩이 필요한 데이터량이 아니었다. 캐시 + 분산락 + 짧은 stream 몇 개. Cluster 를 깔면 16384 슬롯 운영, 멀티키 hash tag 제약, 클라이언트 코드 변경까지 따라온다. 얻는 건 거의 없고 부담만 따라온다.

진짜 요구사항은 "단일 master 가 죽었을 때 자동으로 다른 노드가 master 가 되는 것" 한 줄이었고, 거기엔 Sentinel 이 정확히 fit 했다. Redisson 도 Lettuce 도 Sentinel 모드를 1급으로 지원하니 BE 코드는 한 줄도 안 바꿔도 되는 점도 컸다 (커넥션 string 만 sentinel:// 로 바꾸면 끝).

한 가지 추가 - 향후 데이터량이 정말 폭증해서 샤딩이 필요해지면 그때 Cluster 로 가도 늦지 않다. 지금 Cluster 로 선제 대응하는 건 YAGNI 의 정석.


3. 구조 - 두 가지 워크로드 리소스로 분리

chart 의 핵심 결정은 단순하다.

3.1 왜 data 는 StatefulSet, sentinel 은 Deployment 인가

  • data 는 영속 상태가 있다. Pod 별 PVC, ordinal 기반 안정 hostname (valkey-0/1/2), 부트스트랩 시 valkey-0 을 초기 master 로 삼는 컨벤션. 전부 StatefulSet 의 1급 기능.
  • sentinel 은 무상태 quorum 멤버다. PVC 도 ordinal 도 필요 없다. Deployment 가 자연스럽고, 굴리기도 가볍다.

같은 Pod 에 묶을 수도 있었다. Bitnami chart 가 한때 그랬다. 하지만 한쪽 장애가 다른 쪽으로 전파된다. data 컨테이너 OOM 으로 Pod 가 재시작되면 그 노드의 Sentinel 도 같이 사라져 quorum 이 흔들린다. 장애 도메인을 분리하는 게 Sentinel 의 의도와 맞다.

3.2 AntiAffinity + PDB + 롤링 strategy 의 조합

세 개를 같이 안 박으면 운영 중에 반드시 깨진다.

설정 의도 빠뜨리면
requiredDuringScheduling podAntiAffinity data 3 / sentinel 3 가 서로 다른 노드에 분산 한 노드 죽으면 quorum 동시 손실
PodDisruptionBudget(maxUnavailable=1) 자발적 disruption (drain, upgrade) 시 한 번에 한 대만 빠짐 노드 drain 이 quorum 깨뜨리고 진행
RollingUpdate(maxSurge=0, maxUnavailable=1) required AntiAffinity + replicas=3 환경에서 신규 pod 가 Pending 으로 영원히 멈추는 회귀 차단 helm upgrade 가 영영 안 끝남

특히 마지막 maxSurge=0 은 함정이다. 기본값 maxSurge=25% (= 1 추가) 로 두면, AntiAffinity 가 required 인 환경에서 4 번째 pod 가 들어갈 노드가 없어 영원히 Pending 으로 멈춘다. 이걸 sidecar 활성화 commit 같은 timing 에 한 번 깔끔하게 잡고 갔다.

3.3 부트스트랩 / failover 후 토폴로지 일관성

초기에 짠 init-replica.sh 는 단순했다. ordinal=0 이면 master, 그 외엔 valkey-0 에 REPLICAOF. 이게 failover 후엔 깨진다. master 가 valkey-1 로 옮겨간 상태에서 valkey-0 이 재기동되면 자기를 master 로 가정하고 split-brain 발생.

수정한 정책:

  1. SENTINEL get-master-addr-by-name mymaster현재 인지 master 조회
  2. sentinel 응답 있음:
    • 본인 = master → 작업 없음
    • 그 외 → 인지 master 에 REPLICAOF
  3. sentinel 응답 없음 (최초 부트스트랩) → ordinal=0 은 master, 그 외엔 valkey-0 에 REPLICAOF (fallback)

Sentinel 이 진실의 원천, ordinal 은 부트스트랩 fallback 일 뿐 — 이 한 줄이 토폴로지 일관성의 핵심.

3.4 메트릭 - 한 chart 에서 두 평면 동시 노출

oliver006/redis_exporter 사이드카를 data Pod 와 sentinel Pod 양쪽에 박았다. 같은 이미지지만 노출되는 메트릭이 다르다.

  • data sidecar (:9121) → redis_up, redis_memory_*, redis_master_link_up, redis_stream_*, redis_key_group_* ...
  • sentinel sidecar (:9355) → redis_sentinel_master_status, redis_sentinel_master_ok_sentinels ...

한쪽만 박으면 토폴로지 quorum 또는 분산락/stream 둘 중 하나가 메트릭에 안 잡힌다. 운영 환경의 Prometheus 가 Operator 미사용 vanilla 이라 ServiceMonitor 가 안 먹혀, prometheus.io/scrape Pod annotation 기반 디스커버리로 통일했다. 이 메트릭 평면 설계는 별도 학습 노트로 따로 정리해둠.


4. 검증 - 부하테스트 + failover 시나리오 5종

운영 적용 전에 alpha 에서 다음 시나리오들을 모두 돌렸다. chart 만 짜고 끝내면 진짜 문제는 운영에서 만난다.

시나리오 기대 동작 측정한 것
master pod 강제 kill Sentinel quorum (2/3) 으로 failover, 새 master 승격, BE 재연결 failover 시간, BE 측 에러 윈도우
master 노드 drain PDB 가 한 번에 한 대만 빼고, AntiAffinity 가 다른 노드 보장 drain 동안 service 가용성
Sentinel 2 대 동시 kill (quorum 임계) 1 대 남은 sentinel 은 단독 판단 불가 → 새 failover 트리거 안 됨 (정상) quorum 메트릭이 1 로 떨어지는지, 알람 firing
helm rolling upgrade 도중 master 절체 RollingUpdate strategy + PDB 로 한 번에 한 대만 교체, 토폴로지 보존 새 pod readiness 통과 시점, 분리된 master 미발생
network partition (sentinel ↔ data 일시 단절) partition 동안 sentinel 이 sdown 인지, 복구 시 자동 재합류 sdown → odown 전이, link 복구 시간

부하는 redis-benchmark + 실제 BE 트래픽 mirror 로 SET/GET/락 획득 패턴을 같이 돌렸다. BE 측 클라이언트(Redisson/Lettuce) 가 Sentinel 인지 master 변경에 자동 재연결하는지가 핵심 확인점. 결과는 Lettuce 의 RedisClient.connectSentinel(...) + MasterReplica 패턴이 평균 10 초 이내에 새 master 로 reconnect.


5. 운영 적용 후 부딪힌 두 회귀 - readiness 와 hook timeout

검증을 그렇게 돌리고도, 운영 적용 첫 주에 두 건이 잡혔다. 둘 다 검증 시나리오엔 안 들어 있던 상태에서 발생해서 이번 작업에서 가장 진하게 배운 부분이다.

5.1 단순 PING readiness 가 split-brain 을 ready 처리

기존 readiness 는 한 줄짜리였다 - valkey-cli PING | grep PONG. 그런데 helm rolling 도중 이런 흐름이 발생했다:

helm --wait 도 정상 success 로 처리해서 사람이 인지조차 못 한다. 이 회귀를 잡고 readiness 자체를 다시 짰다.

핵심 변경:

  • init-replica.sh 정상 종료 시 /var/run/valkey/replica-init-done 마커 파일 생성 (emptyDir 라 컨테이너 재기동 시 자동 초기화 → stale marker 없음)
  • 신규 readiness.sh 가 다음을 모두 통과해야 ready:
    1. 마커 파일 존재 (= postStart 정상 종료)
    2. PING 응답
    3. ROLE 확정 → replica 면 master_link_status:up, master 면 sentinel 인지 master = 본인 일치 검증
    4. sentinel 응답이 IP/FQDN 어느 쪽으로 와도 본인을 식별하는 듀얼 매치

핵심 교훈은 한 줄이다. readiness 는 "프로세스 살아있나" 가 아니라 "토폴로지 안에서 정합성 있게 살아있나" 까지 봐야 한다. 알람이 울리기 전에 K8s 가 fail-fast 로 막아주려면, 모니터링 알람의 invariant 와 readiness 의 invariant 가 같아야 한다.

5.2 postStart hook 안의 valkey-cli 가 8 분+ stuck

같은 시기 또 한 건. postStart hook 의 init-replica.sh 가 graceful-shutdown 중인 master 에 SENTINEL get-master-addr / PING / REPLICAOF 를 보냈는데, 응답이 안 와서 hook 자체가 8 분 넘게 stuck. K8s 입장에선 hook 진행 중이라 not-ready 도 아닌 어정쩡한 상태로 helm 이 진행 중처럼 보였다.

대응은 단순하지만 효과는 크다 — 모든 valkey-cli 호출을 coreutils timeout 으로 wrap.

timeout 5  valkey-cli ... SENTINEL get-master-addr-by-name mymaster
timeout 3  valkey-cli ... PING
timeout 10 valkey-cli ... REPLICAOF "$MASTER" 6379

set -e 와 결합되어 stuck 대신 fail-fast → readiness fail → helm timeout → 사람 개입 트리거.

교훈: hook/probe 안의 외부 호출은 무조건 hard timeout. graceful-shutdown / split-brain 같은 비정상 토폴로지에서 무한 대기는 흔하다. shell timeout 한 번 빠지면 K8s 의 self-healing 메커니즘 자체가 동작을 멈춘다.

두 회귀 모두 검증 시나리오 5종에 직접 포함되지 않았다. "검증을 다 통과했는데 운영에서 터졌다" 가 아니라, 검증 시나리오 자체가 split-brain 발생 직전의 readiness 동작이나 hook 의 stuck 가능성을 명시적으로 측정하지 않았기 때문. 다음 마이그레이션 때 이 두 가지는 검증 체크리스트의 1급 항목으로 박아둬야겠다고 정리.


6. 결과 - 무엇이 달라졌나

Before (VM 단일) After (K8s Sentinel HA)
단일장애점 Valkey VM 1 대 = SPOF data 3 / sentinel 3 분산, AntiAffinity 로 노드 분리
절체 수동 (사람이 개입) Sentinel quorum 자동 failover, BE 측 약 10 초 내 재연결
재현성 VM 셋업 가이드 (수동) helm install 한 줄, values 만 환경별로 분리
가시성 호스트 metric 일부 6 인스턴스 (data 3 + sentinel 3) 동시 Prometheus 노출, Grafana 단일 대시보드
납품 적합성 DBA 필수 DBA 없는 사이트에도 동일 chart 로 배포 가능
운영 정합성 단순 PING readiness readiness 가 토폴로지 invariant 까지 검증, hook 은 hard timeout 으로 fail-fast

가장 큰 변화는 "솔루션을 깔러 갈 때 들고 갈 수 있는 형태가 됐다" 는 점이다. 사이트마다 VM 셋업하고 Valkey 깔고 백업 잡 짜는 게 아니라, K8s 클러스터만 있으면 chart 한 번으로 캐시 인프라가 선다. 그 위에 우리 BE 가 그대로 올라간다.


7. 마무리 - 다음에 같은 마이그레이션을 할 때

이번 작업이 남긴 진짜 자산은 chart 자체보다 "단일 자원을 K8s HA 로 옮길 때의 디폴트 사고 흐름" 이었다.

  • 요구사항을 좁힌다 : "HA 화 한다" 가 아니라 "단일장애점 하나를 죽인다" 같은 한 줄로. 그래야 Sentinel/Cluster 같은 선택지가 자동으로 좁혀진다.
  • 장애 도메인을 분리한다 : data 와 quorum 은 같은 Pod 에 두지 않는다. 한쪽 장애가 다른 쪽으로 전파되면 HA 의 가정이 깨진다.
  • AntiAffinity / PDB / 롤링 strategy 는 묶음 단위로 셋팅한다 : 셋 중 하나만 빠져도 운영 중 한 번은 반드시 깨진다.
  • readiness 를 모니터링과 같은 invariant 로 쓴다 : 알람이 울리기 전에 K8s 가 막아주는 게 사후 알람보다 항상 낫다.
  • hook/probe 의 외부 호출엔 무조건 hard timeout : self-healing 의 전제 조건.

다음에 같은 패턴 (VM 단일 자원 → K8s HA) 마이그레이션을 할 일이 또 생기면, 위 다섯 줄을 그대로 펴놓고 시작하면 된다.