본문으로 건너뛰기
01 2,000노드에서 부러지는 것

01 · 2,000노드 Valkey — 무엇이 먼저 부러지고, 그중 무엇이 Kubernetes로 넘어오는가

  • 간판 수치는 Kubernetes에서 잰 게 아닙니다. 2,000노드·1B RPS의 서면판인 valkey.io 블로그 원문에 Kubernetes·pod·StatefulSet·container가 한 번도 나오지 않고, 실험대는 r7g.2xlarge(8코어/64GB) 클러스터 + 부하 생성기 c7g.16xlarge 750대였습니다. 발표 시작 13초에 본인들이 “we come from Amazon, which is mostly a VM based world“라고 밝힙니다.
  • 그래도 엔진 수정 4건은 인프라와 무관하게 넘어옵니다. #654·#1018·#2154·#2277은 배포 방식이 아니라 cluster bus 코드 안에 들어갔습니다. 버전만 맞추면 됩니다. 각각 8.0 / 8.1 / 9.0 / 9.0.
  • CPU 100% 그래프를 클러스터 전체로 읽으면 틀립니다. 슬라이드에서 천장에 붙는 계열은 engine_cpu_percent_p99고 같은 차트의 p90·avg는 한 자릿수입니다(스냅샷 p90 6.2 / avg 5.80). 발표자 노트는 포화 범위를 “atleast 5% of the nodes”(오타는 원문 그대로)라 적습니다. 하한이지 점추정이 아닙니다. 2,000노드가 전부 탄 게 아니라 최소 100대가 탔다는 뜻이고, 발표는 이 구분을 하지 않습니다.
  • Kubernetes 조언 중 실측이 뒷받침하는 건 사실상 하나입니다. CPU limit을 걸지 마라. 나머지(StatefulSet, headless service 부트스트랩, IP 직결, AZ 분산)는 전부 경험칙이고 발표에 수치가 붙지 않습니다.
  • operator는 아직 도입 대상이 아닙니다. API가 v1alpha1이고 README가 “not ready for production use"라고 명시하며 scale-out/in에 Valkey 9.0+를 요구합니다. 그런데 spec.shards는 진짜 최상위 필드라 Q&A의 “shard가 first-class” 주장 자체는 맞습니다.
  • 2,000노드에서 먼저 부러지는 건 처리량이 아니라 장애 복구입니다. 정상 상태 gossip 비용은 노드 수에 선형이었고, 터진 곳은 전부 primary를 수백 개씩 한 번에 죽였을 때의 재접속(415~455 kill)·failure report(499 kill)·투표 경로였습니다.

왜 이 문서인가. 발표 제목은 Kubernetes at XL Scale인데 간판 수치인 2,000노드 / 1B RPS는 EC2에서 나왔습니다. 그래서 이 문서는 발표가 증명한 것(엔진 한계와 그 수정)과 조언에 그친 것(Kubernetes 배치·리소스)을 나눠 놓고, 그중 무엇이 Kubernetes로 전이되는지만 남깁니다. 검증 기준은 발표 전사(942줄), 발표자 노트가 붙은 슬라이드 원본 54장, valkey.io 블로그, 업스트림 PR 4건, valkey.conf unstable 브랜치입니다.

출처: KubeCon + CloudNativeCon Europe 2026 — Scaling Valkey the Right Way: Kubernetes at XL Scale (Sarthak Aggarwal · Madelyn Olson, AWS ElastiCache). 2026-03-26(목) 11:00–11:30 CET, Hall 8 | Room E. 슬라이드 원본은 PPTX 54장입니다. 이 글은 발표 내용에 업스트림 PR 대조·슬라이드 그래프 재해석·근거 등급 판정을 덧붙였고, 발표가 말한 것과 1차 문서가 말하는 것이 어긋나는 지점을 그대로 적었습니다.

자매 문서: 02 발표 전사

1. 이 발표가 증명한 것과 증명하지 않은 것

제목에 Kubernetes가 들어가 있고 그 조언이 실제로 발표의 절반을 차지합니다. 그런데 간판 수치를 만든 실험은 Kubernetes 위에서 돌지 않았습니다.

서면판에 그대로 적혀 있습니다. 같은 실험을 글로 옮긴 Scaling a Valkey Cluster to 1 Billion Request per Second 원문에 Kubernetes·EKS·pod·StatefulSet·container 언급이 0회입니다. 대신 이렇게 적혀 있습니다. “Valkey cluster was deployed on AWS r7g.2xlarge instance type… we used 750 instances of AWS c7g.16xlarge.” 튜닝도 코어 피닝(taskset, cset)과 NIC 인터럽트 어피니티(ethtool)를 손으로 잡았습니다. 셋 다 Kubernetes 위에서는 하기 까다롭거나 아예 안 합니다.

슬라이드 그래프의 계열 이름이 engine_cpu_percent_p99인 것도 같은 방향을 가리킵니다. Prometheus 메트릭이 아니라 ElastiCache/AWS 내부 대시보드입니다. 발표자 본인 진술이 가장 명확합니다.

we come from Amazon, which is mostly a VM based world. So, we’re all we look over here at Kubernetes and we’re like, “Wow, this technology is amazing. I wish we could use more of it.” — 00:13~00:24

그렇다고 Kubernetes가 없는 발표는 아닙니다. 역할이 다를 뿐인데, 증거가 아니라 조언입니다. 발표를 층위로 나눠 보면 이렇습니다.

