<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Etcd on jyukki's Blog</title><link>https://jyukki.com/tags/etcd/</link><description>Recent content in Etcd on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-KR</language><lastBuildDate>Sun, 06 Sep 2026 10:06:00 +0900</lastBuildDate><atom:link href="https://jyukki.com/tags/etcd/index.xml" rel="self" type="application/rss+xml"/><item><title>2026 개발 트렌드: Kubernetes v1.37 etcd RangeStream, 대량 List의 메모리 피크를 용량 계약으로 바꾸다</title><link>https://jyukki.com/posts/2026-09-06-kubernetes-etcd-rangestream-large-list-memory-trend/</link><pubDate>Sun, 06 Sep 2026 10:06:00 +0900</pubDate><guid>https://jyukki.com/posts/2026-09-06-kubernetes-etcd-rangestream-large-list-memory-trend/</guid><description>Kubernetes v1.37과 etcd v3.7의 RangeStream Beta를 계기로, control plane의 대형 List 요청을 단순히 빠르게 만드는 문제가 아니라 object 크기·동시성·fallback·메모리 피크를 함께 관리하는 용량 계약으로 다룹니다.</description><content:encoded><![CDATA[<p>Kubernetes control plane에서 <code>LIST</code>는 평상시에는 눈에 잘 띄지 않습니다. 하지만 API server가 시작하거나 watch cache를 다시 채우고, cache에서 처리할 수 없는 list가 etcd로 내려가며, 수십만 개의 Pod나 큰 CRD object를 읽는 순간에는 메모리 피크를 만들 수 있습니다. Kubernetes v1.37과 etcd v3.7은 이 지점에 <code>RangeStream</code>을 Beta로 넣었습니다. 한 번에 큰 응답을 조립하던 <code>Range</code> 읽기를 byte-aware chunk 스트림으로 바꿔, etcd와 API server가 전체 collection을 동시에 붙잡지 않도록 합니다.</p>
<p>이 변화는 &ldquo;List가 빨라진다&quot;는 한 줄보다 운영적으로 중요합니다. 기존 pagination은 key 개수를 기준으로 page를 나눴습니다. 그러나 key가 500개여도 각 object가 2KiB인지 2MiB인지에 따라 page의 메모리 비용은 전혀 다릅니다. 큰 object와 동시 list가 겹치면 etcd가 응답을 조립하고 API server가 decode하는 동안 같은 payload가 양쪽에 존재할 수 있습니다. RangeStream은 chunk를 받고 처리한 뒤 해제하도록 만들어, 적어도 <strong>응답 조립 때문에 생기는 예측 불가능한 peak</strong>를 줄이는 방향입니다.</p>
<p>이 글은 <a href="/posts/2026-08-27-kubernetes-137-scale-to-zero-control-plane-resilience-trend/">Kubernetes v1.37의 Scale-to-Zero와 Control Plane 회복력</a>, <a href="/posts/2026-09-02-kubernetes-metrics-api-dra-resource-contract-trend/">Kubernetes Metrics API와 DRA 리소스 계약</a>, <a href="/learning/deep-dive/deep-dive-kubernetes-rollouts/">Kubernetes Rollout 전략</a>을 잇습니다. 앞선 글이 control plane이 과부하에서 요청을 어떻게 제한하고 자원 상태를 어떻게 해석할지를 다뤘다면, 여기서는 대량 상태를 <strong>어떤 메모리 상한 안에서 읽을 것인가</strong>를 다룹니다.</p>
<p>공식 참고 자료:</p>
<ul>
<li>Kubernetes Blog, <a href="https://kubernetes.io/blog/2026/09/01/kubernetes-v1-37-etcd-range-stream/">Kubernetes v1.37: etcd RangeStream Cuts Memory Use on Large List Reads</a></li>
<li>etcd Docs, <a href="https://etcd.io/docs/v3.7/dev-guide/api_grpc_gateway/">RangeStream RPC API</a></li>
<li>Kubernetes Blog, <a href="https://kubernetes.io/blog/2026/08/26/kubernetes-v1-37-release/">Kubernetes v1.37: Garhwal</a></li>
</ul>
<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>RangeStream이 기존 <code>Range</code>·key-count pagination과 무엇이 다른지, 그리고 어떤 메모리 피크를 줄이는지 이해합니다.</li>
<li>실제 사용 조건인 Kubernetes v1.37, etcd v3.7, <code>EtcdRangeStream</code> feature gate와 runtime fallback을 구분합니다.</li>
<li>&ldquo;기능이 켜졌다&quot;가 아니라 listStream metric·RSS·cache 초기화 시간·tail latency로 효과를 판정하는 기준을 세웁니다.</li>
<li>대형 CRD와 list-heavy controller를 가진 cluster에서 canary와 rollback을 설계할 수 있습니다.</li>
</ul>
<h2 id="핵심-개념이슈">핵심 개념/이슈</h2>
<h3 id="1-list-비용은-object-개수가-아니라-object-크기--동시성--경로로-계산해야-한다">1) List 비용은 object 개수가 아니라 object 크기 × 동시성 × 경로로 계산해야 한다</h3>
<p>API server의 watch cache는 대부분의 list/watch를 메모리에서 처리하지만, cache는 시작과 재초기화 때 resource의 전체 상태를 etcd에서 읽어야 합니다. cache miss나 특정 fallback list도 etcd를 읽을 수 있습니다. 기존의 paginated <code>Range</code>는 page를 key 수로 나눴으므로, 한 page에 우연히 큰 Pod spec·status, annotation이 과도한 CRD, 많은 managed field가 섞이면 byte 크기는 여전히 커집니다.</p>
<p>문제는 payload가 한 번만 존재하지 않는다는 점입니다. unary <code>Range</code>는 etcd가 page 전체를 조립한 후 전송하고, API server는 이를 받은 뒤 decode합니다. 큰 page가 여러 request와 겹치면 etcd heap, API server heap, GC pause, OOM retry가 연쇄할 수 있습니다. <code>kubectl get</code> 한 번이 문제가 아니라, controller restart·API server failover·inventory job이 동시에 대량 list를 하는 복구 장면이 위험합니다.</p>
<table>
  <thead>
      <tr>
          <th>관측값</th>
          <th>먼저 묻는 질문</th>
          <th>우선 조치</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>apiserver RSS 급증</td>
          <td>어떤 resource의 cache 초기화와 겹쳤는가</td>
          <td>resource별 object 크기·개수·초기화 시간을 inventory</td>
      </tr>
      <tr>
          <td>etcd memory spike</td>
          <td>unary Range 응답 조립과 list 동시성이 겹쳤는가</td>
          <td>v3.7 호환·listStream 사용 여부 확인</td>
      </tr>
      <tr>
          <td>list p99 증가</td>
          <td>cache hit가 줄었는가, client가 너무 넓게 list하는가</td>
          <td>selector·namespace·controller cache 범위 축소</td>
      </tr>
      <tr>
          <td>OOMKilled 뒤 반복 복구</td>
          <td>peak를 만든 object가 사라졌는가</td>
          <td>limit 상향 전 oversized object와 concurrency를 먼저 제거</td>
      </tr>
  </tbody>
