kube-proxyとServiceの仮想IP実装(iptables/IPVS/eBPF)
ClusterIPに応答するホストが1台も存在しないのにPod間通信が成立する理由を原理から理解。iptables/IPVS/eBPFがどうDNATで宛先を差し替え、conntrackが戻りパケットを正しく復路へ戻すのかまで押さえられます。
- 1.ClusterIPは実体のない仮想IP。応答するNICもPodも存在せず、kube-proxyが全ノードのカーネルに仕込んだDNATルールが、宛先をその場で実Pod IPへ書き換えることで通信が成立する。
- 2.iptablesはルールを線形評価しPod数に比例して遅くなる。IPVSはハッシュ表で定数時間に近く、Cilium等のeBPFはカーネル内mapで照合しiptables連鎖そのものを置き換える。
- 3.DNATで宛先を書き換える以上、戻りパケットの逆変換が必須。これを担うのがconntrackで、フローを記録して復路を自動でSNAT/逆DNATする。conntrack表の枯渇が大規模クラスタの実害になる。
kubectl get svc が返す ClusterIP に ping は通らず、その IP を持つ NIC もホストもクラスタ内に1つも存在しません。それでも curl <ClusterIP> は実 Pod に届き、複数 Pod へ分散される。本稿は、この「実体のない仮想 IP」がどうロードバランスされるのかを、kube-proxy が各ノードのカーネルに仕込むパケット書き換えの原理から解きます。
ClusterIP は「宛先書き換えの引き金」でしかない
ClusterIP は IPAM 上の予約値にすぎず、誰も Listen していません。Service の本体は EndpointSlice(背後の実 Pod IP とポートの集合、例 {10.0.1.5:8080, 10.0.2.7:8080})で、ClusterIP はこの集合への間接参照キーです。
kube-proxy は API サーバーを watch し、Service と EndpointSlice の変化を全ノードのカーネルのパケット処理ルールへ翻訳し続けます。つまり ClusterIP 宛のパケットは、どのノードから出ても「カーネルの NAT 段で実 Pod IP へ書き換えられる(DNAT)」。仮想 IP の正体は、ルーティング先のホストではなく書き換えの引き金です。
誤解されがちですが、通常の ClusterIP 経路で kube-proxy 自身はパケットを1バイトも転送しません。kube-proxy の仕事は「ルールをカーネルに書く制御プレーン」であり、実際の書き換えと分散は Netfilter / IPVS / eBPF というカーネル機構が担います。だから kube-proxy が一時停止しても、既存ルールが残る限り通信は流れ続けます。
モード1: iptables — DNAT を確率で分岐する
iptables モードでは、ClusterIP 宛トラフィックを Netfilter の nat テーブル(主に PREROUTING と OUTPUT から飛ぶ KUBE-SERVICES チェーン)で捕捉します。ロードバランスは statistic マッチによる確率分岐で実装されます。Pod が3つなら次のような連鎖になります。
KUBE-SERVICES
└─(宛先=ClusterIP:port)→ KUBE-SVC-XXXX
├─ probability 0.3333 → KUBE-SEP-A (DNAT to 10.0.1.5:8080)
├─ probability 0.5000 → KUBE-SEP-B (DNAT to 10.0.2.7:8080)
└─ (残り) → KUBE-SEP-C (DNAT to 10.0.3.9:8080)
確率が 1/3, 1/2, 1 と増えるのは、各ルールが前段で外れた残余に対して評価されるためです。1番目で 1/3、外れた 2/3 のうち 1/2(=全体の 1/3)、最後は残り全部。結果として各 Pod へ等確率で振り分けられます。最終段の KUBE-SEP-* が実際の DNAT を行い、宛先 IP/ポートを実 Pod に書き換えます。
本質的な弱点は 線形評価です。Service とエンドポイントが増えるほどチェーンは長くなり、ルール更新は表全体の置換に近いコストを払います。エンドポイント数が N に対し、マッチ計算もルール再構築も N に比例して重くなる。数千 Service 規模では制御プレーンの収束遅延が顕在化します。
モード2: IPVS — ハッシュ表で定数時間に近づける
IPVS モードは、カーネルの L4 ロードバランサ(IP Virtual Server) を流用します。kube-proxy は ClusterIP をダミーインターフェース kube-ipvs0 に付与し、各 Service を IPVS の「仮想サーバー」、各 Pod を「リアルサーバー」として登録します。
決定的な違いはデータ構造です。iptables がルールの連鎖を上から舐めるのに対し、IPVS は宛先をハッシュ表で引きます。照合コストはエンドポイント数にほぼ依存せず定数時間に近い。さらに分散方式を選べます。
| スケジューラ | 分散の基準 | 向く場面 |
|---|---|---|
| rr (round-robin) | 順番に均等配分 | 汎用・既定的な均し |
| lc (least-conn) | 接続数が最小のPodへ | 処理時間がばらつくワークロード |
| sh (source-hash) | 送信元IPのハッシュで固定 | 簡易セッションアフィニティ |
ただし IPVS は「分散と DNAT」を担うだけで、Netfilter から完全に独立はしません。マスカレード(SNAT)やパケットフィルタは依然 iptables/ipset 側に少数のルールとして残ります。要点は、O(N) の弱点を握っていた分散・照合部分を IPVS の表に追い出したことです。大規模クラスタで IPVS が推奨されるのはこの一点に尽きます。
モード3: eBPF — iptables 連鎖そのものを置き換える
Cilium に代表される eBPF データプレーンは、発想を一段進めます。確率分岐や IPVS 表を使うのではなく、eBPF プログラムをソケットや tc/XDP フックにアタッチし、カーネル内 map で Service→エンドポイントを直接照合して書き換えます(eBPF の仕組みは /devops/ebpf-observability/)。
最も効くのが socket-level LB です。Pod 内プロセスが ClusterIP へ connect() したその瞬間、connect システムコールにアタッチした eBPF が宛先を実 Pod IP へ差し替えます。パケットがネットワークスタックを下る前に宛先が確定するため、PREROUTING での DNAT も戻りパケットの逆変換も不要になり、データ経路上の iptables 連鎖を丸ごとバイパスできます。
本質はどれも「ClusterIP を実 Pod IP に書き換える」ことで同じです。違うのは引きのコストと場所だけ。iptables はルール連鎖を線形に評価し(O(N))、IPVS はハッシュ表で定数時間に寄せ、eBPF は connect 時点で map を引いて連鎖自体を消す。スループットとルール収束時間のトレードオフがそのまま現れます。
conntrack: DNAT を成立させる「逆変換の記憶」
DNAT には必ず対になる問題があります。クライアント Pod は ClusterIP:port 宛に送ったつもりなのに、戻りパケットの送信元は実 Pod IP です。送信元が要求先と食い違えば、クライアントの TCP スタックは「知らない相手からの返信」として破棄します。
これを救うのが conntrack(接続追跡) です。最初のパケットで DNAT を行う際、conntrack は5タプル(プロトコル・送信元/宛先 IP・ポート)でフローを表に記録します。戻りパケットが来ると、この表を引いて送信元を ClusterIP:port に自動で逆 DNATし、必要なら経路上で SNAT も補正する。つまり「行きで書き換えた変換を、帰りに巻き戻す」ための記憶装置です。同一フローのパケットは確率分岐を再評価せず、表のエントリに従って同じ Pod へ固定される——これがコネクション一貫性の出どころでもあります。
行き: src=PodA dst=ClusterIP:80
└ DNAT → dst=10.0.2.7:8080 + conntrack に (PodA↔10.0.2.7) を記録
戻り: src=10.0.2.7:8080 dst=PodA
└ conntrack 照合 → src を ClusterIP:80 に逆変換 → PodA は整合と判断
conntrack 表(nf_conntrack_max)は有限です。Service を多用するマイクロサービス群(/devops/microservices/)で短命コネクションが大量に生まれると表が溢れ、新規フローのパケットが黙って落ちる。症状は「断続的な接続失敗・タイムアウト」で、アプリのログには原因が出にくい。UDP(とくに DNS)のエントリ滞留や、リトライ増幅(/devops/retry-backoff-jitter/)で悪化します。eBPF の socket LB が魅力的なのは、connect 時点で書き換えてしまえばデータ経路の conntrack 依存を減らせる点にもあります。
外部 LB からノードに入った後、別ノードの Pod へ転送する際は SNAT が要ります(戻りを同じノードへ呼び戻すため)。だが SNAT するとクライアント送信元 IP がノード IP に潰れ、Pod からは本当の発信元が見えません。externalTrafficPolicy: Local はローカル Pod のみに振って SNAT を避け送信元を保ちますが、その代わり Pod のいないノードはトラフィックを捨てるため負荷が偏ります。送信元保持と均等分散はトレードオフです。
まとめ
- ClusterIP は実体のない仮想 IP で、応答するホストは存在しない。kube-proxy が全ノードのカーネルに仕込んだ DNAT ルールが宛先を実 Pod IP に書き換えることで通信が成立する。kube-proxy 自身はデータを転送しない制御プレーンである。
- 3モードは「同じ DNAT をどこで引くか」の違い。iptables は確率分岐の線形評価で O(N)、IPVS はハッシュ表で定数時間に近づけ、eBPF は connect 時点で map を引き iptables 連鎖を消す。規模が上がるほど後者が有利になる。
- DNAT は conntrack による逆変換の記憶とセットで初めて成立する。conntrack はフローを表に記録し戻りパケットを逆 DNAT/SNAT する。この表の枯渇が大規模クラスタの断続的接続失敗の正体になりやすい。
- 送信元 IP 保持(
externalTrafficPolicy: Local)と均等分散は両立しない。サービス間通信の信頼性設計(/devops/circuit-breaker-bulkhead/)と合わせ、どのモードと方針を選ぶかが運用の勘所になる。
DevOps/インフラ Article
kube-proxyとServiceの仮想IP実装(iptables/IPVS/eBPF)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
Kubernetes
比較で見る軸
難易度: advanced / カテゴリ: DevOps/インフラ / タグ数: 6
導入後に効く点
iptablesはルールを線形評価しPod数に比例して遅くなる。IPVSはハッシュ表で定数時間に近く、Cilium等のeBPFはカーネル内mapで照合しiptables連鎖そのものを置き換える。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- DevOps/インフラ
- タグ数
- 6
判断チェックリスト
- 自社の用途が「Kubernetes / kube-proxy」に近いか確認する。
- 強みである「ClusterIPは実体のない仮想IP。応答するNICもPodも存在せず、kube-proxyが全ノードのカーネルに仕込んだDNATルールが、宛先をその場で実Pod IPへ書き換えることで通信が成立する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。