층위내용근거의 출처
엔진 한계·수정풀메시 cluster bus, 16384 슬롯, 재접속 폭풍, failure report O(N), 투표 분열, pub/sub 헤더EC2 2,000노드 실측 — 단 #654(8.0)와 #1018(8.1)은 이 캠페인 이전에 이미 머지됐다. 슬라이드 37 노트도 “Before Valkey 9, the community made some other important improvements"라 적는다
Kubernetes 배치 조언standalone은 StatefulSet+Helm, headless service 부트스트랩, pod name을 hostname으로, CoreDNS 지연 피해 IP 직결, primary/replica AZ 분산실측 없음 — 경험칙
Kubernetes 리소스 조언캐시는 메모리로 스케일하니 CPU 집약 워크로드와 co-locate, CPU limit 걸지 마라실측 없음 — 경험칙
미래valkey-operator 개발 중, shard를 first-class 구성 요소로아직 없음

이 표를 읽을 때 1행과 2·3행을 섞지 않는 것이 중요합니다. 1행은 엔진 안에 들어간 코드라 인프라를 가리지 않고 따라옵니다. 2·3행은 두 AWS 엔지니어의 운영 감각이고, 좋은 감각이지만 이 발표의 숫자가 뒷받침하지는 않습니다.

발표 주체의 편향도 밝혀 둘 값어치가 있습니다. Valkey 자체는 Linux Foundation 산하 벤더 중립 BSD-3 프로젝트지만, 이 발표의 수치는 AWS가 AWS 하드웨어에서 잰 것입니다. 발표자 둘 다 AWS ElastiCache 팀이고, valkey.io 블로그도 공저자 4명 중 셋이 AWS 소속으로 적혀 있습니다. 아래에서 볼 수정 PR 작성자 중 Sarthak Aggarwal과 Seungmin Lee도 AWS입니다(블로그 저자 표기·GitHub 프로필로 확인). 투표 분열을 고친 Binbin Zhu만 Tencent Cloud 소속입니다. Roshan Khatri의 소속은 공개 프로필에 없어 이 문서가 확인하지 못했습니다.

2. 왜 2,000노드에서 멈추는가

발표는 첫 슬라이드부터 분명히 말합니다. “You shouldn’t run a 2000 node Valkey cluster, but you might have to.” 2,000은 세 개의 천장이 겹치는 지점이라서 나온 숫자입니다.

천장성질2,000노드에서의 값
full-mesh cluster bus연결 수가 노드 수의 제곱노드당 1,999개 링크, 전체 1,999,000개
16384 슬롯 고정재샤딩 입도가 노드 수에 반비례primary 1,000대 기준 primary당 16.4슬롯
cluster-node-timeout감지 시간이 규모에 종속기본 15000ms — 낮추면 대형 클러스터에서 깨진다

2.1 full-mesh는 저장소를 안 쓰는 대가다

Valkey cluster는 토폴로지를 외부 저장소에 두지 않습니다. 발표자가 직접 대비시킵니다. “a much more cloud native solution to this might be storing all of this topology information [in] something like etcd. What Valkey does instead is every shard owns their state.” 공식 cluster specification도 같은 문장을 씁니다: “Valkey Cluster is a full mesh where every node is connected with every other node using a TCP connection.”

etcd가 없다는 건 컨트롤 플레인 의존이 없다는 뜻입니다. 대신 모든 노드가 나머지 전부와 TCP 연결을 유지해야 하고, N노드면 링크가 N(N-1)/2입니다. 2,000노드에서 노드 하나가 1,999개 연결을 들고 있고 클러스터 전체로는 약 200만 개입니다. 이 상수가 나중에 §3.1의 재접속 폭풍을 만듭니다.

발표는 이 비용을 “1,000노드 부근에서 CPU 1~2%“라는 수치로 요약합니다(05:38). 그런데 이 수치는 슬라이드 밖 1차 출처를 찾지 못했습니다. cluster-spec에도, 1-billion-rps 블로그에도 없습니다. 슬라이드-only 주장으로 표시하고 인용할 때 그렇게 밝히는 편이 안전합니다.

확인된 쪽은 정상 상태에서 gossip 비용이 선형으로 늘었다는 관찰입니다(19:10~19:22). 평시 오버헤드는 예측 가능했고, 문제는 전부 장애 시점에 몰렸습니다.

2.2 슬롯은 개수가 아니라 열 분산이 먼저 문제가 된다

16384라는 상수는 cluster-spec이 “effectively setting an upper limit for the cluster size of 16384 primary nodes"라고 적어 둔 대로 primary 수의 절대 상한입니다. 그런데 2,000노드에서 부딪히는 건 이 상한이 아닙니다.

발표자의 설명이 정확합니다(09:29~09:49). 슬롯을 옮기려 할 때 “there’s too much variance in heat between various slots to actually kind of have a uniform cluster, and you’ll start seeing spikiness.”

산수로 옮기면 primary 1,000대 구성에서 primary당 16384 / 1000 ≈ 16.4슬롯이고, 슬롯 하나를 옮기면 그 노드 데이터의 약 6%가 함께 움직입니다. 100노드였다면 슬롯 하나가 노드의 0.6%였고 그 정도 입도로는 열을 부드럽게 재분배할 수 있습니다. 노드가 늘수록 재샤딩의 최소 단위가 상대적으로 굵어집니다. 그래서 슬롯 개수가 바닥나기 훨씬 전에 균등 분배가 먼저 불가능해집니다.

여기 대응하는 도구가 두 개 있습니다. 관측은 CLUSTER SLOT-STATS로, Valkey 8.0.0부터 슬롯 단위 key-count · cpu-usec · network-bytes-in · network-bytes-out을 줍니다(key-count 외 셋은 cluster-slot-stats-enabled yes가 필요합니다). 이동은 Valkey 9.0의 atomic slot migration으로, 예전 CLI 기반 키 단위 MIGRATE 대신 복제 링크처럼 슬롯 전체를 스트리밍하고 소유권은 맨 마지막에야 넘깁니다.