</table>
<p>따라서 RangeStream은 object 크기를 작게 만들거나 list 요청 수를 제한하는 기능이 아닙니다. <a href="/posts/2026-09-02-kubernetes-metrics-api-dra-resource-contract-trend/">Kubernetes Metrics API와 DRA 리소스 계약</a>에서 resource status를 단일 진실로 보지 않듯, memory 사용량도 &ldquo;기능 gate가 on&quot;이라는 한 신호로 설명하면 안 됩니다.</p>
<h3 id="2-rangestream은-byte-aware-streaming이고-etcd-v37이-실제-경계다">2) RangeStream은 byte-aware streaming이고, etcd v3.7이 실제 경계다</h3>
<p>etcd v3.7의 <code>RangeStream</code> RPC는 <code>RangeRequest</code>와 같은 결과를 반환하되, 응답 전체를 만들기 전에 적응형 chunk로 나눠 전송합니다. 큰 value가 섞여도 chunk는 key 수보다 byte 크기에 맞춰 조절되고, consumer는 받은 chunk를 처리·해제한 뒤 다음 chunk를 받습니다. Kubernetes v1.37의 API server는 whole collection을 읽는 watch cache initialization과 cache에서 답할 수 없는 직접 etcd list에 이 경로를 사용합니다.</p>
<p>조건은 세 개입니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>kube-apiserver &gt;= 1.37
</span></span><span style="display:flex;"><span>etcd           &gt;= 3.7
</span></span><span style="display:flex;"><span>EtcdRangeStream feature gate = true (v1.37에서는 Beta·기본 활성화)
</span></span></code></pre></div><p>여기서 &ldquo;v1.37로 올렸으니 streaming이다&quot;라고 단정하면 안 됩니다. API server는 시작 시 etcd 지원 여부를 확인하고, <code>Unimplemented</code> 응답이면 runtime에서 기존 paginated <code>Range</code>로 fallback합니다. 즉 managed control plane이 etcd 버전을 공개하지 않거나, self-managed cluster가 API server만 먼저 올렸다면 기능은 안전하게 fallback할 수 있습니다. 안전한 fallback은 좋지만, 기대했던 memory profile이 나오지 않는 이유이기도 합니다.</p>
<h3 id="3-새로운-metric은-adoption-확인용이고-slo-자체는-아니다">3) 새로운 metric은 adoption 확인용이고 SLO 자체는 아니다</h3>
<p>공식 안내에서 가장 직접적인 증거는 다음 metric입니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-promql" data-lang="promql"><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">etcd_request_duration_seconds_count</span>{<span style="color:#8be9fd;font-style:italic">operation</span><span style="color:#ff79c6">=</span>&#34;<span style="color:#f1fa8c">listStream</span>&#34;}
</span></span></code></pre></div><p>이 count가 0보다 크면 API server가 RangeStream을 사용한 것입니다. 계속 0이면 오래된 etcd가 가장 흔한 원인이지만, 해당 시간에 whole-collection read가 없었을 수도 있습니다. 따라서 rate를 request volume·cache initialization event와 함께 해석해야 합니다. count 하나만 보고 &ldquo;활성화 실패&rdquo; alert를 걸면 조용한 cluster에서 거짓 경보를 낼 수 있습니다.</p>
<p>시작 기준은 다음처럼 잡을 수 있습니다.</p>
<table>
  <thead>
      <tr>
          <th>지표</th>
          <th>기준선</th>
          <th>canary 통과 예시</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>listStream</code> count/rate</td>
          <td>upgrade 전 0</td>
          <td>계획한 cache warm-up·direct list 때 증가</td>
      </tr>
      <tr>
          <td>apiserver/etcd RSS peak</td>
          <td>같은 object set, 같은 load</td>
          <td>peak가 기존 SLO를 넘지 않고 OOM 0회</td>
      </tr>
      <tr>
          <td>watch cache 초기화 p95</td>
          <td>rollout 전 7일</td>
          <td>20% 이상 악화하지 않음</td>
      </tr>
      <tr>
          <td>direct list p99</td>
          <td>동일 selector·namespace</td>
          <td>사용자·controller deadline 안 유지</td>
      </tr>
      <tr>
          <td>list 오류/429</td>
          <td>복구·부하 이벤트 기준</td>
          <td>retry queue와 oldest work age가 같이 악화하지 않음</td>
      </tr>
  </tbody>
</table>
<p><a href="/posts/2026-08-27-kubernetes-137-scale-to-zero-control-plane-resilience-trend/">Scale-to-Zero와 Control Plane 회복력</a>에서 본 429와 <code>Retry-After</code>도 같은 맥락입니다. streaming이 memory peak를 줄여도 API server의 동시성 한계가 사라지지는 않습니다. controller가 429를 즉시 재시도하면 새 read path의 이점을 스스로 상쇄할 수 있습니다.</p>
<h2 id="실무-적용">실무 적용</h2>
<h3 id="1-업그레이드보다-먼저-list-surface를-inventory한다">1) &ldquo;업그레이드&quot;보다 먼저 list surface를 inventory한다</h3>
<p>첫 단계는 cluster 전체에서 어떤 API resource가 크고, 누가 넓게 list하는지를 알아내는 것입니다. Pod, Secret, Event뿐 아니라 CRD의 spec/status와 <code>metadata.managedFields</code> 크기를 표본으로 뽑으세요. operator 하나가 모든 namespace와 모든 CR을 cache에 넣는다면 RangeStream 도입 뒤에도 steady-state memory가 높을 수 있습니다.</p>
<p>아래처럼 workload별로 성격을 나누면 우선순위가 선명해집니다.</p>
<table>
  <thead>
      <tr>
          <th>후보</th>
          <th>RangeStream 효과 기대</th>
          <th>별도 개선 우선</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>많은 Pod/CR을 가진 control plane 재시작</td>
          <td>높음: cache re-initialization peak</td>
          <td>etcd v3.7·apiserver compatibility 확인</td>
      </tr>
      <tr>
          <td>namespace 하나만 보는 controller</td>
          <td>제한적</td>
          <td>namespace/label selector를 먼저 고정</td>
      </tr>
      <tr>
          <td>수 MiB status를 가진 CRD</td>
          <td>부분적</td>
          <td>status 분리·크기 budget·retention 우선</td>
      </tr>
      <tr>
          <td>ad-hoc 전수 inventory job</td>
          <td>부분적</td>
          <td>pagination, QPS, resume cursor, retry budget 우선</td>
      </tr>
  </tbody>
