Kubernetes 解体新書Part 2 ネットワーク

kube-proxy の世代交代と eBPF

通信の転送ルールを調べる処理と、更新する処理。規模が大きくなるときの負担を、実装ごとに比べます。

  • L0 ブラウザだけ
  • 更新 2026年10月6日

Service の向き先が変わると、通信を転送するルールも更新する必要があります。 Service や Pod が増えるほど、ルールを調べる処理と、書き換える処理の負担が気になってきます。

この章では、その 2 つの処理を分けて見ます。 iptables、IPVS、nftables、eBPF で何が変わるのかを、同じ通信の例で比べていきます。

GOAL この章のゴール

  • ルールの照合と更新、それぞれの負担を区別できる
  • iptables、IPVS、nftables、eBPF の違いを説明できる
  • 方式を選ぶときに、規模や必要な機能を確認できる

年表

Service の書き換え方式の年表: userspace、iptables、ipvs、eBPF、nftables2015 年の userspace モードから、2016 年に iptables が既定、2018 年に ipvs が GA、2019 年に Cilium の kube-proxy replacement、2023 年に nftables が alpha、2025 年に GA。userspace は 1.26 で削除、ipvs は 1.35 で非推奨。2026 年時点の kube-proxy の既定は iptables。userspace1.0 (2015)1.26 で削除iptables1.2 で既定 (2016)1.37 でも既定ipvs1.11 で GA (2018)1.35 で非推奨eBPF (Cilium)1.6 で kube-proxy 代替 (2019)nftables1.29 alpha → 1.33 GA (2025)将来の既定kube-proxy のモード (上段)kube-proxy の外 (下段)
図 1kube-proxy のモードと、その外で生まれた eBPF 実装。2026 年時点の kube-proxy の既定は iptables で、将来 nftables に変わる。

kube-proxy は Kubernetes 1.0 (2015 年) の時点では userspace モード で、ノード上のプロセスが TCP 接続を受けて Pod に中継していました。 1.2 (2016 年) で iptables モードが既定になり、書き換えをカーネルの netfilter に任せる形に変わります。 2018 年の 1.11 で ipvs モードが GA になり、2019 年 8 月に Cilium 1.6 が「100% kube-proxy replacement」を掲げて kube-proxy を丸ごと置き換える機能を出しました。 nftables モードは 2023 年の 1.29 で alpha、2025 年の 1.33 で GA です。 userspace モードは 1.26 で削除され、ipvs モードは 1.35 で非推奨になりました。

iptables モードが遅くなる 2 つの理由

iptables モードはクラスタ規模に比例して遅くなります。 原因は 2 つあり、それぞれ別の場所で効きます。

最初のパケットが Service を見つけるまで: iptables は上から順に照合、nftables と eBPF は map を 1 回引くiptables の KUBE-SERVICES では、宛先が 1 万番目の Service なら 1 万行を順に照合する。nftables の verdict map と eBPF map はハッシュで 1 回の表引き。iptables: 線形スキャンKUBE-SERVICES-d 10.96.0.1 --dport 443 → KUBE-SVC-A-d 10.96.0.10 --dport 53 → KUBE-SVC-B-d 10.96.12.34 --dport 80 → KUBE-SVC-C… (Service の数だけ続く) …-d 10.96.200.9 --dport 80 → KUBE-SVC-ZN 行宛先 10.96.200.9 のパケットは N 回比較される(1 接続につき最初の 1 パケット。以降は conntrack)nftables / eBPF: map の表引きvmap @service-ips / cilium_lb4_services_v2hash(宛先 . proto . port)10.96.0.1 . tcp . 443 → chain A10.96.12.34 . tcp . 80 → chain C10.96.200.9 . tcp . 80 → chain Z…1 回Service が何万あっても表引きは 1 回
図 2最初のパケットは KUBE-SERVICES を上から順に照合される。Service が N 個あれば最悪 N 回の比較。map ならハッシュで 1 回。

1 つ目は 照合の線形スキャン です。 nftables モードを紹介した Kubernetes のブログによれば、iptables モードのルールセットの最上位には「Service の IP とポートごとに 1 つずつ照合ルール」が並び、カーネルがパケットを全 Service のルールと照合する時間は Service 数に比例 (O(n)) します。 ただし比較が走るのは接続の最初の 1 パケットだけです。 第 2 章で見たとおり、nat 型のチェーンを通るのは接続の最初のパケットだけで、2 パケット目以降は conntrack の表に乗り、ルールを通らずに書き換えられます。 そのため影響が出るのは新規接続のレイテンシで、確立済みの接続のスループットは落ちません。

