Cilium 運用で遭遇した問題とその対応

この記事は、CYBOZU SUMMER BLOG FES '24 (クラウド基盤本部 Stage) DAY 17の記事です。

こんにちは。クラウド基盤本部 Cloud Platform 部で Kubernetes 基盤(Neco)のネットワークを担当している寺嶋です。

Neco の Kubernetes クラスタはネットワークに Cilium を採用しています。 Neco の他のブログは以下を参照してください。

blog.cybozu.io

Cilium は 2023 年 10 月に CNCF Graduated Project となった成熟したプロジェクトです。 一方で、大規模な環境で運用する際には性能問題や不具合に遭遇してしまうことがあります。

私たちはそのような Cilium に関連して発生する問題に対して、ツール開発や upstream への貢献などの様々な方法で対処しています。

本ブログでは、Neco で Cilium を運用していく中で遭遇した様々な問題と、それらから得られた知見を紹介します。

Cilium とは

Cilium とは Kubernetes のネットワークを構築する CNI プラグイン実装のひとつで、CNI プラグインとしての基本的な機能の他に、ネットワークポリシーや Service Mesh などの様々な高度なネットワーク制御機能を備えています。

cilium.io

また、Cilium は eBPF を基幹技術に採用したプロジェクトです。 eBPF は Linux カーネルの機能で、高速かつ柔軟なパケット転送やシステムトレースを実現する技術です。 eBPF のプログラムは事前に安全性を検証され、カーネル空間で実行されます。 Cilium は eBPF を用いて高速なデータプレーンや高度なネットワークポリシー、Service Mesh などの機能を提供しています。

Cilium の導入により得られたメリット

私たちが Cilium を導入した最大の要因は L4LB 機能です。

以前は MetalLB と kube-proxy で L4LB 機能を提供していましたが、ノード障害時にグレースフルにロードバランサ構成を変更することができませんでした。詳しくは以下の記事を参照してください。

metallb.universe.tf

一方、Cilium は eBPF ベースの高性能かつ高可用な L4LB を提供しています。 Kubernetes の type :NodePorttype: LoadBalancer の Service に宛てた通信に対して、maglev hashing というアルゴリズムを用いたバックエンド選択を行うことで、ノード故障などによるロードバランサ構成の変化の影響を最小限に抑えて通信を捌くことができます。

詳細については以下の記事や論文を参照してください。

cilium.io

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 が抱えている性能問題を解決することができます。

詳細は以下をご覧ください。

cilium.io

Neco のクラスタは現在 Pod 数約 16000、Service 数約 3000 程度の規模で運用しています。 これらの数はこれからも増加することが見込まれているので、kube-proxy 置き換え機能による性能向上は非常に重要です。

また Cilium のネットワークポリシーは、Kubernetes 標準のネットワークポリシーより高度な制御を実装できます。 L3、L4 ルールの他に L7 ルールや DNS ルールを記述できます。 さらに、kube-apiserverremote-host といった entity と呼ばれる単位でネットワークポリシーを制御できます。 こちらも詳細は以下をご覧ください。

docs.cilium.io

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 との接続性確認を無効化するオプションを提案して実装しています。

github.com

github.com

今後の 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 の詳細については以下を参照してください。

ebpf.io

Cilium の connection tracking table はハッシュマップにより実装されています。 マップのサイズは cilium-agent が BPF マップのために使用できるメモリから動的に計算されており、約 10M コネクションを同時に保存できる状態でした。

ある日、この connection tracking 用 BPF マップが数時間のうちに 2M エントリから 8M エントリに急激に増加しました。 原因はあるワークロードが接続する対象が約 2 倍に増えたことによるコネクション数の増加でした。

connection tracking の使用率の変化

このままのエントリ数の伸び方だとこの後数時間で使用率 100% になってしまいます。 connection tracking 用 BPF マップは使用率が 100% になってしまうと、新規のコネクションを保存するために、既存のコネクションの情報を捨ててしまいます。

これは通信の切断を意味するので避けなければなりません。

対応

私たちはこのような BPF マップの容量逼迫を未然に検出できるように事前に準備していました。 Cilium は BPF マップの使用率を cilium_bpf_map_pressure というメトリクスに出力しています。

しかし、このメトリクスは当時は conection tracking 用マップの使用率については未対応でした。

補足:1.16.0 の Cilium で新たに connection tracking 用マップの使用率を出力するようになりました。

docs.cilium.io

そのため、私たちは独自に bpf-map-pressure-exporter というツールを開発して connection tracking 用 BPF マップの使用率を監視していました。

github.com

これにより使用率が 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 のドキュメントにも紹介されています。

docs.cilium.io

batch.kubernetes.io/controller-uid などの Pod ごとにユニークなラベルを CiliumIdentity の生成に利用しないようして、以下のようにネットワークポリシー用 BPF マップの使用率は安定しました。

policy map の使用率

また、現在 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 の整合性を検査するツールを開発してアラートを仕掛けています。

github.com

Neco と Cilium の今後

これまで Neco のクラスタで Cilium を活用する中で発生した障害とそれに対する対応をいくつか紹介しました。

大規模な環境で Cilium を運用していると様々な問題に遭遇することがあります。

私たちはそれらの問題に対して、 upstream へのコントリビューションやツールの自作など、様々な形で対処してきました。

これからも同様に、積極的に問題解決していきたいと考えています。

また、Cilium は ServiceMesh や Gateway API など新しい機能が次々実装されており、進化の速いソフトウェアです。

今後も様々な機能が実装されると思うので、Neco でもウォッチして便利な機能を導入していきたいと考えています。