</table>
<p>CRD object 1개가 수 MiB까지 자라면 chunk streaming은 OOM 피크를 낮출 수 있어도, 그 object를 모든 cache와 webhook이 deserialize하는 구조를 건강하게 만들지 않습니다. 플랫폼 팀은 resource 유형별 p95/p99 serialized size, object count, owner, retention 정책을 기록하고, p99 크기가 정한 budget을 넘으면 새 필드·상태 이력을 제한하는 gate를 두는 편이 낫습니다.</p>
<h3 id="2-canary에서는-startup만이-아니라-fallback-list를-재현한다">2) canary에서는 startup만이 아니라 fallback list를 재현한다</h3>
<p>RangeStream의 사용 지점은 watch cache 초기화만이 아닙니다. cache로 서비스할 수 없는 direct etcd list도 대상이므로, canary 계획에는 API server restart와 cache miss 성격의 read를 모두 넣어야 합니다. production과 같은 object 크기 분포를 쓸 수 없다면 최소한 큰 object의 p95/p99, resource 수, controller 동시성을 재현합니다.</p>
<p>권장 순서는 다음입니다.</p>
<ol>
<li><strong>호환성 확인</strong>: control-plane과 etcd의 실제 버전, feature gate, managed provider 지원 범위를 문서로 확인합니다.</li>
<li><strong>기준선 확보</strong>: 7일 동안 RSS peak, OOM, cache initialization duration, list p95/p99, 429·workqueue age를 저장합니다.</li>
<li><strong>한 cluster 또는 control-plane canary</strong>: API server restart와 controller restart를 분리해 실행하고, <code>listStream</code> metric과 memory profile을 확인합니다.</li>
<li><strong>복구 시험</strong>: etcd가 구형이거나 RPC가 <code>Unimplemented</code>인 fallback을 확인해, 기존 Range path에서도 deadline·memory limit이 안전한지 봅니다.</li>
<li><strong>확대 기준</strong>: OOM 0회, initialization p95가 기준선 대비 20% 이상 나빠지지 않음, list p99와 429·workqueue oldest age가 SLO 안이라는 세 조건을 모두 만족할 때만 확대합니다.</li>
</ol>
<p>여기서 rollback은 <code>EtcdRangeStream=false</code>만을 뜻하지 않습니다. 심각한 memory 회귀가 있으면 controller cache scope, list concurrency, oversized CRD rollout도 함께 되돌릴 수 있어야 합니다. <a href="/learning/deep-dive/deep-dive-kubernetes-rollouts/">Kubernetes Rollout 전략</a>의 canary 원칙처럼, feature gate가 아니라 관찰 가능한 behavior를 rollback 단위로 다뤄야 합니다.</p>
<h3 id="3-대시보드는-memory와-사용자-영향-사이를-연결한다">3) 대시보드는 memory와 사용자 영향 사이를 연결한다</h3>
<p>etcd/apiserver memory graph만 보면 절반만 보입니다. control plane에서 list가 느려지면 scheduler·controller의 reconcile가 늦고, 최종적으로 Pod readiness, Service endpoint 갱신, 배포 진행 시간에 영향을 줍니다. 그래서 같은 dashboard에 다음을 같이 놓습니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>object size / listStream rate / apiserver·etcd RSS
</span></span><span style="display:flex;"><span>     -&gt; cache initialization duration / list p99 / 429 rate
</span></span><span style="display:flex;"><span>     -&gt; controller workqueue oldest age / reconcile latency
</span></span><span style="display:flex;"><span>     -&gt; deployment progress deadline / workload readiness
</span></span></code></pre></div><p>예를 들어 RSS peak는 줄었는데 <code>listStream</code> 사용이 거의 없고 list p99가 높다면, RangeStream 문제가 아니라 cache hit 저하나 list client의 selector 누락일 수 있습니다. 반대로 streaming count가 늘고 OOM은 사라졌지만 workqueue oldest age가 계속 증가하면, request 동시성이나 downstream 처리량을 따로 봐야 합니다. 관측을 한 개의 &ldquo;성능 개선&rdquo; 수치로 접으면 원인별 조치가 섞입니다.</p>
<h2 id="트레이드오프주의점">트레이드오프/주의점</h2>
<p>첫째, RangeStream은 v1.37에서 Beta입니다. production 사용은 가능하지만, 기능 gate와 etcd 버전, provider가 지원하는 control-plane 조합을 명시적으로 확인해야 합니다. 둘째, 스트리밍은 peak memory를 줄이는 기술이지 average memory·etcd I/O·network 비용을 0으로 만들지 않습니다. object 전체를 읽는 필요 자체가 남아 있다면 list surface와 data model을 고쳐야 합니다.</p>
<p>셋째, fallback이 자동이라는 이유로 compatibility 검증을 생략하면 안 됩니다. 구형 etcd와도 동작할 수 있지만, 운영 목표가 &ldquo;memory peak 감소&quot;라면 fallback은 통과가 아니라 관찰·추가 업그레이드가 필요한 상태입니다. 넷째, metric cardinality를 늘려 resource 이름·namespace를 무한 label로 넣어 RangeStream 효과를 측정하려 하면 관측성 비용을 새로 만듭니다. <a href="/posts/2026-09-03-opentelemetry-metric-cardinality-overflow-data-completeness-trend/">Metric Cardinality Limit과 데이터 완전성</a>의 budget 원칙을 적용해 상위 resource type과 sampled exemplar를 분리하세요.</p>
<h2 id="체크리스트-또는-연습">체크리스트 또는 연습</h2>
<h3 id="체크리스트">체크리스트</h3>
<ul>
<li><input disabled="" type="checkbox"> kube-apiserver와 etcd가 각각 v1.37·v3.7 이상이며 provider 지원 정책도 확인했다.</li>
<li><input disabled="" type="checkbox"> <code>EtcdRangeStream</code> gate, <code>listStream</code> metric, fallback Range path를 별도 상태로 기록한다.</li>
<li><input disabled="" type="checkbox"> resource별 object count와 serialized size p95/p99, owner, retention 정책을 inventory했다.</li>
<li><input disabled="" type="checkbox"> canary에서 API server restart·controller restart·direct list를 각각 재현했다.</li>
<li><input disabled="" type="checkbox"> RSS peak, OOM, cache initialization p95, list p99, 429, workqueue oldest age를 기준선과 비교했다.</li>
<li><input disabled="" type="checkbox"> failure 시 gate뿐 아니라 cache scope·list concurrency·oversized CRD 변경을 되돌릴 owner와 절차가 있다.</li>
</ul>
<h3 id="연습">연습</h3>
<p>staging에서 object 크기가 큰 CRD 하나와 Pod collection 하나를 고릅니다. 업그레이드 전후로 API server restart를 한 번씩 수행하고 <code>listStream</code> count, etcd/apiserver RSS peak, watch cache 초기화 시간, controller workqueue oldest age를 표로 비교하세요. <code>listStream</code>이 0이면 성능 결론을 내리지 말고 etcd 버전과 해당 read가 실제로 발생했는지부터 확인합니다.</p>
<h2 id="관련-글">관련 글</h2>
<ul>
<li><a href="/posts/2026-08-27-kubernetes-137-scale-to-zero-control-plane-resilience-trend/">Kubernetes v1.37: Scale-to-Zero와 Control Plane 회복력</a></li>
<li><a href="/posts/2026-09-02-kubernetes-metrics-api-dra-resource-contract-trend/">Kubernetes Metrics API와 DRA 리소스 계약</a></li>
<li><a href="/learning/deep-dive/deep-dive-kubernetes-rollouts/">Kubernetes Rollout 전략</a></li>
<li><a href="/posts/2026-09-03-opentelemetry-metric-cardinality-overflow-data-completeness-trend/">OpenTelemetry Metric Cardinality Limit</a></li>
</ul>
]]></content:encoded></item></channel></rss>