발표에서 가장 실무적인 한 줄은 여기서 나옵니다. 가장 뜨거운 슬롯은 건드리지 말고 두 번째로 뜨거운 슬롯을 옮겨라. 제일 뜨거운 걸 옮기는 게 가장 큰 작업이기 때문이고 목표는 “그 노드에서 hottest 아닌 것들을 먼저 걷어내는 것"입니다(14:17~14:26).

2.3 cluster-node-timeout은 규모에 종속된다

기본값 15초는 valkey.conf unstable 브랜치 1844행 # cluster-node-timeout 15000에서 확인했습니다. 발표에서 두 번(06:14, 20:15) 인용되는 값이고 둘 다 맞습니다.

중요한 건 값 자체보다 이 값이 규모와 묶여 있다는 사실입니다. 소규모 클러스터에서는 1초, 500ms까지 내릴 수 있는데 “that won’t scale very well"입니다. 노드가 늘면 감지·전파가 그만큼 오래 걸립니다. §3.1에서 보듯 이 타임아웃은 재접속 예산의 분모이기도 합니다. Kubernetes로 옮겨도 이 상수는 그대로 따라옵니다.

3. 대량 장애에서 부러진 네 곳

테스트 시나리오부터 확인합니다. 2,000노드(primary 1,000 / replica 1,000)에서 primary를 1 → 100 → 250 → 330 → 499개까지 늘려 가며 죽였습니다. 499에서 멈춘 건 quorum 때문인데, primary 1,000대 중 501대가 살아 있어야 클러스터가 스스로 회복합니다. 그래서 499가 자가 치유의 한계선입니다. 2AZ·3AZ 구성을 모두 걸었고 그중 3AZ에서 AZ 하나를 통째로 날리는 시험(약 666노드 동시 실패)도 돌렸습니다(17:19). 부하는 25M → 100M → 1B RPS로 올렸습니다. 클라이언트는 valkey-py · valkey-glide · valkey-go에 더해 발표자 노트 기준 iovalkey · redisson까지 다섯 종을 걸었습니다.

네 곳이 부러졌고 전부 대량 장애 직후의 경로였습니다. 정상 상태에서 터진 것은 하나도 없습니다.

3.1 재접속 폭풍 — valkey-io/valkey#2154

primary를 415~455개씩 죽이면 살아남은 노드들이 죽은 노드에 100ms마다 재접속을 시도했습니다. clusterCron이 매 사이클 link == NULL인 피어(PFAIL/FAIL 상태)에 다시 다이얼을 걸었기 때문입니다. 연결이 성공할 리 없다는 걸 알면서도 여는 비용과 해제하는 비용을 계속 냈습니다.

풀메시가 이 비용을 곱해 줍니다. 죽은 노드가 450개면 살아남은 노드 하나가 매 100ms마다 450번의 실패할 connect를 시도합니다. 발표자 노트가 결과를 그대로 적습니다. “The compute was consistently 100% for at atleast 5% of the nodes at a time. Specially in cases where we failed anything north of 450 primaries, the cluster was almost never able to recover.”(atleast·Specially는 노트 원문 표기입니다.) 여기서 5%는 하한입니다. 5%일 수도, 그보다 훨씬 많을 수도 있다는 뜻입니다.

프로파일링은 특별한 도구를 쓰지 않았습니다. Q&A에서 밝힌 대로 “it was just a normal perf tool"이고 거기서 뽑은 flame graph가 슬라이드에 그대로 붙어 있습니다(기법 자체의 표준 레퍼런스는 Brendan Gregg의 페이지지만 발표자가 이 출처를 지목한 적은 없습니다). 아래 콜 경로는 그 슬라이드 이미지에서 직접 읽은 것이고 전사에도 슬라이드 텍스트에도 나오지 않습니다.

