# eBPFを用いてPod ごとのインターネットトラフィック量を計測するツールの開発 ## 概要 Preferred Networksの2024年夏季インターンシップで、俵 遼太が社内機械学習基盤のKubernetesクラスタ向けに開発した、Podごとのインターネットトラフィック量計測ツールを紹介する記事である。複数テナントが共有するクラスタで、限られたインターネット帯域を占有しているPodを特定することが目的である。 クラスタ内のノードは複数の100GbEインターフェイスを持つ一方、インターネットアクセス帯域は相対的に小さい。そのため、Pod間通信が高速でも、一部ユーザーのPodが外部通信帯域を使い切ると、他のユーザーの通信が低速になる。記事は、この原因調査に必要な「Podごと、通信相手の種類ごとのバイト数」を既存の標準メトリクスだけでは得にくい問題として扱う。 ## 既存手法の限界 `cAdvisor`はPodやコンテナのトラフィック量を公開するが、Pod全体の通信を合算するため、Pod間・Service宛て・クラスタ外という通信相手の区別ができない。上流ルーターで計測すればインターネットアクセス量は得られるが、NodeでNATされた後の通信になるため、送信元Nodeまでは分かっても、そのNode上のどのPodが通信したかを特定できない。 そこで記事のツールは、個別のIPアドレスや通信相手を完全に列挙する代わりに、次の3分類へ集約する。 1. Podに対する通信 2. KubernetesのServiceに対する通信 3. クラスタ外との通信 この粒度であれば、帯域占有の原因となったPodを特定しつつ、Mapのキー数と出力メトリクスのカーディナリティを抑えられる。 ## 全体設計 実装は、PodのネットワークインターフェイスにアタッチするeBPFプログラムと、Mapを読み出してメトリクスを出力するユーザー空間プログラムに分離される。 ![eBPF Mapの構成。PodごとにIngress用とEgress用のper-CPU配列を持ち、Pod・Service・Otherの3キーでバイト数を集計する。](_attachments/pod-level-internet-traffic-measurement-using-ebpf/fig01-ebpf-map-structure.png) 図1は、PodごとにIngress用とEgress用の`BPF_MAP_TYPE_PERCPU_ARRAY`を用意し、キー0をPod、キー1をService、キー2をOtherとして各CPUの累積バイト数を持つ構成を示す。 ### Pod生成時のアタッチ Pod生成を検出し、ネットワークネームスペースとインターフェイス名を取得する処理には、Chained CNI Pluginを使う。CNI PluginはPod生成時に呼び出され、eBPFプログラムのアタッチに必要な情報を受け取れるためである。 CNI Pluginは処理が終わると終了するため、Mapの値を周期的に読み出す処理は別プログラムに分離する。読み出し側はKubernetesのDaemonSetとして常時稼働するTraffic-Exporter Daemon Podを想定する。 ### TCを選択した理由 ネットワーク用のeBPFフックにはXDPとTraffic Control(TC)がある。この記事ではTCを選ぶ。XDPは受信時のIngressでは実行されるが、送信時のEgressでは同じ形でイベントを取得できないためである。TCはIngressとEgressの双方でパケットを処理でき、送受信両方のバイト数を集計できる。 ### CIDR判定とMap PodとServiceのCIDRは環境によって異なるため、eBPFプログラムへハードコードできない。ユーザー空間プログラムが先にCIDR情報をeBPF Mapへ書き込み、eBPFプログラムがその値を参照して宛先分類を行う。Mapだけを先にカーネルへロードし、CIDRの初期化後にプログラムをアタッチすることで、アタッチ時点の初期状態を保証する。 Mapの値には、各CPUごとに値を持つ固定長配列である`BPF_MAP_TYPE_PERCPU_ARRAY`を使う。複数CPUが同じ値を更新する競合を避けられるため、パケット処理中にロックやアトミック加算を使わず、該当エントリへパケットサイズを直接加算できる。 ![ツールのアーキテクチャ。CNI PluginがPodのTCへeBPFをアタッチし、Daemon Podが`/sys/fs/bpf`にPinningされたMapを読み出してメトリクスを公開する。](_attachments/pod-level-internet-traffic-measurement-using-ebpf/fig02-architecture.png) 図2の処理経路は、(1) CNI PluginによるTCへのアタッチ、(2) eBPF Mapの読み書き、(3) Traffic-Exporter Daemon PodによるMapの読み出しと`/metrics`公開、の3段階である。 ## eBPFプログラムの処理 TCにアタッチしたプログラムは`__sk_buff`からパケットデータを取得し、EthernetヘッダとIPv4ヘッダを境界チェック付きで解析する。IPv4ヘッダから送信元アドレス、宛先アドレス、パケットサイズを取り出し、CIDRのネットワークアドレスとサブネットマスクをビット演算で比較して3分類のいずれかを決定する。 IPv4アドレスは32ビット値であるため、Network Byte OrderからHost Byte Orderへ変換する際には`bpf_ntohl`を使う。記事では、誤って`bpf_ntohs`を使ったため、`10.146.0.7`が`0x0a920000`のように下位2バイトを失う不具合が起きた。`bpf_ntohl`へ修正することで正しいアドレスを取得できた。 分類後、IngressまたはEgressに対応するMapからキーを検索し、エントリの値へパケットサイズを加算する。per-CPU配列を使うため、CPU間の更新競合を避けた直接加算が可能になる。 ## eBPFオブジェクトの永続化 アタッチ元のプロセスが終了すると、参照がなくなったeBPFプログラムやMapは通常アンロードされる。そこでPinningを使い、eBPFオブジェクトを`/sys/fs/bpf`上の疑似ファイルとして保持する。これにより、終了したCNI Pluginとは別のDaemon PodからMapを読み出せる。 Daemon PodにはNodeの`/sys/fs/bpf`を`HostPath`でマウントする。Mapの疑似ファイル名にはPod名とNamespace名を使うため、読み出し側はKubernetes APIを呼ばずに対象Podを識別できる。 ただし、MapのPinning時にファイル名へピリオド(`.`)を含めると`Operation not permitted`が返る。記事ではPodのIPv4アドレスをファイル名に使ったことでこの問題に遭遇し、PodのNamespace名と名前を使う方式へ変更した。Linuxカーネル側では、BPFファイルシステム上のピリオドを将来の拡張用に予約している実装が原因と説明される。 ## メトリクスの出力 読み出し側は`cilium/ebpf`の`LoadPinnedMap`でPinningされたMapを開き、`Lookup`でキーごとの値を取得する。Mapの値を定期的に読み出し、`go.opentelemetry.io/otel`ライブラリを使ってOpenTelemetry形式またはPrometheus形式のメトリクスとして出力する。 この設計では、パケットごとの詳細イベントをユーザー空間へ送るのではなく、カーネル内で3分類の累積バイト数へ集約する。収集側は周期的に小さなMapを読むだけでよく、通信量の把握に必要な情報を保ったままイベント転送量を抑えられる。 ## 検証 検証環境では`sample-pod-1`から、`sample-pod-2`内の約100KiBのファイルをPod IP経由とService IP経由で取得し、その後`https://www.preferred.jp/`へ約45KiBのリクエストを送る。Pod IP通信、Service IP通信、クラスタ外通信がそれぞれ異なる分類へ入ることを確認した。 ![検証環境。`sample-pod-1`から`sample-pod-2`へPod IP経由・Service IP経由でアクセスし、クラスタ外のPreferred Networksサイトへもアクセスする。](_attachments/pod-level-internet-traffic-measurement-using-ebpf/fig03-test-environment.png) ![sample-pod-1のIngressトラフィック量。Pod・Service・Otherの系列が、対応するファイル取得や外部リクエストの後に増加する。](_attachments/pod-level-internet-traffic-measurement-using-ebpf/fig04-sample-pod-1-ingress.png) ![sample-pod-2のEgressトラフィック量。Pod IP経由とService IP経由のファイル取得後に、Pod・Service系列が増加する。](_attachments/pod-level-internet-traffic-measurement-using-ebpf/fig05-sample-pod-2-egress.png) 図4と図5では、各リクエストの後に該当するメトリクスがファイルサイズ相当増加しており、通信相手の分類とIngress/Egressの集計が正しく動作したことを確認できる。 ## 記事から得られる設計上の要点 - 目的に必要な通信相手の粒度を先に定義し、個別IP単位ではなくPod・Service・クラスタ外へ集約する。 - CNIの接続性実装へ依存せず、Chained CNI Pluginを計装の差し込み点として使う。 - IngressとEgressを同時に計測するため、受信専用のXDPではなくTCを選ぶ。 - 高頻度のパケットイベントをMap内で累積し、ユーザー空間への転送を周期的な読み出しへ集約する。 - CNI Pluginの短いプロセス寿命と、メトリクスExporterの長いプロセス寿命を分離する。 - eBPFオブジェクトをPinningし、アタッチ処理と読み出し処理を独立させる。 ## 関連 - 組織: [[Preferred Networks]] - 基盤: [[Kubernetes]] / [[Container Network Interface (CNI)]] - 実装: [[eBPF]] / [[eBPFマップ]] / [[XDP]] - 監視: [[ネットワーク監視]] / [[OpenTelemetry]] / [[Prometheus]] - 概念: [[Pod単位ネットワークトラフィック計測]] - 原本: [[.raw/articles/pod-level-internet-traffic-measurement-using-ebpf-2024-11-13.md]] ## 出典 - [Preferred Networks, 「eBPFを用いてPod ごとのインターネットトラフィック量を計測するツールの開発」](https://www.preferred.jp/ja/blog/tech/pod-level-internet-traffic-measurement-using-ebpf), 2024-11-13. - [[.raw/articles/pod-level-internet-traffic-measurement-using-ebpf-2024-11-13.md]] — 取得した記事原本。 - [Container Network Interface (CNI) Specification](https://github.com/containernetworking/cni/blob/main/SPEC.md) — 記事の参考文献[1]。 - [Map Type ‘BPF_MAP_TYPE_PERCPU_ARRAY’ – eBPF Docs](https://docs.ebpf.io/linux/map-type/BPF_MAP_TYPE_PERCPU_ARRAY/) — 記事の参考文献[2]。