この記事は、CYBOZU SUMMER BLOG FES '24 (クラウド基盤本部 Stage) DAY 17の記事です。
こんにちは。クラウド基盤本部 Cloud Platform 部で Kubernetes 基盤(Neco)のネットワークを担当している寺嶋です。
Neco の Kubernetes クラスタはネットワークに Cilium を採用しています。 Neco の他のブログは以下を参照してください。
Cilium は 2023 年 10 月に CNCF Graduated Project となった成熟したプロジェクトです。 一方で、大規模な環境で運用する際には性能問題や不具合に遭遇してしまうことがあります。
私たちはそのような Cilium に関連して発生する問題に対して、ツール開発や upstream への貢献などの様々な方法で対処しています。
本ブログでは、Neco で Cilium を運用していく中で遭遇した様々な問題と、それらから得られた知見を紹介します。
Cilium とは
Cilium とは Kubernetes のネットワークを構築する CNI プラグイン実装のひとつで、CNI プラグインとしての基本的な機能の他に、ネットワークポリシーや Service Mesh などの様々な高度なネットワーク制御機能を備えています。
また、Cilium は eBPF を基幹技術に採用したプロジェクトです。 eBPF は Linux カーネルの機能で、高速かつ柔軟なパケット転送やシステムトレースを実現する技術です。 eBPF のプログラムは事前に安全性を検証され、カーネル空間で実行されます。 Cilium は eBPF を用いて高速なデータプレーンや高度なネットワークポリシー、Service Mesh などの機能を提供しています。
Cilium の導入により得られたメリット
私たちが Cilium を導入した最大の要因は L4LB 機能です。
以前は MetalLB と kube-proxy で L4LB 機能を提供していましたが、ノード障害時にグレースフルにロードバランサ構成を変更することができませんでした。詳しくは以下の記事を参照してください。
一方、Cilium は eBPF ベースの高性能かつ高可用な L4LB を提供しています。
Kubernetes の type :NodePort
や type: LoadBalancer
の Service に宛てた通信に対して、maglev hashing というアルゴリズムを用いたバックエンド選択を行うことで、ノード故障などによるロードバランサ構成の変化の影響を最小限に抑えて通信を捌くことができます。
詳細については以下の記事や論文を参照してください。
https://storage.googleapis.com/pub-tools-public-publication-data/pdf/44824.pdf
さらに、Cilium が提供する kube-proxy 置き換え機能も利用しています。
kube-proxy 置き換え機能は従来の iptables ベースの kube-proxy を eBPF ベースの実装に完全に置き換えてしまう機能です。 eBPF を用いて実装することにより、iptables ベースの kube-proxy が抱えている性能問題を解決することができます。
詳細は以下をご覧ください。
Neco のクラスタは現在 Pod 数約 16000、Service 数約 3000 程度の規模で運用しています。 これらの数はこれからも増加することが見込まれているので、kube-proxy 置き換え機能による性能向上は非常に重要です。
また Cilium のネットワークポリシーは、Kubernetes 標準のネットワークポリシーより高度な制御を実装できます。
L3、L4 ルールの他に L7 ルールや DNS ルールを記述できます。
さらに、kube-apiserver
や remote-host
といった entity と呼ばれる単位でネットワークポリシーを制御できます。
こちらも詳細は以下をご覧ください。
Cilium に関連する障害とその対応
Neco の Kubernetes クラスタは Cilium の導入によりさまざまなメリットを享受できています。 一方で、Cilium に起因する問題もいくつか発生しています。 本章では Neco で遭遇した Cilium 関連の問題とそれに対する対応をいくつか紹介します。
Cilium が Kubernetes の API server と通信できなくなったとき cilium-agent が停止してしまう
Cilium は カスタムリソースなどの管理を行う cilium-operator
(Deployment)と、各ノード上で Pod のネットワークの設定などを行う cilium-agent
(Daemonset)の二つのコンポーネントで構成されます。
私たちは以前、etcd の不具合により API server が正常に処理できなくなる障害に遭遇しました。 その際に cilium-agent も停止してしまい、Kubernetes クラスタ外部に向けて公開しているエンドポイントがアクセスできない状態になってしまいました。
原因は cilium-agent のヘルスチェックが API server に依存した実装になっており、API server が正常に処理できないことでヘルスチェックに失敗し続け、cilium-agent が異常終了してしまったためでした。
私たちは Service type LoadBalancer
のアドレス広報に Cilium が持つ BGP の機能を利用しています。
そのため、cilium-agent が停止したことにより、アドレスを広報している BGP のプロセス も停止し、LoadBalancer のアドレス情報がネットワーク上から消失してしまいました。
この問題は cilium-agent 単体ではどうしようもない事象だったため、etcd を復旧させて、API server が正常に戻るまで解消しませんでした。
対応
運用者としては、LoadBalancer アドレスの広報はサービス提供の継続に重大な影響を及ぼすため、たとえ API server が停止したとしてもできるだけ継続してほしいです。
この問題に対して、現在 Neco では Cilium の upstream に対して、この API server との接続性確認を無効化するオプションを提案して実装しています。
今後の Cilium にこの修正が取り込まれれば、cilium-agent と API server の依存が少なくなり、より安定した運用が可能になるはずです。
connection tracking table が溢れそうになった
connection tracking はネットワーク上の各通信の状態を保存・追跡する技術です。 connection tracking table に通信の状態を保存して NAT やネットワークポリシーに活用します。
Neco の Kubernetes クラスタには現在様々なワークロードがデプロイされており、通信特性も様々です。 長時間一つの TCP コネクションを継続して利用するワークロードもあれば、短時間で終了する TCP コネクションを大量に作成するものも存在します。 ここで紹介する障害は短命なコネクションを大量に作成するワークロードと Cilium の組み合わせで発生した障害です。
Cilium の kube-proxy 置き換え機能は Kubernetes クラスタの通信のほぼ全てを eBPF を利用した独自のプログラムで処理します。 そのため、connection tracking も BPF マップを利用して独自に実装されています。
BPF マップとは eBPF プログラムの中で利用できるキーバリューストアです。 eBPF や BPF map の詳細については以下を参照してください。
Cilium の connection tracking table はハッシュマップにより実装されています。 マップのサイズは cilium-agent が BPF マップのために使用できるメモリから動的に計算されており、約 10M コネクションを同時に保存できる状態でした。
ある日、この connection tracking 用 BPF マップが数時間のうちに 2M エントリから 8M エントリに急激に増加しました。 原因はあるワークロードが接続する対象が約 2 倍に増えたことによるコネクション数の増加でした。
このままのエントリ数の伸び方だとこの後数時間で使用率 100% になってしまいます。 connection tracking 用 BPF マップは使用率が 100% になってしまうと、新規のコネクションを保存するために、既存のコネクションの情報を捨ててしまいます。
これは通信の切断を意味するので避けなければなりません。
対応
私たちはこのような BPF マップの容量逼迫を未然に検出できるように事前に準備していました。
Cilium は BPF マップの使用率を cilium_bpf_map_pressure
というメトリクスに出力しています。
しかし、このメトリクスは当時は conection tracking 用マップの使用率については未対応でした。
補足:1.16.0 の Cilium で新たに connection tracking 用マップの使用率を出力するようになりました。
そのため、私たちは独自に bpf-map-pressure-exporter
というツールを開発して connection tracking 用 BPF マップの使用率を監視していました。
これにより使用率が 80% を超えた段階でアラートが鳴り、容量拡張という対策を実施して障害を未然に防ぐことができました。
connection tracking 用 BPF マップの容量は cilium-agent の以下のパラメータのうちどちらかを変更することで拡張可能です。
bpf-map-dynamic-size-ratio
:cilium-agent が BPF マップ用に利用可能なメモリの総量の割合bpf-ct-global-tcp-max
:TCP 用の connection tracking table の総エントリ数bpf-ct-global-any-max
:TCP 以外の connection tracking table の総エントリ数
ネットワークポリシー関連の BPF マップが溢れて Pod が起動しなくなった
もう一つ BPF マップに関連して発生した障害を紹介します。
Cilium はネットワークポリシーについても BPF マップを利用して管理しています。 本障害はネットワークポリシー用の BPF マップが溢れてしまったことによって発生した障害です。
Cilium のネットワークポリシーは Pod に付与されたラベルをもとに CiliumIdentity
というカスタムリソースを作成し、この CiliumIdentity をもとに制御の対象を識別します。
例えば、以下の二つの Pod は付与されたラベルが同一なので同じ CiliumIdentity
に属します。
apiVersion: v1 kind: Pod metadata: name: nginx labels: app: nginx spec: containers: - name: nginx image: nginx:latest ports: - containerPort: 80
apiVersion: v1 kind: Pod metadata: name: nginx-2 labels: app: nginx spec: containers: - name: nginx image: nginx:latest ports: - containerPort: 80
一方、以下の Pod はラベルが同一でないので異なる CiliumIdentity
に属します。
apiVersion: v1 kind: Pod metadata: name: nginx-3 labels: app: nginx role: test spec: containers: - name: nginx image: nginx:latest ports: - containerPort: 80
このように、CiliumIdentity
はユニークなラベルの集合の数だけ作成され、ネットワークポリシー用の BPF マップに書き込まれます。
ある日、短期間に大量の Job 生成するワークロードがデプロイされました。
Job から生成される Pod には batch.kubernetes.io/controller-uid
などのユニークなラベルが付与されるため、それらに対応する CiliumIdentity
が大量に生成され、BPF マップの容量を超えてしまいました。
その結果、CNI 呼び出しが失敗し、新しい Pod の作成に失敗するようになりました。
対応
障害発生時、暫定的にネットワークポリシー用 BPF マップの容量を拡張する処置を実施しました。 これにより、一時的に障害は解消しました。
また、長期的な対策として、CiliumIdentity
の生成に利用されるラベルを制限するという方法を取りました。
これは Cilium のドキュメントにも紹介されています。
batch.kubernetes.io/controller-uid
などの Pod ごとにユニークなラベルを CiliumIdentity
の生成に利用しないようして、以下のようにネットワークポリシー用 BPF マップの使用率は安定しました。
また、現在 Neco のクラスタではより安定した運用のために CiliumIdentity
の生成に利用するラベルをホワイトリスト形式に変更して、意図しないラベルを CiliumIdentity
の生成に利用しないように制御しています。
Cilium のリソースが作成されず、Pod が通信できなくなかった
次は通信に必要な CiliumEndpoint
というカスタムリソースが作成されなかったために発生した障害を紹介します。
本障害は根本原因は現在調査中ですが、発生原因は判明し、短期的な対策がとれているので紹介します。
Cilium は Pod の作成時(CNI 呼び出し時)に Pod と一対一に対応した CiliumEndpoint
というリソースを作成します。
CiliumEndpoint
を作成することによって、この Pod がどの CiliumIdentity
に所属するか判断します。
CiliumEndpoint
は以下のように確認できます。
$ kubectl get cep NAME ENDPOINT ID IDENTITY ID INGRESS ENFORCEMENT EGRESS ENFORCEMENT VISIBILITY POLICY ENDPOINT STATE IPV4 IPV6 ubuntu-86668c67d7-572ps 2639 12560 <status disabled> <status disabled> <status disabled> ready 10.64.1.166 fe80::547c:51ff:fe67:3ac2
本障害では、Deployment に所属する複数の Pod のうち一つだけが通信できなくなる現象が発生しました。
調査の際に Cilium のパケット解析ツールである Hubble で通信を解析したところ、本来通信が許可されるべきパケットがネットワークポリシーにより拒否されていることがわかりました。
さらに調査すると、拒否されている通信で、当該の Pod は本来 2788 という CiliumIdentity
に解決されなければいけないにも関わらず 16777233 という値に解決されていました。
16777233 という値は以下のように定義されており、クラスタ外からの通信に割り当てられる番号でした。
$ cilium identity get 16777233 ID LABELS 16777233 cidr:64.0.0.0/2 reserved:world
なぜこのように本来割り当てられるべき CiliumIdentity
が割り当たっていないかを調査したところ、対象の Pod に対して CiliumEndpoint
が作成されていないことがわかりました。
CiliumEndpoint
が作成されないことで、その Pod が本来所属すべき CiliumIdentity
に所属できず、最終的にその Pod のアドレスが 16777233 の CiliumIdentity
の CIDR にマッチしたため、16777233 に解決されていました。
結果として、ネットワークポリシーにより拒否されていました。
対応
調査により、本来存在すべき CiliumEndpoint
が存在しないことがわかりました。
CiliumEndpoint
は Pod 作成時に作成されます。
そのため、対象の Pod を再作成することで、正常に CiliumEndpoint
が作成され、障害は解消しました。
CiliumEndpoint
が作成されなかった原因は現在調査中です。
短期対策として cep-checker
という定期的に Pod と CiliumEndpoint
の整合性を検査するツールを開発してアラートを仕掛けています。
Neco と Cilium の今後
これまで Neco のクラスタで Cilium を活用する中で発生した障害とそれに対する対応をいくつか紹介しました。
大規模な環境で Cilium を運用していると様々な問題に遭遇することがあります。
私たちはそれらの問題に対して、 upstream へのコントリビューションやツールの自作など、様々な形で対処してきました。
これからも同様に、積極的に問題解決していきたいと考えています。
また、Cilium は ServiceMesh や Gateway API など新しい機能が次々実装されており、進化の速いソフトウェアです。
今後も様々な機能が実装されると思うので、Neco でもウォッチして便利な機能を導入していきたいと考えています。