endpoint が 1 つ変わったときの更新コスト: iptables は Service 数に比例した更新、nftables と eBPF は差分だけiptables モードの kube-proxy は、1.26 以降は変更の無いルールを飛ばすが、iptables-restore の制約で更新は Service 数に比例する。nftables モードは変わった map 要素とチェーンだけを送る。eBPF は map のエントリを 1 つ書き換える。iptablesルールセット全体KUBE-SVC-… × NKUBE-SEP-… × Miptables-restore で送る量がService 数に比例 (1.26 以降も)O(Service 数)nftablestable ip kube-proxymap service-ipschain service-…変わった要素とチェーンだけ送るO(変わった Service と endpoint)eBPFeBPF maplb4_backends[id]= 10.244.0.40:80エントリを 1 つbpf_map_updateO(1)
図 3endpoint が 1 つ変わったとき、iptables は iptables-restore の制約で Service 数に比例した更新を送る。nftables と eBPF は変わった分だけ送る。

2 つ目は 更新のコスト です。 同じブログは、iptables モードは「もともと更新のたびに全ルールを書き直していて、1.26 から変更の無いルールの大半を飛ばすよう改善したが、iptables-restore という API の制約で、更新のサイズはやはり Service 数に比例した」と説明しています。 公式ドキュメントも、Pod と Service が数万あるクラスタでは、Service や EndpointSlice が変わったときにカーネルへのルール更新に時間がかかる、と書いています。 その間は新しい Pod へ転送されない、消えた Pod へ転送され続けるといった遅れが出ます。 minSyncPeriod で更新をまとめる設定がありますが、まとめた分だけ反映が遅れます。

ipvs: ハッシュで照合を解いた世代

IPVSアイピーブイエスIP Virtual Server。Linux カーネルに古くからある L4 ロードバランサーで、宛先 IP:ポートから backend の一覧をハッシュテーブルで引く。用語集で見る モードは、照合の問題をハッシュテーブルで解きました。 公式ドキュメントは「netfilter の hook を使う点は iptables モードと似ているが、ハッシュテーブルをデータ構造として使う」と説明しています。 宛先の検索は Service の数によらず一定時間で済み、振り分けアルゴリズムをラウンドロビンや最小接続数から選べる利点もあります。

一方で ipvs モードは netfilter を完全には離れていません。 公式ドキュメントは「カーネルの IPVS と iptables の両方の API を使う」と書き、2018 年の GA 時のブログは、パケットのフィルタリング、SNAT、NodePort の処理に iptables (と ipset) を併用すると説明しています。 さらに公式ドキュメントは、IPVS の API が Kubernetes の Service API とうまく噛み合わず、Service の全ての細かい挙動を正しく実装できなかった、として ipvs モードを 1.35 で非推奨にし、1.40 で既定で無効、1.43 で削除する予定を示しています。

nftables: map と差分更新

nftables モードは、照合と更新の両方を netfilter の中で解きます。 第 5 章で見た service-ips の map は、宛先 IP とプロトコルとポートの組をキーにした verdict map で、ブログの言い方では「ルールは 1 本だけで、map の表引きはほぼ O(1) なので、パケットの処理時間はクラスタの規模によらずほぼ一定」です。 更新は map の要素とチェーンの単位で行えるので、「更新のサイズは前回の同期から変わった Service と endpoint の数に比例する」だけで済みます。 公式ドキュメントは、nftables モードを iptables と ipvs 両方の後継と位置づけ、ipvs からの移行先として推奨しています。

ここまでの 3 世代を、同じ構成で見比べます。 Service を増やしたときに iptables の行数がどう伸び、nftables の map がどう変わるかを確認してください。

  • default/web10.96.12.34:80 · NodePort 30080
    10.244.0.23:808010.244.0.31:8080
  • default/api10.96.40.7:443
    10.244.0.52:8443

eBPF (Cilium) (15 行)

# cilium-dbg service list  (cilium-agent の Pod の中で)
ID   Frontend               Service Type   Backend
1    10.96.12.34:80/TCP     ClusterIP      1 => 10.244.0.23:8080/TCP (active)
                                           2 => 10.244.0.31:8080/TCP (active)
2    0.0.0.0:30080/TCP      NodePort       1 => 10.244.0.23:8080/TCP (active)
                                           2 => 10.244.0.31:8080/TCP (active)
3    10.96.40.7:443/TCP     ClusterIP      1 => 10.244.0.52:8443/TCP (active)

# cilium-dbg bpf lb list  (eBPF map cilium_lb4_services_v2 / cilium_lb4_backends_v3 の中身)
SERVICE ADDRESS         BACKEND ADDRESS (REVNAT_ID) (SLOT)
10.96.12.34:80/TCP      0.0.0.0:0/TCP (1) (0) [ClusterIP, non-routable]
                        10.244.0.23:8080/TCP (1) (1)
                        10.244.0.31:8080/TCP (1) (2)
0.0.0.0:30080/TCP       0.0.0.0:0/TCP (1) (0) [ClusterIP, NodePort, non-routable]
                        10.244.0.23:8080/TCP (1) (1)
                        10.244.0.31:8080/TCP (1) (2)
