Kubernetes環境でCilium CNIを導入:本番環境でkube-proxyをeBPFに完全リプレース

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

現場の課題:iptablesが直面するスケーラビリティの限界

昨年の秋、当社のプライマリKubernetesクラスターは40台のベアメタルワーカーノード上で500以上のマイクロサービスを稼働させる規模にまで拡大しました。全体のCPUおよびメモリ使用率は45%程度と安定しているように見えましたが、チェックアウトサービスや決済サービスで突発的なレイテンシスパイクが頻発するようになりました。定常的な15,000 req/secのバーストトラフィック発生時には、通常8msだったp99レスポンスタイムが240ms以上にまで跳ね上がっていたのです。

監視ダッシュボードでは、以下の3つの深刻な問題が浮き彫りになっていました。

  • アプリケーションのスループットがそれほど高くない場合でも、ワーカーノードのCPUでカーネル空間の処理時間(system %)が高止まりしていた。
  • Horizontal Pod Autoscaler(HPA)によってレプリカが追加されるたびに、Serviceのルーティング更新が全ノードに行き渡るまでに20〜45秒もかかっていた。
  • パケットドロップの調査には、インシデント発生中にエフェメラルなPodへtcpdumpをアタッチする必要があり、貴重なデバッグ時間が浪費されていた。

頻繁なPodの生成・破棄(churn)により、ネットワークサブシステムが限界に達していました。

根本原因:大規模環境でkube-proxyが破綻する理由

この問題を解決するため、標準的なKubernetesネットワークの内部挙動を詳細に調査しました。デフォルトのkube-proxyは、Linuxのiptables(またはIPVS)を設定することで、インバウンドのServiceトラフィックを各Podエンドポイントへルーティングします。

1. O(N) の線形・逐次パケット探索

iptablesでは、Linuxカーネルがファイアウォールルールを先頭から順番に走査します。クラスター内に5,000個のバックエンドエンドポイントが存在する場合、1つの受信パケットが適切な宛先に到達するまでに数千回ものルール評価が行われる可能性があります。この線形探索が、カーネルのネットワーク高速処理パスで膨大なCPUリソースを消費します。

2. テーブル全体の同期ロック

Podが起動、停止、またはReadyプローブに失敗するたびに、kube-proxyはiptablesの全ルールリストを再生成して書き直します。急激なオートスケーリングが発生すると、カーネルはアプリケーショントラフィックのルーティングではなく、メモリバッファのロックやコピー処理に貴重なCPUサイクルを費やすことになります。

3. Conntrackテーブルの枯渇とサイレントドロップ

従来のkube-proxyは、ServiceのNAT処理をLinuxのコネクショントラッキング(conntrack)に大きく依存しています。同時接続TCPセッション数が200,000を超えると、テーブルの割り当て上限に達してしまいます。その結果、カーネルはnf_conntrack: table full, dropping packetエラーを出力し、アクティブなクライアント接続をサイレントに破棄し始めます。

選択肢の比較:Calico vs IPVS vs Cilium eBPF

本番ノードに変更を加える前に、10,000個のServiceエンドポイントと20,000の同時RPS環境を想定し、3つのネットワークアーキテクチャでベンチマーク検証を実施しました。

アーキテクチャ指標 Calico(標準 iptables) kube-proxy(IPVSモード) Cilium CNI(eBPFネイティブ)
サービス検索の計算量 O(N) 逐次探索 O(1) IPVS ipsetハッシュ O(1) BPF Map直接メモリルックアップ
パケットのフックポイント Netfilterフックチェーン Netfilter + IPVSフック ソケット層およびTC / XDP直接アタッチ
ルール更新の遅延(5,000エンドポイント) 約12〜30秒 約2〜5秒 < 50ミリ秒(アトミックなMap入れ替え)
レイヤー7ネットワークテレメトリ Envoyサイドカーの注入が必要 組み込み機能なし Hubble eBPFによるネイティブ対応
大規模環境でのカーネルオーバーヘッド 高いCPU消費 中程度 最小(ノードのシステムCPUが約35%削減)

IPVSはハッシュセットを用いることで線形探索の問題を解決しますが、依然としてNetfilterのconntrackによるオーバーヘッドが残ります。一方、Ciliumは別のアプローチを取ります。extended Berkeley Packet Filter(eBPF)を活用し、Linuxのソケット層やTraffic Control(tc)フックでサンドボックス化されたプログラムを直接実行します。これにより、パケットはNetfilterスタックを完全にバイパスします。

実践手順:Strictモードでkube-proxyを完全置換するCiliumの導入