재접속 폭풍에서 CPU가 실제로 타는 지점 — serverCron의 100ms tick마다 clusterCron이 죽은 peer 전원에 connect syscall을 다시 겁니다. #2154는 이 재시도를 cluster-node-timeout 창 안에서 ~750ms 간격 · ~10회로 흩뿌립니다.
도식 텍스트
  • serverCron — 메인 이벤트루프 tick
  • clusterCron — 죽은 peer 전원 재접속 시도
  • freeClusterLink — 끊긴 소켓 정리
  • connSocketConnect
  • 재접속 스로틀 — cluster-node-timeout 내 분산 · ~750ms 간격 · ~10회 (#2154)
  • connSocketClose
  • anetTcpGenericConnect
  • __close — syscall
  • connect
  • __sys_connect — syscall
  • tcp_v4_connect — 커널 · CPU 소모 지점
  • 매 100ms 틱
  • 죽은 peer 재접속
  • 끊긴 링크 해제
  • #2154 이후

#2154가 이걸 고쳤습니다. 작성자가 발표자 본인(Sarthak Aggarwal)이고 2025-07-22 머지, 이슈 #2122를 닫습니다. 재시도 총량을 cluster-node-timeout 안으로 예산화해서, 100ms마다 무한정 두드리는 대신 PFAIL 구간 전체에 약 10회를 흩뿌립니다(기본값에서 재시도 간격 약 750ms). 모든 죽은 노드가 “fair chance"를 받되 컴퓨트를 태우지는 않게 하는 절충입니다.

측정치는 세 갈래로 나뉩니다.

조건지표개선
20~30노드 클러스터P99 노드 CPU-35%
200~300노드 클러스터P90 / Avg CPU-10%
primary 1,000 중 450 killP99 노드 CPU-75%
슬라이드 노트 기준engine CPU 상한100% → 40%

3.2 failure report O(N) — #2277

499개를 죽인 직후 CPU가 두 번 크게 치솟았다가 스스로 내려왔습니다. 원인은 살아남은 노드들끼리의 뒷정리였고, 죽은 노드 자체는 아니었습니다. 발표자 노트에 규모가 나옵니다. 2,000노드 중 499개를 죽인 뒤 남은 1,501노드가 이미 failed로 마킹된 노드를 두고 계속 gossip과 report를 주고받았습니다.

failure report는 리스트로 관리됐고 정리(clusterNodeCleanupFailureReports)가 O(N) 스캔이었습니다. 2,000노드에서 이 O(N)이 곱해지면 정리 작업만으로 CPU의 상당 부분이 사라집니다.

수정은 #2277입니다. Seungmin Lee 작성, 2025-07-28 unstable 머지, 이슈 #2139를 닫습니다. 리스트를 만료 타임스탬프로 버킷팅한 radix tree(rax)로 바꿨습니다. 발표자 노트가 설계 의도를 한 줄로 요약하는데, 만료 시각을 1초 단위로 올림해서 묶어 저장합니다. 그러면 같은 초에 만료될 report들이 한 노드에 모이고, 만료 정리가 개별 스캔에서 버킷 단위 삭제로 바뀝니다.

PR 자체 벤치마크는 m7g.2xlarge 2,000노드(primary 1,000 / replica 1,000)에서 잰 것입니다. 그 baseline은 300노드 failover 중 CPU 약 100%였고 그중 정리 작업이 약 60%를 차지했습니다. 최적화 후에는 더 큰 450노드 failover에서 CPU 약 3035%였습니다. 무대에서 말한 “2830%“는 조금 후한 반올림입니다.

3.3 투표 분열 — #1018

발표자가 “the main problem"이라 부른 대목이 여기입니다. 메커니즘은 이렇습니다. primary가 죽으면 그 shard의 replica가 살아 있는 primary들에게 표를 구합니다. 그런데 Valkey는 한 epoch에 primary 하나가 한 번만 투표할 수 있습니다. 여러 replica가 같은 shard의 소유권을 두고 무한히 도는 걸 막으려는 정상적인 설계입니다.

문제는 서로 다른 shard 둘이 같은 시점에 표를 구할 때 생깁니다. 살아 있는 primary들이 갈라져서 투표하면 어느 쪽도 quorum을 못 채우고 둘 다 승격에 실패한 채 다음 epoch을 기다립니다. shard 수백 개가 한꺼번에 죽으면 이 분열이 반복돼 회복이 사실상 멈춥니다.

shard1·shard3 replica가 같은 epoch에서 동시에 투표를 요청하면 생존 primary들의 표가 갈려 어느 쪽도 quorum을 못 채웁니다. #1018은 shard-id 사전순으로 랭크를 매겨 요청을 ~500ms씩 스태거해 shard1이 먼저, shard3가 뒤이어 당선되게 합니다.
도식 텍스트
  • shard1 replica
  • shard3 replica
  • 생존 primary들
  1. 분기 — 고침 전 — 동시 요청
    1. FAILOVER_AUTH_REQUEST (epoch=E)
    2. FAILOVER_AUTH_REQUEST (epoch=E)
    3. epoch당 1표만 행사 가능 · 두 요청 동시 도착 → 표 분산
    4. AUTH_ACK (일부)
    5. AUTH_ACK (일부)
    6. 양쪽 다 quorum 미달 · failover 실패 → 재시도
  2. 고침 후 — #1018 rank 순 stagger
    1. shard-id 사전순 랭크: shard1 < shard3 · shard3는 shard1보다 ~500ms 늦게 요청
    2. FAILOVER_AUTH_REQUEST (epoch=E, t1)
    3. AUTH_ACK (quorum 확보)
    4. FAILOVER_AUTH_REQUEST (epoch=E+1, t2≈t1+500ms)
    5. AUTH_ACK (quorum 확보)

#1018이 이 분열을 풀었습니다. Valkey 메인테이너 Binbin Zhu 작성이고 2025-01-11 머지인데, 네 건 중 작성자가 AWS 소속이 아닌 유일한 PR(Tencent Cloud)이자 Valkey 8.1에 들어간 유일한 수정입니다. failed_primary_rank 필드를 추가해 failover 요청을 순서대로 흘립니다. rank가 하나 밀릴 때마다 FAILOVER_AUTH_REQUEST를 약 500ms씩 뒤로 미루니 여러 primary가 한꺼번에 죽어도 투표 창이 겹치지 않습니다. primary가 하나만 죽을 때의 동작은 그대로입니다.

무대 표현 하나는 짚고 가야 합니다. 발표자는 “we ordered it lexicographically“라고만 말하는데(23:29), PR이 실제로 정렬하는 키는 죽은 primary의 shard-id이고 대상도 failed shard들 사이로 한정됩니다. 노드 ID를 사전순으로 줄 세우는 게 아닙니다. 같은 PR·같은 저자·같은 메커니즘이니 틀린 말은 아니고 무대용 축약입니다.

결과는 회복 시간 그래프로 나옵니다. primary 1개 실패부터 499개 실패까지 전부 1분 이내에 자가 치유했습니다.

3.4 pub/sub 헤더 2KB — #654

Valkey의 pub/sub은 cluster bus를 그대로 탑니다. 아무 노드에나 publish하면 구독자가 붙은 노드로 전달되는 편의가 여기서 나옵니다. 문제는 cluster bus 메시지 헤더가 pub/sub에 필요 없는 정보를 통째로 싣고 다녔다는 데 있습니다.

가장 큰 덩어리가 슬롯 소유권 비트맵입니다. 산수를 맞춰 보면 16384비트 = 2048바이트, 곧 발표가 말한 2KB입니다. 100바이트짜리 메시지에 2KB 헤더가 붙습니다. 그런데 pub/sub 메시지는 슬롯을 신경 쓰지 않습니다.

수정은 #654입니다. Roshan Khatri 작성, 2024-07-26 머지, 이슈 #557을 닫고 Valkey 8.0에 들어갔으니 네 건 중 가장 이릅니다. clusterMsgLight라는 경량 헤더를 도입해 data union 앞에 고정 헤더만 남겼습니다. 8.0 이전 엔진과 통신할 때는 전체 메시지로 폴백합니다.

여기서 발표와 공식 블로그가 어긋납니다. 어긋나는 쪽은 블로그입니다. clusterMsgLight의 필드를 더하면 16바이트입니다.

필드타입바이트
sig[4]char[4]4
totlenuint324
veruint162
notused1uint162
typeuint162
notused2uint162
합계16

발표와 슬라이드는 16바이트라 말하고 이 구조체와 일치합니다. valkey.io 블로그만 “~30 bytes"라고 적습니다. 머지된 구조체가 이기므로 16바이트가 맞고 블로그가 이상값입니다. 최종 커밋을 바이트 단위로 대조하지는 않았으니 정밀도가 중요하면 src/cluster_legacy.hclusterMsgLight를 직접 읽는 게 맞습니다.

3.5 네 건 정리

PR작성자머지일반영 버전측정치발표 주장과의 차이
#654 pub/sub 경량 헤더Roshan Khatri2024-07-268.0헤더 2048B → 16B없음 — 발표가 맞고 valkey.io 블로그의 “~30B"가 이상값
#1018 failover 랭킹Binbin Zhu (Tencent Cloud)2025-01-118.1rank당 약 500ms 지연, 499 primary 실패도 60초 내 회복“lexicographically"는 축약 — 실제 정렬 키는 failed primary의 shard-id
#2154 재접속 스로틀링Sarthak Aggarwal (AWS)2025-07-229.0450 kill 시 P99 CPU -75%, 노트 기준 100% → 40%없음
#2277 failure report raxSeungmin Lee (AWS)2025-07-289.0450노드 failover에서 CPU 30~35%무대에서는 “28~30%” — 약간 후하다

버전이 세 갈래로 나뉜다는 게 실무에서 제일 중요합니다. “2,000노드가 된다"는 결론은 9.0의 것이지만 네 수정 중 둘은 8.0/8.1에 이미 들어가 있습니다. 8.x에 머물러 있어도 pub/sub 헤더와 투표 분열은 이미 고쳐져 있습니다.

4. 그래프를 다시 읽어라 — p99와 클러스터 전체는 다르다

이 글이 발표에 가장 크게 덧붙이는 대목이 여기입니다.

슬라이드의 CPU 스파이크 그래프는 100%에 붙습니다. 그리고 이야기는 “재접속 폭풍이 컴퓨트를 태웠다"로 이어집니다. 그런데 그래프를 자세히 보면 100%에 붙는 계열은 하나뿐이고 그 이름이 engine_cpu_percent_p99입니다. 같은 차트의 p90과 avg는 바닥에 깔린 채 거의 움직이지 않습니다. 스로틀링 적용 후 슬라이드에는 스냅샷 시점 값이 p99 24.363 / p90 6.2 / avg 5.80으로 찍혀 있습니다.

계열스파이크 구간
engine_cpu_percent_p99100% 도달상위 1% 노드가 포화
engine_cpu_percent_p90한 자릿수 (스냅샷 6.2)상위 10%도 여유
engine_cpu_percent_avg한 자릿수 (스냅샷 5.80)클러스터 평균은 한산

곧 2,000노드 전체가 탄 게 아닙니다. 탄 것은 일부 노드인데, 그 “일부"의 크기를 노트가 확정해 주지는 않습니다. “compute was consistently 100% for at atleast 5% of the nodes at a time"은 하한입니다. 2,000노드의 5%면 100대이니 최소 100대이고 실제로는 그보다 많았을 수 있습니다. 이 문서도 정확한 규모는 확인하지 못했습니다.

이 구분이 왜 중요한가요. 다음이 달라집니다.

  • 용량 산정이 달라집니다. 평균 한 자릿수를 보고 노드를 줄이면 정확히 그 포화 노드들이 먼저 죽습니다. 반대로 p99 100%를 보고 전 클러스터를 키우면 대부분의 용량이 놉니다.
  • 알람 설계가 달라집니다. 클러스터 평균 CPU 임계값으로는 이 사고가 안 잡힙니다. 잡히는 건 분위수 알람과 “포화 노드 비율” 알람입니다.
  • 원인 추적이 달라집니다. 소수 노드가 탄다는 건 부하보다 토폴로지 비대칭을 가리킵니다. 어떤 노드가 유난히 많은 죽은 피어를 붙들고 있었다는 뜻입니다.

발표는 이 구분을 하지 않습니다. 스파이크를 클러스터 전체 현상처럼 서술하고 넘어갑니다. 잘못된 서술은 아니지만 그래프를 그대로 옮겨 “2,000노드 클러스터가 CPU 100%를 쳤다"고 인용하면 원 데이터보다 훨씬 센 주장이 됩니다.

한 가지 더. 이 계열 이름들은 ElastiCache/AWS 내부 메트릭이고 Prometheus 지표가 아닙니다. Kubernetes에서 같은 그림을 그리려면 대응 지표를 직접 만들어야 합니다. pod별 CPU를 분위수로 집계하고 “CPU 90% 이상인 pod 수 / 전체 pod 수"를 별도 시계열로 뽑는 쪽이 이 발표의 그래프에 가장 가깝습니다.

5. Kubernetes로 올릴 때 — 조언과 그 근거 등급

발표의 Kubernetes 파트는 유용하지만 대부분은 실측 없이 두 AWS 엔지니어의 운영 감각에 기댑니다. 그대로 두면 §3의 실측치와 같은 무게로 읽히므로 항목마다 등급을 붙였습니다.

등급은 셋입니다. EC2 실측은 이 발표나 인용된 1차 문서에 수치가 있는 경우입니다. 경험칙은 발표자의 운영 경험이고 수치가 없습니다. 미검증은 발표는 말했지만 1차 문서에서 확인하지 못한 것입니다.

5.1 배치와 디스커버리

조언근거 등급근거 실체
standalone은 StatefulSet + Helm 차트경험칙valkey-io/valkey-helmvalkey(standalone/replication) 차트가 실제로 있다. 수치는 없다
headless service 하나를 부트스트랩 시드로경험칙09:01 “typically in Kubernetes, you’ll maybe have a headless service that just always has one configured node”
pod name을 node hostname으로 지정경험칙노드 ID는 기본 무작위다. 직접 지정할 수 있지만 발표자가 “프로덕션에서 쓰는 걸 많이 보지 못했다"고 덧붙인다(10:03)
CoreDNS 지연 피해 IP로 직결경험칙10:24~10:36. hostname은 TLS 검증용으로만 쓰라는 단서가 붙는다
primary를 AZ에 고르게 분산경험칙failover에 primary quorum이 필요하니 한 AZ에 몰리면 안 된다는 논리. AZ 장애 시험(3AZ, 약 666노드 동시 실패) 자체는 EC2 실측이지만 배치 규칙에 붙은 수치는 아니다
replica를 primary와 다른 AZ에경험칙08:04. Valkey에 내장 수단이 없고 operator가 풀 문제라고 본인이 인정한다
클라이언트 AZ-local readEC2 실측(단, 별건)이 발표가 아니라 AZ affinity 블로그의 AWS 워크로드 예시다

토폴로지를 클라이언트에 넘기는 명령을 발표는 CLUSTER SLOTS로 시연합니다(슬라이드 12~16). 슬라이드에 찍힌 응답에는 슬롯 범위, ip/port, 노드 id에 더해 hostnameavailability-zone이 들어 있습니다. 그런데 공식 문서와 어긋납니다. CLUSTER SHARDS 문서는 노드 수준 availability-zone을 돌려주는 건 CLUSTER SHARDS이고 레거시 CLUSTER SLOTS는 hostname·AZ를 같은 방식으로 싣지 않는다고 적습니다. 슬라이드가 설명용으로 손본 응답인지 문서가 뒤처진 것인지 이 문서는 확정하지 못했습니다. 실제로 토폴로지를 파싱할 거면 CLUSTER SHARDS를 쓰는 쪽이 문서와 일치합니다.

AZ-local read의 값어치는 이 발표가 아니라 AZ affinity 블로그에 숫자로 있습니다.

항목AZ affinity 없음있음
cross-AZ 데이터 전송$3,285/월$0
클러스터 기본 비용$1,088/월$1,088/월
지연800µs300µs (-60%)

GLIDE에는 read 전략이 네 가지입니다. PRIMARY(기본), PREFER_REPLICA, AZ_AFFINITY, AZ_AFFINITY_REPLICAS_AND_PRIMARY. AZ_AFFINITY는 클라이언트가 자기 AZ를 client_az로 알려 줘야 동작하고 서버 쪽은 Valkey 8.0+가 필요합니다. 캐시 워크로드에서 이건 가장 저렴한 개선 중 하나입니다. 코드 변경이 클라이언트 설정 두 줄이고 효과가 지연과 청구서 양쪽에 나옵니다.

5.2 리소스 — CPU limit을 걸지 마라

발표가 내놓는 유일하게 모호하지 않은 판정입니다. 슬라이드 노트 원문이 명령형입니다.

Most caching scales on memory, not CPU. So co-locate caches. Avoid setting CPU limits, because that will cause latency spikes!

논리 사슬은 짧고 정확합니다. 캐시는 메모리로 스케일하니 CPU가 남습니다 → CPU 집약 워크로드와 같은 노드에 두면 양쪽 사용률이 좋아집니다 → 그런데 캐시에 CPU limit을 걸면 동시 명령이 몰릴 때 지연 스파이크가 납니다.

왜 그런지는 발표가 다른 곳에서 이미 깔아 뒀습니다. Valkey의 메인 스레드는 여전히 단일 스레드입니다(12:22). 요청 하나는 마이크로초 단위지만 커넥션 수립은 수백 마이크로초입니다(27:57). 큐가 짧고 개별 작업이 짧은 시스템일수록 CFS throttle 한 번의 상대적 타격이 큽니다. 평균 CPU 사용률이 limit의 절반이어도 순간 수요만으로 throttle이 걸립니다. 지표상으로는 여유 있어 보이는데 p99만 무너집니다.

판정항목근거 등급
필수CPU limit 제거 (requests만 설정)경험칙 — 발표자 노트가 명령형으로 적시. 수치는 없다
좋음캐시와 CPU 집약 워크로드 co-locate경험칙

메모리 쪽 조언은 이 표에 없습니다. 발표도 슬라이드 노트도 메모리 requests·limits 설정을 한 번도 언급하지 않기 때문입니다. 캐시가 메모리로 스케일한다는 전제를 받아들이면 메모리는 requests = limits로 고정하는 쪽이 맞습니다. 그건 이 문서의 추론이고 발표자의 조언은 아닙니다.

같은 CPU limit 문제를 커널 쪽에서 본 문서가 k8s-features 03 CPU Burst인데, throttling이 평균은 놔두고 꼬리를 망가뜨린다는 게 거기 실측으로 나옵니다.

5.3 커넥션

발표자가 “Kubernetes 클러스터에서 보는 가장 큰 문제"로 지목한 것이 커넥션입니다(11:30~11:44). pod 수천 개가 각자 캐시 노드로 연결을 여는 순간 개별 노드가 죽습니다.

조언근거 등급근거 실체
connection pooling 필수EC2 실측(별건)AWS Database 블로그 벤치마크 — PHPRedis 풀링 없음 2.82ms/op vs 영속 연결+풀링 0.21ms/op, 약 13배
retry 상한 + jitter 백오프경험칙발표는 “limit connection retries"까지만. 지수 백오프+지터의 구체 파라미터는 위 AWS 블로그(Lettuce 기준 min 1s / max 5s)
topology refresh를 합쳐라경험칙12:34~12:46. GLIDE가 커넥션별 개별 refresh를 하나로 모은다
Envoy는 커넥션 풀링 용도로만경험칙11:45~11:54
1B RPS 실험도 풀링을 썼다EC2 실측Q&A 27:40 — 질문이 정확히 이것이었고 답이 “yes”

Envoy를 두고 발표의 입장이 두 갈래라는 걸 놓치면 안 됩니다. 토폴로지를 숨기려고 Envoy를 앞에 두는 건 “a little bit of an antiquated concept"이라고 깎습니다. 요즘 클라이언트가 충분히 좋고 RESP는 L7 HTTP 프록시가 이해하기엔 특이한 프로토콜이라 엔진에 직접 붙을 때의 이점을 잃습니다. 그런데 커넥션 풀링을 Envoy 계층에서 하는 건 “one good reason"이라고 분명히 인정합니다. 슬라이드도 “Proxies (like envoy!) can help with connection limits“라고 용도를 좁혀 적습니다. 토폴로지 추상화용으로 쓰면 안 되고 커넥션 수를 줄이는 용도면 쓸 만합니다.

이 실패 모드 자체를 정면으로 다룬 별도 글이 Managing Connection Storms in Valkey at Scale입니다. 멀티플렉싱으로 n×p 커넥션 방정식을 줄이는 방법, 프록시 계층이 커넥션 증가를 곱셈에서 선형으로 바꾸는 구조, Uber와 Snap의 대응(노드별 rate limiter·서킷 브레이커, CPU 95%에서 load shedding)이 정리돼 있습니다.

6. operator는 지금 쓸 수 있나

발표가 여러 번 가리키는 미래가 valkey-io/valkey-operator입니다. Q&A의 마지막 질문(“초기화할 때 모든 pod가 primary가 돼 버리지 않게 어떻게 보장하나”)에 답한 것도 결국 operator였습니다.

판정: 부적합. 지금 프로덕션에 넣을 것은 아닙니다.

항목상태판정
API 버전valkey.io/v1alpha1, README가 “may change in future releases”부적합
성숙도README 원문 “This operator is in active development and not ready for production use부적합
서버 요구사항scale-out/in에 Valkey 9.0+ 필요반쪽
배포 모드cluster 모드 전용반쪽
미지원cert-manager, module, backup없음
spec.shards / spec.replicasquickstart CR에 최상위 필드로 실재좋음

마지막 행이 Q&A 주장을 확인해 줍니다. quickstart 예제가 spec: {shards: 3, replicas: 1}로 “3-shard cluster with 1 replica per shard (6 pods total)“를 만듭니다. shard는 StatefulSet 개수로 환산되는 파생 개념이 아니라 진짜 1급 필드입니다. “the operator does basically support these first-class constructs as shards"는 정확한 서술입니다.

그동안의 대안으로 발표자가 준 것이 replica migration입니다. 본인이 “the poor man’s solution"이라 부릅니다. 노드를 전부 클러스터에 넣고 일부를 replica로 세워 두면 replica 없는 primary가 생겼을 때 유휴 노드가 알아서 가서 붙습니다. 관련 설정 세 개를 valkey.conf에서 확인했습니다.

설정기본값동작
cluster-migration-barrier1자기 primary에 replica가 최소 1개 남을 때만 이주한다
cluster-allow-replica-migrationyesno면 고아 primary로의 이주가 꺼진다
cluster-node-timeout15000 (ms)장애 감지 창

부수적인 사실 하나. valkey-helm 메인테이너 목록에 jdheyburn이 있는데 바로 다음 세션의 발표자 Joe Heyburn(Braze)입니다. 발표자가 “Joe가 operator를 얘기할 것"이라 예고한 배경이 이것입니다. 그런데 Joe의 세션 초록에는 operator라는 단어가 없습니다. Redis 250여 인스턴스를 EC2에서 Kubernetes로 무중단 이관한 사례 발표고 operator 언급은 YouTube 설명란에만 나옵니다.

7. 정리 — 넘어오는 것과 안 넘어오는 것

항목Kubernetes로 넘어오는가이유
엔진 수정 4건 (#654 / #1018 / #2154 / #2277)✅ 그대로배포 방식이 아니라 cluster bus 코드다. 버전만 맞추면 된다
full-mesh · 16384 슬롯 · cluster-node-timeout✅ 그대로인프라와 무관한 프로토콜 상수
CLUSTER SLOT-STATS 기반 재샤딩 절차✅ 그대로명령 단위 도구, 8.0+
atomic slot migration✅ 그대로9.0+
p99와 클러스터 전체를 가르는 관측 규율✅ 오히려 더 필요pod가 많을수록 소수 포화가 평균에 더 묻힌다
CPU limit 금지✅ Kubernetes 전용 조언EC2에는 해당 개념이 없다
499 primary 실패 후 60초 내 회복⚠️ 반쪽엔진 회복 시간이다. pod 재스케줄·CNI·이미지 pull은 별도로 얹힌다
1B RPSc7g.16xlarge 750대의 부하 생성 능력이 전제다
코어 피닝(taskset/cset)·IRQ 어피니티(ethtool)cpuset·특권 컨테이너·노드 수준 접근이 필요하다
“2,000노드 실증” 그 자체Kubernetes에서 잰 적이 없다
operator 기반 shard 관리✗ 지금은v1alpha1, “not ready for production use”

이 발표가 증명한 건 Valkey 엔진이 2,000노드에서 자가 치유한다는 것이고 그 증명은 EC2에서 이뤄졌으며 코드는 8.0·8.1·9.0에 나눠 들어가 있습니다. 그래서 Kubernetes에서 챙길 첫 번째는 배포 토폴로지가 아니라 버전입니다. Kubernetes 파트에서 실측이 뒷받침하는 조언은 사실상 CPU limit을 걸지 마라 하나뿐이고 나머지 배치·디스커버리 조언은 좋은 경험칙이되 이 발표의 수치와 같은 무게로 인용하면 안 됩니다. 그리고 슬라이드의 CPU 100% 그래프는 p99 계열입니다. 탄 것은 소수 노드였고 클러스터 전체가 아니었습니다. 이 구분이 용량 산정과 알람 설계를 바꿉니다.

8. 확인하지 못한 항목

아래는 이 문서가 확인하지 못했거나 1차 문서와 어긋나는 지점입니다. 인용할 때 그대로 밝혀십시오.

  • “1,000노드 부근에서 cluster bus CPU 1~2%"(05:38)의 1차 출처를 찾지 못했습니다. cluster-spec에도 1-billion-rps 블로그에도 없습니다. 슬라이드-only 주장으로 취급하십시오.
  • 발표자가 언급한 2025년 선행 발표(02:49 “we did a similar talk last year”)를 찾지 못했습니다. kccnceu2025 전 일정, 양 발표자의 valkey.io 저자 페이지, YouTube/CNCF 검색을 모두 훑었으나 일치하는 것이 없었습니다. KubeCon NA 2025(Atlanta)는 확인하지 않았고 남은 후보로 가장 유력합니다.
  • CLUSTER SHARDSavailability-zone 필드 시점이 어긋납니다. 명령 문서는 노드 수준 availability-zone 필드가 9.1.0에서 추가됐다고 적는데, AZ affinity 블로그는 서버 availability-zone 설정이 Valkey 8에 들어왔다고 적습니다. 설정과 응답 필드가 서로 다른 릴리스에 들어온 것으로 보이지만 이 문서는 그 관계를 확정하지 못했습니다.
  • 슬라이드의 CLUSTER SLOTS 응답이 명령 문서와 어긋납니다. 슬라이드 13~16의 CLUSTER SLOTS 응답에는 hostnameavailability-zone이 들어 있는데, CLUSTER SHARDS 문서는 레거시 CLUSTER SLOTS가 이 둘을 같은 방식으로 싣지 않는다고 적습니다. 슬라이드가 설명하려고 손본 응답인지 문서가 뒤처진 것인지 확인하지 못했습니다.
  • 재접속 폭풍 콜스택은 슬라이드 이미지에서 읽었습니다. 전사에도 슬라이드 텍스트 추출본에도 이 심볼들은 없습니다. 이미지 판독 결과이므로 정확한 심볼명이 중요하면 원본 PPTX의 해당 flame graph를 직접 보십시오.
  • 재접속 폭풍이 태운 노드 규모는 하한만 알 수 있습니다. 슬라이드 노트의 “atleast 5% of the nodes"는 최소값이고, 실제 포화 노드 비율을 밝힌 자료를 찾지 못했습니다.
  • Roshan Khatri(#654 작성자)의 소속을 확인하지 못했습니다. GitHub 프로필에 회사 표기가 없고 1-billion-rps 블로그 저자 목록에도 없습니다. 나머지 세 명(Sarthak Aggarwal·Seungmin Lee = AWS, Binbin Zhu = Tencent Cloud)은 확인했습니다.
  • #2154 / #2277 / #1018의 버전 귀속은 GitHub milestone이 아니라 추론입니다. 세 PR 모두 milestone 필드가 비어 있어 valkey.io 블로그의 명시적 서술 + 머지일과 9.0.0 GA(2025-10-21)의 시간 관계로 판정했습니다. #654 → 8.0도 같은 종류의 추론입니다.
  • #654의 16바이트는 PR 본문의 구조체 정의에서 합산한 값입니다. 최종 머지 커밋을 바이트 단위로 대조하지는 않았습니다.
  • Pokémon Go / Niantic의 “예측 대비 50배” 이야기(01:15~02:00)를 확인하지 않았습니다. 슬라이드에 Google Cloud 블로그 링크가 붙어 있지만 이 문서의 검증 대상이 아니었습니다.
  • 발표가 참조한 Envoy 관련 Valkey 공식 문서를 찾지 못했습니다(13:02). 발표자의 경험칙 이상으로 볼 근거가 없습니다.
  • 전사 오인식 주의. 자동 자막이 Valkey를 “Valkyrie”, Madelyn을 “Madeline/Merlin/Marlin”, StatefulSet을 “stateless set”, etcd를 “CD”, RESP를 “the REST protocol”, 16384를 “16,000"으로 적습니다. 전사를 그대로 인용하지 마십시오.

9. 출처

발표

Valkey 공식 문서

업스트림 PR · 이슈

생태계

관련 세션

마지막 수정 일자