10.96.40.7:443/TCP      0.0.0.0:0/TCP (2) (0) [ClusterIP, non-routable]
                        10.244.0.52:8443/TCP (2) (1)

# スロット 0 がサービス自身 (backend 数などのメタ)、1.. がバックエンド。
# tc / socket の eBPF プログラムが接続ごとにスロットを 1 つ選び、宛先を書き換える。
# nft list ruleset を見ても、kube-proxy 相当のルールはどこにも無い。

Service や Pod を増減すると、3 つの表現が同時に変わります。iptables は Service と endpoint の数だけルールとチェーンが伸び、nftables は map の要素と Service ごとのチェーンが増え、eBPF は map のスロットが増えます。

eBPF: netfilter の外で解く

eBPF が取り付く 3 つの場所: socket (connect 時)、tc (veth / NIC の直後)、XDP (NIC ドライバ)パケットがアプリから NIC へ降りていく道筋に沿って、socket 層、tc 層、XDP 層の 3 箇所に eBPF プログラムを取り付けられる。netfilter (nft) はその途中にあり、Cilium の Service 転送はそこを通らない。アプリ (Pod / ホスト)connect()socket LBconnect() の宛先を書き換えClusterIP 宛てのパケットがそもそも作られないIP スタックルーティングnetfilter (nft)kube-proxy はここtc eBPFveth (lxc…) と NIC の直後Pod からの NodePort / ClusterIP、外から入った NodePort を DNATXDP eBPFNIC ドライバ (最速)NodePort / LB の転送をカーネルに入る前に処理 (DSR と相性)NIC (eth0)
図 4eBPF は socket、tc、XDP の 3 箇所に取り付けられる。netfilter はその途中にあり、Cilium の Service 転送はそこを通らない。

Cilium や Calico の eBPF モードは、書き換えを netfilter の外で行います。 Cilium のドキュメントによれば、eBPF プログラムを取り付けられる場所は 3 つあります。 NIC ドライバの最も早い段階で動く XDP、ネットワークスタックが初期処理を終えた直後に NIC や veth ごとに動く tc ingress/egress、そして TCP のイベントや send のたびに動く socket 層です。 表は eBPF map (ハッシュ) で、照合は 1 回の表引き、更新は map のエントリ 1 つの書き換えです。 この構成で kube-proxy を外すのが、Cilium の kube-proxy replacement です。

netfilter の外に出たことで、次の機能も使えます。

  • socket LB:connect() などのシステムコールの時点で宛先を backend に差し替える。アプリは Service に接続したつもりでも、ソケットは最初から backend に接続している。
  • DSR (Direct Server Return):NodePort や LoadBalancer で受けたノードが別ノードの Pod へ転送するとき、SNAT せず、backend が Service の IP とポートを送信元にしてクライアントへ直接返す。第 6 章の「余計なホップ」が戻り方向で消え、クライアントの送信元 IP も backend に残る。
  • XDP:NIC ドライバの段階で NodePort / LoadBalancer の転送を処理する。ドライバが XDP に対応している必要がある。
  • Maglev:一貫性ハッシュで backend を選び、ノードが増減しても既存の接続が同じ backend に当たりやすくする。外部からの (N-S) 通信に適用される。

kube-proxy は消えるのか

2026 年でも、kube-proxy は Kubernetes 本体に残っていて、多くのクラスタで動いています。 理由は 3 つあります。

  1. Pod ネットワークの実装と独立している:公式ドキュメントは「Kubernetes はサービスプロキシの既定の実装として kube-proxy を提供し、一部の Pod ネットワーク実装は代わりに自前のプロキシを使う」と書いている。Flannel のように Service の書き換えを持たない実装では kube-proxy が要る。
  2. カーネル要件が緩い:Cilium 1.20 はカーネル 5.10 以降と BTF などの eBPF 関連設定を前提にし、nftables モードも 5.13 以降が要る。iptables モードはそれより古いカーネルでも動く。
  3. Service の新機能が先に入る:GKE のドキュメントは「kube-proxy は Kubernetes コミュニティが保守しているので、Service の新機能は Dataplane V2 の Cilium より先に kube-proxy に実装されやすい」と注意書きを置いている。

ふりかえり

Qiptables モードの線形スキャンが影響するのはどれ?

KUBE-SERVICES の照合は接続ごとに最初の 1 パケットだけで、2 パケット目以降は conntrack の表で書き換えられます。影響は新規接続のレイテンシに出ます。

Qnftables モードが iptables モードに対して改善したことは?

nftables モードも netfilter の中で動きます。verdict map で Service を 1 回で引き、更新は map の要素とチェーンの単位で行えるので、更新のサイズが変更分に比例します。

QDSR (Direct Server Return) が解決することは?

SNAT の代わりに backend から直接クライアントへ返させるので、戻り方向で受けたノードを経由せず、クライアント IP も保たれます。eBPF 実装が netfilter の外で動くために可能になった機能です。

参考