ステージング環境および本番クラスターにおいて、kube-proxyを完全に置き換える形でCiliumを導入しました。以下が実際の導入手順です。

ステップ1:カーネルバージョンの確認とBPFファイルシステムの初期マウント

Ciliumの動作にはモダンなeBPFヘルパーが必要です。Linuxカーネル5.4以上(5.15+ LTSまたは6.xを推奨)を使用してください。また、全クラスターノードでBPFファイルシステム(bpffs)がマウントされていることを確認します。

# 稼働中のカーネルバージョンを確認
uname -r

# 未マウントの場合はbpffsをマウント
mount | grep /sys/fs/bpf || sudo mount -t bpf bpffs /sys/fs/bpf

ステップ2:既存のkube-proxy DaemonSetを削除

kubeadmで新規構築するクラスターの場合は、初期化時に--skip-phases=addon/kube-proxyを指定します。稼働中の既存クラスターの場合は、DaemonSetを削除し、残存するレガシーなiptablesチェーンをクリアします。

# kube-proxyのDaemonSetとConfigMapを削除
kubectl -n kube-system delete daemonset kube-proxy
kubectl -n kube-system delete configmap kube-proxy

# ワーカーノード上の残存KUBE-* iptablesルールをフラッシュ(SSHまたはAnsible経由)
sudo iptables-save | grep -v KUBE | sudo iptables-restore

ステップ3:Helmを使用したCiliumのインストール

ネットワークPodが完全に初期化される前でもCiliumがクラスター間通信を確立できるよう、API Serverのコントロールプレーンエンドポイントを明示的に指定します。

# CiliumのHelmリポジトリを追加・更新
helm repo add cilium https://helm.cilium.io/
helm repo update

# 完全なソケット負荷分散とHubbleの可観測性を有効化してCiliumをデプロイ
helm install cilium cilium/cilium --version 1.15.4 \
  --namespace kube-system \
  --set kubeProxyReplacement=true \
  --set k8sServiceHost="10.0.0.100" \
  --set k8sServicePort="6443" \
  --set bpf.masquerade=true \
  --set ipam.mode="kubernetes" \
  --set hubble.enabled=true \
  --set hubble.relay.enabled=true \
  --set hubble.ui.enabled=true \
  --set autoDirectNodeRoutes=true

※ 10.0.0.100 は、ご利用のクラスターのAPI ServerロードバランサーVIPまたはプライマリコントロールプレーンのIPアドレスに置き換えてください。

ステップ4:eBPFホストルーティングとService Mapの確認

Ciliumデーモンのステータスを確認し、ソケットレベルの負荷分散がすべてのルーティング処理を引き継いでいることを検証します。

# Ciliumクラスター全体の健全性を確認
cilium status --wait

# kube-proxyの完全置換がStrictモードで稼働していることを確認
kubectl -n kube-system exec -ti ds/cilium -c cilium-agent -- cilium-dbg status | grep -A 10 "KubeProxyReplacement"

出力結果にKubeProxyReplacement: True (Strict)と表示されていれば成功です。これにより、NodePort、ClusterIP、HostPort、および外部トラフィックがeBPFマップを通じて直接管理されるようになります。

ステップ5:Hubbleを使用したトラフィックとパケットドロップのリアルタイム追跡

HubbleはカーネルのリングバッファからeBPFイベントを直接読み取るため、サイドカーやtcpdumpのオーバーヘッドなしに通信フローを詳細に検査できます。

# クラスター全体のパケットドロップをリアルタイムでストリーミング監視
cilium hubble observe --type drop

# checkoutネームスペースへ入るトラフィックを追跡
cilium hubble observe --namespace checkout --follow

本番環境への導入から6か月後の成果

kube-proxyからの移行により、高負荷時におけるクラスターのネットワーク安定性は劇的に向上しました。

  • テールレイテンシの予測可能性向上: ピーク時のトラフィック急増時でも、決済マイクロサービスのp99レイテンシが240msから安定した6msへと大幅に低減しました。
  • コンピュートリソースの回収: 以前はiptablesチェーンの評価やconntrackテーブルの管理で浪費されていたワーカーノードのCPUキャパシティが、約14%回収されました。
  • 即時性のあるオートスケーリング: 100以上のレプリカを追加する急激なスケールアウト時でも、アトミックなBPFマップ挿入によりルーティングが即座に更新され、ネットワークジッターが発生しなくなりました。

数百規模のサービスが稼働している、あるいは数千以上の同時接続を処理するKubernetesインフラストラクチャを運用している場合、Cilium eBPFへの移行はパフォーマンスと運用の両面で即座に大きなメリットをもたらします。

Share: