kube-apiserver が etcd から collection を丸ごと読む経路が、v1.37 で変わる。EtcdRangeStream feature gate が beta に上がって既定で有効になり、etcd が v3.7 以上なら、ページング付きの Range の代わりに streaming の RangeStream RPC が使われる。etcd が v3.7 より古ければ API server は起動時にそれを検出し、従来の経路を使い続ける。影響があるのは etcd v3.7 以上と組んだクラスタだけで、それ以外では読み出しの挙動は何も変わらない。
変更の狙いは、Pods のように数が多いか一つ一つが大きいリソースを読むときの、API server と etcd のメモリ使用量を下げて、ピークを予測しやすくすることだ。ブログを書いたのは Google の Jeffrey Ying で、公開日は 2026年9月1日。
今の経路は listStream のカウンタで分かる
API server は streaming で読んだ操作を、etcd metrics の独自の operation ラベルで記録する。次のカウンタが 0 でなければ RangeStream が使われている。
etcd_request_duration_seconds_count{operation="listStream"}
0 のままなら、API server はまだページング付きの Range を通っている。ブログが最も可能性が高い理由として挙げるのは、etcd が v3.7 より古いことだ。v1.37 に上げた直後に見るなら、この一つで足りる。
| kube-apiserver | etcd | etcd からの読み出し経路 |
|---|
| v1.37 以上、gate 有効(既定) | v3.7 以上 | RangeStream |
| v1.37 以上、gate 有効(既定) | v3.7 未満 | Range(ページング)に自動で戻る |
| v1.37 以上、gate 無効 | どのバージョンでも | Range(ページング) |
出典:Kubernetes ブログ、2026年9月1日公開「etcd RangeStream Cuts Memory Use on Large List Reads」の Requirements 節
key 数で切ったページは、オブジェクトの大きさを知らない
API server は list と watch の大半を、メモリ上の watch cache から返す。その cache を埋めるには、起動時と再初期化のたびに、リソースの全状態を etcd から読む必要がある。オブジェクトが多いか大きいリソース、たとえば Pods では、この読み出しが高くつく。
読み出しはすでにページングされていた。collection を一度に要求せず、決まった数の key ずつ etcd に頼む形だ。ただ key 数で区切られたページはオブジェクトのサイズを見ていないので、大きなオブジェクトが並ぶページは依然として大きくなりうる。メモリ使用量が読めなくなり、オブジェクトサイズと同時読み出しの組み合わせが悪ければ OOM に至りうる。
もう一つの問題は、同じペイロードが両側に同時に載ることだ。etcd の unary な Range はページを丸ごと組み立ててから送り、API server はデコードの間それを保持する。コストの大半は etcd 側に落ちる。streaming が最も効くのもそこだ。
RangeStream は同じ RangeRequest を chunk に分けて流す
etcd v3.7 が追加した RangeStream RPC は、Range と同じ RangeRequest を受け取り、同じ結果集合を返す。違いは組み立て方で、応答を先に全部作らず、chunk に分けて順に流す。chunk のサイズは返す値に合わせて適応的に調整されるので、大きなオブジェクトの collection は key 数ではなくバイト数で区切られ、メモリはページが完成するまで抱えられずに、stream が進むにつれて解放される。
gate が有効なとき、API server は etcd から collection を丸ごと読む場面すべてで RangeStream を使う。watch cache の初期化に加え、list 要求を cache から返せず etcd を直接読むフォールバック経路も含まれる。どちらの場合も API server は chunk を届いた順にデコードし、次を取りに行く前に解放するので、collection 全体を一度に持つ側はどちらにもない。
止める手段は kube-apiserver の gate 一つで、起動引数で落とす。
--feature-gates=EtcdRangeStream=false
API server は etcd の対応を起動時に解決し、加えて呼び出しが Unimplemented を返した場合は実行時にも Range に戻る。etcd の更新が kube-apiserver より遅れているクラスタでも、手動で gate を触る必要はない。
v1.37 では Storage Version Migration も GA になった。どちらも etcd に触る変更だが、RangeStream が変えるのは読み出しの経路で、返る結果は Range と同じだ。
減る量の数字はブログにない
メモリがどれだけ減るかの計測値は、ブログに載っていない。あるのは減らす、ピークを予測しやすくするという説明だけで、削減率やベンチマークは示されていない。効果は自分のクラスタで、API server と etcd のメモリを更新前後で見るしかない。beta から GA へ上がる時期も書かれていない。
chunk サイズは適応的に調整されるとだけあり、運用者が上限を指定できるかは読み取れない。control plane をマネージドで使っていて kube-apiserver の metrics を取れているなら、上の listStream カウンタを見れば自分の etcd が v3.7 に達しているかの間接的な手掛かりになる。ただしゼロの理由を etcd のバージョンだと決めつけるのは早い。ブログ自体、それを最も可能性が高い原因と位置づけているだけだ。
etcd を自分で運用していて v3.7 未満なら、今日は何も変わらない。API server の再起動時のメモリの山が課題なら、先に動くのは etcd 側の更新だ。すでに v3.7 以上なら、v1.37 への更新で経路は勝手に切り替わる。比べるのはカウンタとメモリのピークの、更新前後の差だ。問題が出たときの退避先は gate の無効化になる。