Cilium Connection Tracking Deep Dive – Cybozu Inside Out

この記事は、CYBOZU SUMMER BLOG FES ’25の記事です。

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

Neco では Cilium を Kubernetes ネットワークに採用しています。
先日我々の Cilium 活用の事例が CNCF のサイトに公開されましたので、ぜひそちらもご覧ください。

www.cncf.io

本記事では Cilium の通信制御のコアである Cilium の Connection Tracking の挙動と実装について解説するものです。

前書き

Cilium は eBPF をパケット処理などに採用したコンテナネットワークを管理するソフトウェアです。Cilium は高速な Kubernetes ネットワークを提供するため、kube-proxy 代替機能や Connection Tracking を eBPF ベースで独自に実装しています。

本記事は大きく以下の三つのパートから構成されます。

  • Cilium の紹介
  • Cilium の conntrack の理解に必要な要素の解説
    • Conntrack の一般的な解説
    • Cilium が管理する Kubernetes ネットワークの概要
    • Cilium の BPF プログラムや BPF マップの概要
  • Cilium の conntrack の挙動と実装の解説

※ 本記事は、 Cilium 1.16.12 のコードをもとに執筆します。Cilium は開発スピードが非常に早いソフトウェアです。本記事で解説した挙動が、新しいバージョンでは修正されている可能性があることに注意してください。
本記事で解説する Cilium の挙動や実装は Neco で利用している構成を前提としています。

目次

Cilium とは

Cilium は Kubernetes をはじめとしたクラウドネイティブ環境のネットワークにおけるパフォーマンス向上やセキュリティ・オブザーバビリティを提供するソフトウェアです。
後述する CNI プラグイン機能や Network Policy や L4LB、トラフィック可視化などの機能を提供しています。

cilium.io

Cilium はデータプレーンに eBPF を採用しており、 高速で安全なデータプレーンを実現しています。

CNI プラグイン

Cilium は CNI プラグインとして実装されています。CNI プラグイン(CNI) とはコンテナのネットワークを設定するコンポーネントです。CNI Spec という仕様が公開されており、実装は各プラグインに委ねられています。
CNI プラグイン は様々な実装が存在しています。広く利用されているものに Calicoflannel があります。Neco でも独自の Coil というプラグインを開発して利用しています。

eBPF

Cilium のパケット処理は eBPF で実装されています。
eBPFの基本的な情報については以下の資料をご覧ください。

ebpf.io

www.oreilly.co.jp

Neco での Cilium の活用例

Cilium は非常に機能が多いソフトウェアです。Neco では、その中で必要な機能のみに絞って利用しています。我々が主に利用している機能は以下です。

  • kube-proxy replacement(KPR)
  • Network Policy
  • Layer 4 Load Balancer

我々がどのように Cilium を利用して Kubernetes のネットワークを構築しているかはすでにブログが公開されているのでこちらを読んでみてください。

blog.cybozu.io

Cilium のすべての機能の土台となる kube-proxy replacement は iptables で実装されていた kube-proxy のスケーラビリティ問題を克服するために開発されました。
興味のある方は以下の動画を観てみてください。

youtu.be

Deep Dive の準備

本パートでは Cilium の conntrack を深く知るための準備として、いくつかの前提となる知識を紹介します。

始めに、conntrack の概要を一般的な視点で解説します。Conntrack は Cilium に特有の技術ではありません。身近な conntrack の利用例を紹介して conntrack への解像度を上げます。

次に、Cilium が管理する Kubernetes ネットワークの概要を紹介します。
本記事では Cilium が管理する Kubernetes ネットワークとは、Cilium の kube-proxy replacement(以下 KPR) が導入された Kubernetes のネットワークを指します。
Cilium の conntrack は Kubernetes ネットワークに最適化して実装されているので、Cilium の conntrack を理解するには KPR の通信の仕組みを理解しておく必要があります。

最後に、Cilium の conntrack の実装を理解するために必要な Cilium の具体的な BPF プログラムや BPF マップの構成を紹介します。

※ 本パートの各項目は内容を既に知っている場合は読み飛ばしていただいて構いません。

Connection Tracking とは

Connection Tracking(以下 conntrack)は、大まかに言うと通信の状態を保存、追跡する機能です。
送信元と宛先のアドレスやポート、プロトコルなどの情報を記録し、ステートフルな通信の制御や負荷分散などに利用します。
具体例は、NAT や Load Balancer、Firewall、Syn Flooding 攻撃に対する防御などがあります。

Linux の Conntrack

Conntrack は Linux では netfilter サブモジュールとして実装されています。
netfilteriptablesnftables コマンドで設定でき、IP masquerade(Network Address Port Translation; NAPT) やパケットフィルタリングなどに利用されます。
それらの機能のコアとして conntrack が利用されています。

ここでは IP masquerade を例にとって conntrack の仕組みを簡単に紹介します。
この図では Local Network 上の local-b というアドレスを持つ Client が External Network 上の、global-b というアドレスを持つ Server に接続するために、IP masquerade が設定された local-aglobal-a というアドレスを持つ Proxy を利用するシナリオを示しています。

IP masquerade の conntrack の動作例

  1. Client は外部ネットワークの Server の y というポートで公開されているサービスにアクセスするために、local-b:x -> global-b:y というパケット(SYN)を Proxy に向けて送出します。
  2. パケットを受け取った Proxy は masquerade の設定に従って、そのパケットを自分の global-a というアドレスに変換します。この時に conntrack table に対応するアドレスとポート、通信の状態(SYNSENT)を保存します。
  3. アドレスとポートを書き換えて global-a:z -> global-b:y として Server に向けてパケットを転送します。
  4. Server はglobal-b:y -> global-a:z に対して返答(SYN/ACK)を返します。
  5. Proxy は Server からの SYN/ACK を観測して、conntrack エントリの状態を SYNRECV に更新します。
  6. Proxy は Server からの応答を Client に返します。
  7. その後、Client と Server 間で TCP コネクションが確立され、アプリケーションの通信が始まります。このとき、conntrack は接続確立を観測して conntrack エントリの状態を ESTABLISHED に更新します。

コネクションの終了時も TCP の終了シーケンスに則って似たような処理が実施されて、保存されていた conntrack エントリは削除されます。

Cilium も同じような仕組みで conntrack を実装していますが、より Kubernetes ネットワークに最適化されたものになっています。

Linux カーネルの conntrack の詳細に興味のある方は以下の資料を読んでみてください。

Cilium が管理する Kubernetes ネットワーク

Cilium は Kubernetes クラスタ内の全ての通信を扱います。また、Cilium の conntrack は後述する Kubernetes の各種通信方式に応じて様々な形で通信を管理します。
ここでは、 Cilium を導入したクラスタにおける Kubernetes ネットワークの概要を解説します。

※ 冒頭で述べた通り、ここでは Cilium kube-proxy Replacement(KPR) を前提としたネットワークについて説明します。

通常の kube-proxy は iptables や nftables を利用して通信制御を実装していますが、Cilium の KPR は kube-proxy を無効化して、eBPF で独自の通信制御を実装しています。

Pod 間通信

最も基礎的な通信です。Cilium の BPF プログラムがパケットを処理する以外で特別なことはありません。

ClusterIP Service の通信

Service リソースの ClusterIP type は、Cilium では Pod 間通信とほとんど変わりません。

Pod から送出された ClusterIP service 宛の通信は Cilium の BPF プログラムによって、送信元の Pod のノード上でその ClusterIP の任意のバックエンド Pod のアドレスに変換します。それ以降は Pod 間通信と同様の処理を行います。

※ Cilium は ClusterIP Service の実装として SocketLB と Per-Packet LB の二つを提供しています。デフォルトは SocketLB ですが、本記事では Per-Packet LB を解説します。

LoadBalancer Service の通信

Service リソースの LoadBalancer type は、クラスタ内のエンドポイントをクラスタ外部に公開するために利用します。クラスタ外部にアドレス(EXTERNAL-IP)を公開して外部からの通信を受け付けて負荷分散する Layer 4 Load Balancer です。

LoadBalancer service は .spec.externalTrafficPolicy の値(Cluster or Local)によって処理が異なります。

externalTrafficPolicy=Cluster

externalTrafficPolicy=Cluster(eTP=Cluster) の場合、かなり複雑な通信経路になります。
利用するメリットは、LoadBalancer service のバックエンド Pod への負荷が均等になることです。

Cilium はいくつかの実装を提供していますが、我々は Maglev hashing + DSR(Direct Server Return) with Geneve encap という組み合わせで L4LB を利用しています。

※ 本記事では DSR with Geneve encacp に限って解説します。

DSR with Geneve encap の詳細については以前 Cloud Native Days で発表していますのでそちらを参照してください。

cloudnativedays.jp

speakerdeck.com

Maglev hashing は consistent hashing アルゴリズムの一つで、 耐障害性や高い可用性を備えた負荷分散を提供します。
公開資料だと以下がまとまっています。

ここでは簡単に、DSR with Geneve encacp の L4LB の通信の仕組みを解説します。

L4LB の通信では、重要な役割を持つノードが二つあります。

  • 中間ノード(Load Balancing Node)
    • 「LB ノード」と呼ぶ場合もありますが、本記事では「中間ノード」と表記します
    • 外部クライアントからのパケットを一次受けするノード
    • 負荷分散の役割を担います
  • バックエンドノード
    • 転送すべきバックエンド Pod が存在するノード
    • このノードから直接クライアントに返信します(DSR)

cilium-agent が動作するすべてのノードが中間ノードとして振る舞うことができます。中間ノードとなるノードは何らかの方法(BGP が利用されることが多いです。)でクラスタの上流のルータに LB Service の EXTERNAL-IP を広報します。
下図は中間ノードとして選択された NodeA と、バックエンドの Pod1 が存在する NodeB が LB Service の通信を処理する様子を示しています。

LoadBalancer eTP=Cluster の通信の流れ

  1. 上流のルータは ECMP(Equal Cost Multi Path) の仕組みを使って、外部のクライアントからの EXTERNAL-IP 宛の通信を任意の中間ノード(今回は NodeA が選択されたという想定)に転送します
  2. NodeA が適切なバックエンド(今回は NodeB 上の Pod1)を選択してさらにパケットを転送します
  3. 転送の際に、NodeA の cilium-agent は Geneve プロトコル でカプセル化して、LB の EXTERNAL-IP とポートの情報を NodeB に引き渡します。
  4. NodeB の cilium-agent がカプセル化の解除などの必要な処理をした後、バックエンド Pod にパケットが到達します
    返信パケットは NodeB の cilium-agent が必要なデータを持っているので、クライアントに直接送信できます
externalTrafficPolicy=Local

externalTrafficPolicy=Local(eTP=Local) の場合はかなりシンプルで、外部ネットワークから通信を受けた時、直接バックエンドの Pod に転送されます。
この方式は Cluster がバックエンド Pod に均等に負荷を分散できるのに対して、Local はノード単位での負荷分散までしか対応できません。また、耐障害性も劣るので、特別な事情がない限り Cluster を利用する方が好ましいです。

NodePort Service の通信

Service リソースの NodePort type もクラスタ内のエンドポイントをクラスタ外に公開するために利用します。LoadBalancer service と異なるのは、サービスの公開のために、EXTERNAL-IP の代わりにポートを利用することです。任意の Kubernetes ノードのそのポート宛の通信を、NodePort service に紐づいたバックエンドに転送します。

※ Cilium の BPF プログラムの実装としては、NodePortLoadBalancer は同一です。

Cilium の BPF プログラムとマップの概要

Cilium は eBPF でパケット処理を行います。
eBPF はパケット処理のためにさまざまなフックポイントを提供しており、Cilium では主に TC にアタッチする BPF プログラムでパケットを処理します。

※ Linux カーネル 6.6 以降から TC の強化版の TCX が実装されました。
TCX は従来の TC Classifier の強化版と思っていただければ十分です。
Cilium はこれを v1.16 からサポートしていて、デフォルトで利用されるのは TCX です。

[PATCH bpf-next v6 0/8] BPF link support for tc BPF programs – Daniel Borkmann

※ BPF プログラム上は TCTCX で変化はほとんどなく、基本的な挙動も変わらないので、本記事では馴染みのある TC で以降の解説を行います。

※ 設定次第では XDP モードを利用することもできますし、一部で cgroup も利用します。

TC は Ingress/Egress それぞれにプログラムをアタッチできます。
Cilium がカーネルにアタッチする BPF プログラムの概要は以下の図のようになっています。

Cilium の BPF プログラムの構成

ノード上の Pod は veth(Virtual Ethernet Device) のペアがコンテナ側とホスト側で一対一対応で作成されて、通信を行います。
Cilium は上の図のように、ノードのプライマリなデバイス(ここでは eth0)と必要に応じてトンネルデバイス(cilium_geneve)、ホスト側の veth にそれぞれ異なる BPF プログラムを Ingress/Egress それぞれにアタッチしています。

cilium_geneve デバイスは設定値に応じて cilium-agent が作成するデバイスです。
本記事中では LB のパケットを DSR するために利用されます。

externalTrafficPolicy=Cluster

以前似た内容で LT をした際の資料です。興味のある方は覗いてみてください。

speakerdeck.com

Cilium は BPF プログラムと同様に複数の BPF マップも作成します。その用途は多岐に渡りますが、本記事で登場するのは CT map(正式名称:cilium_ct4_global/cilium_ct_any4_global) と Service map(正式名称:cilium_lb4_services_v2) 、RevNAT map(正式名称:cilium_lb4_reverse_nat) などです。それぞれ以下の用途で利用されます。

  • CT map
    • cilium_ct4_global(TCP 用)
    • cilium_ct_any4_global(TCP 以外のプロトコル用)
    • Conntrack テーブル。本記事の主役
    • 詳細は CT map で解説します
  • Service map
    • cilium_lb4_services_v2
    • Kubernetes の Service のエンドポイントなどの情報を保存しておきます
    • 各 Service のアドレスとポートなどをキー、上記のような情報をバリューに保持します
  • RevNAT map
    • cilium_lb4_reverse_nat
    • Service の通信の reverse NAT に利用します
    • Service に対して一意な ID をキー、Service のアドレスとポートをバリューに保持します
  • NAT map
    • cilium_snat_v4_external
    • Pod からパケットをクライアントに DSR するときに SNAT するための情報を保存します

Cilium Connection Tracking Deep Dive

ここまで Cilium の conntrack の前提知識を紹介しました。
ここからは本題の Cilium の conntrack がどのように実装され、動作しているかを解説します。

※ Cilium v1.16.12 実装をベースに紹介します。最新版では異なる実装となっている可能性は十分あるので注意してください。

GitHub – cilium/cilium at v1.16.12

※ IPv4 の実装のみを解説します。

※ 今後 CT map と書くとき、基本的には cilium_ct4_global を指します。cilium_ct_any4_global について言及する際は明に記述します。

Cilium の conntrack は eBPF による パケット処理の実装と Go で書かれた control plane (cilium-agent) の二つの部分に分けられます。
Conntrack についてはコアとなるのは実際にパケットを処理する BPF プログラムです。ほとんどの処理は BPF プログラム内で完結しますが、CT map の GC(Garbage Collection) を control plane で実施しています。

概要

まずは概要を掴みましょう。
下図は Cilium の conntrack の概要を示しています。

Cilium の conntrack の概要

CT map はノードごとに作成され、ノード上の各デバイスにアタッチされた BPF プログラムから操作され、パケットごとに対応するエントリが最新の状態に更新されます。
また、cilium-agent は定期的にユーザー空間から CT map を GC して不要なエントリを削除します。

CT map

本格的に通信の解説に入る前に、CT map について説明します。
Cilium-の-BPF-プログラムとマップの概要 で述べた通り、CT map には TCP 用の cilium_ct4_global とそれ以外のプロトコル用の cilium_ct_any4_global という二つが存在します。

BPF マップは基本的にはキーバリューストアです。マップのデータ構造はさまざまなものが選べますが、CT map は LRU(Least Recently Used) Hash を利用しています。
LRU Hash については以下を参照してください。

Map Type ‘BPF_MAP_TYPE_LRU_HASH’ – eBPF Docs

cilium_ct4_globalcilium_ct_any4_global どちらも共通のキーとバリューを持ち、マップのサイズは上限値はありますが、設定によって変更可能です。

BPF マップの定義

実際の定義は以下のようになっています。

cilium/bpf/lib/conntrack_map.h at v1.16.12 · cilium/cilium · GitHub

struct {
    __uint(type, BPF_MAP_TYPE_LRU_HASH);
    __type(key, struct ipv4_ct_tuple);
    __type(value, struct ct_entry);
    __uint(pinning, LIBBPF_PIN_BY_NAME);
    __uint(max_entries, CT_MAP_SIZE_TCP);
} CT_MAP_TCP4 __section_maps_btf;

キー構造体(ipv4_ct_tuple)

キーとなる ipv4_ct_tuple は 5-tuple と flag をフィールドとして持ちます。
CT エントリは内部的に以下の三つのタイプに分類され、flag に埋め込まれます。

  • CT_SERVICE
    • Service の通信用
    • Service 宛の通信の状態を保存して、転送すべきバックエンド Pod などを判断できるようにします
  • CT_EGRESS
    • Pod から外に出ていく(Egress)通信用
  • CT_INGRESS
    • Pod に入っていく(Ingress)通信用

cilium/bpf/lib/common.h at v1.16.12 · cilium/cilium · GitHub

struct ipv4_ct_tuple {
    


    __be32      daddr;
    __be32      saddr;
    


    __be16      dport;
    __be16      sport;
    __u8        nexthdr;
    __u8        flags;
} __packed;

バリュー構造体(ct_entry)

バリューとなる ct_entry は多くのフィールドを持ちます。ここで全てを紹介はできないので、適宜説明することとします。

cilium/bpf/lib/common.h at v1.16.12 · cilium/cilium · GitHub

struct ct_entry {
    __u64 reserved0;    
    __u64 backend_id;
    __u64 packets;
    __u64 bytes;
    __u32 lifetime;
    __u16 rx_closing:1,
          tx_closing:1,
          reserved1:1, 
          lb_loopback:1,
          seen_non_syn:1,
          node_port:1,
          proxy_redirect:1,    
          dsr_internal:1,  
          from_l7lb:1, 
          reserved2:1, 
          from_tunnel:1,   
          reserved3:5;
    __u16 rev_nat_index;
    



    __u16 ifindex;

    


    __u8  tx_flags_seen;
    __u8  rx_flags_seen;

    __u32 src_sec_id; 

    


    __u32 last_tx_report;
    __u32 last_rx_report;
};

以降は Kubernetes の各通信方式ごとに Cilium の conntrack がどのように動作するか説明します。

※ 以降の解説では NodePortLoadBalancer を同一視し、便宜上 LoadBalancer として解説します。
また、LoadBalancer については externalTrafficPolicy=Cluster のみを解説します。

※ Neco で現在利用している設定 Maglev hashing + DSR with Geneve encap に限って解説します。

※ 以降の解説では Cilium の eBPF のコードを GitHub へのリンクをポイントしながら解説します。Cilium の BPF プログラムは tail call やマクロを多用した非常に複雑なコードになっています。
エディタの定義ジャンプが機能しないため、コードを読む際は tail call を意識して文字列検索を駆使して頑張ってください。

Tail calls – eBPF Docs

Pod 間通信

最も基本的な通信です。そのため conntrack の操作も最もシンプルです。
しかし、これ以降に紹介する Service 関連の通信の基礎となるので非常に重要でもあります。

NodeA 上の Pod1(10.64.0.36) から NodeB 上の Pod2(10.64.0.64) への通信を例に動作を下図を用いて解説します。

Pod 間通信の conntrack 操作とパケットの流れ

Pod 間通信を行う時、送信側(Egress)と受信側(Ingress)双方で conntrack の処理が実行されます。そのため、ここでは Egress/Ingress に分けて説明します。

Pod1 からの Egress

Pod1 から Pod2 に通信をする時、コンテナ内部では一般的なコンテナと同様に Linux カーネルのネットワークスタックを通ってホスト側の veth デバイスにパケットが引き渡されます。
その後、cil_from_container プログラムが実行され、以下のような conntrack の処理が実行されて Pod2 にパケットが送出されます。

※ 箇条書きの番号は上図の NodeA 上の黒の番号と対応しています。

  1. CT map を lookup(検索)して既存のエントリがないか調べます
    1. cilium/bpf/bpf_lxc.c at v1.16.12 · cilium/cilium · GitHub
    2. 新規通信の場合、CT_NEW というアクションが選択されます
  2. CT map に CT_EGRESS のエントリを作成します
    1. cilium/bpf/bpf_lxc.c at v1.16.12 · cilium/cilium · GitHub
    2. 上図の NodeA CT map に示した CT_EGRESS エントリを作成します
    3. ICMP を扱うために、cilium_ct_any4_global CT map にもエントリが作成されます
      1. このコネクションに対して ICMP パケットが返ってきた場合に Network Policy の判定をスキップするために作成されます
  3. ホスト上の eth0 にアタッチされた cil_to_netdev を通った後、Pod2 に向けて送出されます

以下が実際の conntrack エントリです。

node-a# cilium bpf ct list global | grep 10.64.0.64
TCP OUT 10.64.0.36:54260 -> 10.64.0.64:8000 expires=3026362 Packets=0 Bytes=0 RxFlagsSeen=0x1b LastRxReport=3026352 TxFlagsSeen=0x1b LastTxReport=3026352 Flags=0x0013 [ RxClosing TxClosing SeenNonSyn ] RevNAT=0 SourceSecurityID=3096 IfIndex=0 BackendID=0
ICMP OUT 10.64.0.36:0 -> 10.64.0.64:0 related expires=3026412 Packets=0 Bytes=0 RxFlagsSeen=0x00 LastRxReport=0 TxFlagsSeen=0x02 LastTxReport=3026352 Flags=0x0000 [ ] RevNAT=0 SourceSecurityID=3096 IfIndex=0 BackendID=0

Pod2 への Ingress

Pod1 からのパケットを NodeB が受信すると、まずはホストデバイスの cil_from_netdev で必要な処理を実行します。
その後、コンテナのホスト側 veth に転送され、cil_to_container プログラムで以下のように conntrack に関連する処理が実施されます。

※ 箇条書きの番号は上図の NodeB 上のの番号と対応しています。

  1. 受信したパケットに紐づくエントリを lookup します
    1. cilium/bpf/bpf_lxc.c at v1.16.12 · cilium/cilium · GitHub
    2. 新規通信の場合、CT_NEW アクションが選択されます
  2. CT map に CT_INGRESS のエントリを作成します
    1. cilium/bpf/bpf_lxc.c at v1.16.12 · cilium/cilium · GitHub
    2. 上図の NodeB CT mapCT_INGRESS エントリを作成します
    3. ICMP を扱うために、cilium_ct_any4_global CT map にもエントリが作成されます
  3. Pod1 への返信パケットは、すでに (5) で CT_INGRESS エントリを作成しているため、このエントリを利用して管理され、送出されます

以下が実際の conntrack エントリです。

node-b$ cilium bpf ct list global | grep 10.64.0.64
TCP IN 10.64.0.36:54260 -> 10.64.0.64:8000 expires=3026362 Packets=0 Bytes=0 RxFlagsSeen=0x1b LastRxReport=3026352 TxFlagsSeen=0x1b LastTxReport=3026352 Flags=0x0013 [ RxClosing TxClosing SeenNonSyn ] RevNAT=0 SourceSecurityID=3096 IfIndex=0 BackendID=0
ICMP IN 10.64.0.36:0 -> 10.64.0.64:0 related expires=3026412 Packets=0 Bytes=0 RxFlagsSeen=0x02 LastRxReport=3026352 TxFlagsSeen=0x00 LastTxReport=0 Flags=0x0000 [ ] RevNAT=0 SourceSecurityID=3096 IfIndex=0 BackendID=0

ClusterIP Service の通信

Cilium が管理する Kubernetes ネットワーク – ClusterIP Service の通信 で述べたように、パケットフローの観点からは ClusterIP の通信は Pod 間通信とあまり変わりませんが、conntrack の観点では少し複雑になっています。

本セクションでは NodeA 上の Pod1(10.64.0.36) から ClusterIP Service の SvcA(10.68.174.146:80) で、そのバックエンドの Pod2(10.64.0.98) に通信する場合を例に解説します。

ここでは、Pod1 の Egress/Ingress の処理に限って解説します。

※ ClusterIP 通信の Pod2 側の conntrack 処理は Pod 間通信と同じなので省略します。同じになる理由は Cilium が管理する Kubernetes ネットワーク – ClusterIP Service の通信 を参照してください。

Pod1 からの Egress

ClusterIP の通信が Pod 間通信と違うところは、Pod1 は ClusterIP の VIP(Virtual IP) 宛に通信するという点です。
しかし、実際には NodeA 上で Cilium がその ClusterIP に紐づくバックエンド Pod(今回は Pod2)に宛先を書き換えて転送します。
その過程で、Pod 間通信と異なる conntrack の処理が実行されます。

下図は Pod1(10.64.0.36) から ClusterIP Svc1(10.68.174.146) 宛の通信が Pod2(10.64.0.98) に転送されるまでの conntrack に関する処理を示しています。

Pod1 からの Egress 通信の conntrack 操作とパケットの流れ

Pod 間通信と同様に、Cilium 的に処理のエントリーポイントは cil_from_container です。

  1. Pod1 から通信を開始する時、その宛先が Service に紐づくアドレスかを Service map から検索します
    1. cilium/bpf/bpf_lxc.c at v1.16.12 · cilium/cilium · GitHub
    2. 今回は ClusterIP 宛の通信のためその ClusterIP の情報を取得します
  2. パケットに紐づいた CT_SERVICE のエントリを検索します
    1. cilium/bpf/lib/lb.h at v1.16.12 · cilium/cilium · GitHub
    2. 新規通信の場合、CT_NEW アクションが選択されます
    3. (1) で取得した ClusterIP の情報をもとに、実際に転送するバックエンドを決定します
      1. cilium/bpf/lib/lb.h at v1.16.12 · cilium/cilium · GitHub
      2. 余談ですが、session affinity の処理もここで実行します
    4. CT map に CT_SERVICE のエントリを作成します
      1. 選択したバックエンドの ID を ct_entry.backend_id に保存します
      2. Ingress on Pod1 で後述する reverse NAT のための ct_entry.rev_nat_index も保存します
        1. cilium/bpf/lib/lb.h at v1.16.12 · cilium/cilium · GitHub
  3. ClusterIP の VIP から選択したバックエンドの IP に DNAT(Destination NAT)します
    1. cilium/bpf/lib/lb.h at v1.16.12 · cilium/cilium · GitHub
  4. ここから Pod 間通信で通ったパスと合流して、CT map に CT_EGRESS のエントリを作成します
    1. 注意が必要なのは、この CT_EGRESS のエントリは返信パケットの処理で利用されるので、(2) で作成した CT_SERVICE のエントリの情報を引き継いでエントリが作成されます
      1. cilium/bpf/bpf_lxc.c at v1.16.12 · cilium/cilium · GitHub

以下が NodeA での CT map の様子です。

node-a# cilium bpf ct list global | grep 10.64.0.36
TCP SVC 10.64.0.36:59530 -> 10.68.174.146:80 expires=3034283 Packets=0 Bytes=0 RxFlagsSeen=0x00 LastRxReport=0 TxFlagsSeen=0x1b LastTxReport=3034273 Flags=0x0013 [ RxClosing TxClosing SeenNonSyn ] RevNAT=15 SourceSecurityID=0 IfIndex=0 BackendID=19
TCP OUT 10.64.0.36:59530 -> 10.64.0.98:8000 expires=3034283 Packets=0 Bytes=0 RxFlagsSeen=0x1b LastRxReport=3034273 TxFlagsSeen=0x1b LastTxReport=3034273 Flags=0x0013 [ RxClosing TxClosing SeenNonSyn ] RevNAT=15 SourceSecurityID=3096 IfIndex=0 BackendID=0
ICMP OUT 10.64.0.36:0 -> 10.64.0.98:0 related expires=3034333 Packets=0 Bytes=0 RxFlagsSeen=0x00 LastRxReport=0 TxFlagsSeen=0x02 LastTxReport=3034273 Flags=0x0000 [ ] RevNAT=15 SourceSecurityID=3096 IfIndex=0 BackendID=0

Pod1 への Ingress

次に Pod2 からの返信パケットが Pod1 に到着するまでの処理を解説します。
Pod1 からの Egress で示したように、Pod2 への通信は最終的には単なる Pod 間通信です。そのため、Pod2 はこの通信が ClusterIP 宛の通信であることを認識せず、単に Pod1 への返信としてパケットを送出します。

※ 以下は NodeB 上の Conntrack エントリです。このように、NodeB では CT_SERVICE のエントリは作成されていません。

node-b# cilium bpf ct list global | grep 10.64.0.36
TCP IN 10.64.0.36:59530 -> 10.64.0.98:8000 expires=3034283 Packets=0 Bytes=0 RxFlagsSeen=0x1b LastRxReport=3034273 TxFlagsSeen=0x1b LastTxReport=3034273 Flags=0x0013 [ RxClosing TxClosing SeenNonSyn ] RevNAT=0 SourceSecurityID=3096 IfIndex=0 BackendID=0
ICMP IN 10.64.0.36:0 -> 10.64.0.98:0 related expires=3034333 Packets=0 Bytes=0 RxFlagsSeen=0x02 LastRxReport=3034273 TxFlagsSeen=0x00 LastTxReport=0 Flags=0x0000 [ ] RevNAT=0 SourceSecurityID=3096 IfIndex=0 BackendID=0

下図は Pod2(10.64.0.98) からの返信パケットが Pod1(10.64.0.36) に到着して、Pod1Svc1(10.68.174.146:80) からの返信としてパケットを受け取るまでを示しています。

Pod2 からの Ingress の conntrack 操作とパケットの流れ

BPF プログラムのエントリーポイントは cil_to_container です。

  1. Pod2 からの返信パケットが NodeA に到達して、Pod1cil_to_container で処理されます
  2. CT map を検索して Pod1 からの Egress の (4) で作成した既存のエントリ(CT_EGRESS)を取得します
    1. cilium/bpf/bpf_lxc.c at v1.16.12 · cilium/cilium · GitHub
    2. TAIL_CT_LOOKUP4 というマクロ中のコードです
  3. 取得したエントリ中の rev_nat_index を元に、RevNAT map から ClusterIP の情報を取得し、reverse NAT します
    1. cilium/bpf/bpf_lxc.c at v1.16.12 · cilium/cilium · GitHub
    2. ここで reverse NAT して、パケットを ClusterIP からの返信に見せかけます
      1. cilium/bpf/lib/lb.h at v1.16.12 · cilium/cilium · GitHub
  4. コンテナ側の veth デバイスで Svc1 からのパケットとして受信します

LoadBalancer Service の通信

Cilium が管理する Kubernetes ネットワーク – LoadBalancer Service の通信 で述べたように、LoadBalancer の通信はさらに複雑で、中間ノードとバックエンドノードそれぞれの Ingress/Egress の BPF プログラムで conntrack の処理が発生します。

ここでは、以下のセクションに分けて解説します。

  • 中間ノードへの Ingress
    • クライアントから一次受けの中間ノードでパケットを受信した際に Geneve でカプセル化して転送します
  • バックエンドノードへの Ingress
    • バックエンドノードで Geneve パケットを受信して Pod に転送します
  • バックエンドノードからの Egress
    • Pod からクライアントに DSR で送信します

本セクションでは以下のような LoadBalancer の通信を例に取り、中間ノードとバックエンドノードの Ingress/Egress に分けて解説します。

  1. クラスタ外部のクライアント(172.191.0.1)から LoadBalancer Service の LB1(172.190.0.17:80) 宛に通信する
  2. NodeA が中間ノードとして振る舞い、バックエンド Pod に NodeB 上に存在する Pod1(10.64.0.39) を選択して転送する
  3. Pod1 からクライアントに DSR する

中間ノードへの Ingress

下図はクラスタ外部のクライアント(172.191.0.1)から LoadBalancer Service の LB1(172.190.0.17:80) 宛に通信した時、NodeA が中間ノードとして振る舞い、NodeB 上の Pod1(10.64.0.39) に到達するまでを示しています。

中間ノードへの Ingress の conntrack 操作とパケットの流れ

中間ノードでの処理は図内の eth0 の Ingress にアタッチされた cil_from_netdev でまずは処理されます。
LoadBalancer(以下 LB) と NodePort の処理は nodeport_lb4 という関数で実行されます。

cilium/bpf/bpf_host.c at v1.16.12 · cilium/cilium · GitHub

  1. 宛先アドレスとポートから Service map を検索して、LB 宛のパケットの場合、LB の情報を取得します
    1. cilium/bpf/lib/nodeport.h at v1.16.12 · cilium/cilium · GitHub
  2. 取得した LB の情報とパケットを元に CT_SERVICE のエントリを検索します
    1. cilium/bpf/lib/lb.h at v1.16.12 · cilium/cilium · GitHub
    2. この辺りの処理は lb4_local で処理されていて、ClusterIP の時と同一です
    3. 新規通信の場合、 CT_NEW アクションが選択されます
    4. (1) で取得した LB の情報をもとに、実際に転送するバックエンドを決定します
      1. cilium/bpf/lib/lb.h at v1.16.12 · cilium/cilium · GitHub
      2. Maglev hashing に基づいてバックエンド Pod の選択を行います
    5. CT map に CT_SERVICE のエントリを作成します
    6. パケットの宛先をバックエンド Pod に DNAT します
  3. 選択したバックエンド Pod が中間ノード上に存在するかを判定します
    1. cilium/bpf/lib/nodeport.h at v1.16.12 · cilium/cilium · GitHub
    2. 中間ノードにバックエンド Pod が存在するなら CT map に CT_EGRESS のエントリを作って Pod に転送します
      1. cilium/bpf/lib/nodeport.h at v1.16.12 · cilium/cilium · GitHub
      2. 本記事では、選択したバックエンド Pod が中間ノード上に存在した場合の解説を省略します
  4. LB の情報を Geneve パケットのオプション領域に詰めて cilium_geneve デバイスに転送します
    1. cilium/bpf/lib/nodeport.h at v1.16.12 · cilium/cilium · GitHub
    2. Geneve のカスタムオプション構造体に、LB の EXTERNAL-IP とポートを保存します
      1. cilium/bpf/lib/nodeport.h at v1.16.12 · cilium/cilium · GitHub
    3. 保存した情報がリダイレクト時に引き渡されるように登録します
      1. cilium/bpf/lib/nodeport.h at v1.16.12 · cilium/cilium · GitHub
    4. cilium_geneve へリダイレクトします
      1. cilium/bpf/lib/nodeport.h at v1.16.12 · cilium/cilium · GitHub
  5. cilium_geneve でカプセル化され、 Geneve パケットが NodeB に向けて送信されます
    1. 実際には eth0 を経由して NodeB に転送されますが、簡単のため、ここでは cilium_geneve から矢印を出しています

以下は中間ノードの実際の CT map の様子です。

node-a# cilium bpf ct list global --time-diff | grep 172.191.0.1
TCP SVC 172.191.0.1:53438 -> 172.190.0.17:80 expires=3619996 (remaining: -305 sec(s)) Packets=0 Bytes=0 RxFlagsSeen=0x00 LastRxReport=0 TxFlagsSeen=0x1b LastTxReport=3619986 Flags=0x0013 [ RxClosing TxClosing SeenNonSyn ] RevNAT=24 SourceSecurityID=0 IfIndex=0 BackendID=24

バックエンドノードへの Ingress

下図は NodeB が Geneve パケットを受け取って、カプセル化を解いて Pod1(10.64.0.39) にパケットを転送する様子を示しています。
バックエンドノードでは NAT map という BPF マップが利用されます。

バックエンドノードへの Ingress の conntrack 操作とパケットの流れ

バックエンドノードに到達した Geneve パケットはまず eth0 で受信してカーネル内部で cilium_geneve に転送されます。図では簡単のため cilium_geneve で直接受信するように描いています。主な処理は cilium_geneve にアタッチされた cil_from_overlay で実行されます。

  1. cilium_geneve に Geneve でカプセル化した LB のパケットが到達します

    1. cilium_geneve の Ingress TC で処理する段階ではすでにカーネルによってカプセル化が解かれています
  2. 中間ノードへの Ingress と同様に nodeport_lb4 が実行されます

    1. cil_from_overlay から実行されるので、IS_BPF_OVERLAY=1 で以下が実行されます
      1. cilium/bpf/lib/nodeport.h at v1.16.12 · cilium/cilium · GitHub
    2. DSR が必要なパケットの場合、Geneve オプションから LB のアドレスとポート情報を取り出します
      1. cilium/bpf/lib/nodeport.h at v1.16.12 · cilium/cilium · GitHub
  3. CT map から CT_EGRESS かつ DSR フラグ(ct_entry.dsr_internal)が立っているエントリを検索します
    1. cilium/bpf/lib/nodeport.h at v1.16.12 · cilium/cilium · GitHub
    2. 新規通信の場合、CT_NEW アクションが選択されます
    3. ここで作るエントリは Egress 用なので入ってきたパケットと宛先を逆向きにしてエントリを作ります
      1. cilium/bpf/lib/nodeport.h at v1.16.12 · cilium/cilium · GitHub
    4. CT map に CT_EGRESS かつ dsr_internal=true でエントリを作ります
      1. cilium/bpf/lib/nodeport.h at v1.16.12 · cilium/cilium · GitHub
  4. DSR のために NAT map に (3) で作成した CT エントリのキーと LB の情報を保存します
    1. cilium/bpf/lib/nodeport.h at v1.16.12 · cilium/cilium · GitHub
  5. Pod1 の veth デバイスにパケットを転送します
    1. cilium/bpf/bpf_overlay.c at v1.16.12 · cilium/cilium · GitHub
    2. パケットのデータ構造(sk_buff)のメタデータ領域に from_tunnel=true (トンネルデバイスからのパケットであるフラグ)を保存します
      1. cilium/bpf/lib/l3.h at v1.16.12 · cilium/cilium · GitHub
    3. 転送します
      1. cilium/bpf/bpf_overlay.c at v1.16.12 · cilium/cilium · GitHub
  6. Pod1 のホスト側 veth デバイスにアタッチされた cil_to_container で処理されます
    1. Pod 間通信の Ingress と同様の処理が実行されます
      1. 異なる点として、(4-b) でsk_buff のメタデータに保存した from_tunnel=true を扱うことが挙げられます
    2. CT map を CT_INGRESS かつ ct_entry.from_tunnel=true でエントリを作成します

以下はバックエンドノードの実際の CT map の様子です。

node-b# cilium bpf ct list global | grep 10.64.0.39
TCP IN 172.191.0.1:53438 -> 10.64.0.39:8000 expires=3619996 Packets=0 Bytes=0 RxFlagsSeen=0x1b LastRxReport=3619986 TxFlagsSeen=0x1b LastTxReport=3619986 Flags=0x0413 [ RxClosing TxClosing SeenNonSyn FromTunnel ] RevNAT=0 SourceSecurityID=2 IfIndex=0 BackendID=0
TCP OUT 172.191.0.1:53438 -> 10.64.0.39:8000 expires=3619996 Packets=0 Bytes=0 RxFlagsSeen=0x1b LastRxReport=3619986 TxFlagsSeen=0x1b LastTxReport=3619986 Flags=0x0093 [ RxClosing TxClosing SeenNonSyn DSRInternal ] RevNAT=0 SourceSecurityID=2 IfIndex=0 BackendID=0
ICMP IN 172.191.0.1:0 -> 10.64.0.39:0 related expires=3620046 Packets=0 Bytes=0 RxFlagsSeen=0x02 LastRxReport=3619986 TxFlagsSeen=0x00 LastTxReport=0 Flags=0x0400 [ FromTunnel ] RevNAT=0 SourceSecurityID=2 IfIndex=0 BackendID=0

バックエンドノードからの Egress

下図は Pod1(10.64.0.39) がクライアントからのパケットを処理して、DSR でクライアントに直接返信パケットを送信する様子を示しています。

バックエンドノードからの Egress の conntrack 操作とパケットの流れ

エントリーポイントは cil_from_container で、続いて cil_to_netdev に引き渡されて処理が実行されます。

  1. CT map を CT_EGRESS で検索します
    1. cilium/bpf/bpf_lxc.c at v1.16.12 · cilium/cilium · GitHub
    2. エントリが存在するので、それに従って必要な処理をします
  2. eth0 にパケットを転送します

    1. cilium/bpf/lib/nodeport.h at v1.16.12 · cilium/cilium · GitHub
  3. eth0 上の cil_to_netdev で DSR のパケットか判定して、NAT map を検索します

    1. cilium/bpf/bpf_host.c at v1.16.12 · cilium/cilium · GitHub
    2. NAT map から取得した情報を元に、パケットの送信元アドレスとポートを LB のものに SNAT します
      1. cilium/bpf/lib/nodeport.h at main · cilium/cilium · GitHub
  4. 外部のクライアントに転送します

Cilium の conntrack の状態管理

Conntrack は CT map にエントリを作って終わりではありません。特に TCP の場合、End to End のコネクションの状態を追跡、管理しなければなりません。
Cilium は BPF プログラム上で観測した TCP フラグに応じて conntrack エントリのフラグを立てて状態を管理しています。

CT エントリの詳細は CT map – バリュー構造体(ct_entry)を参照して下さい。

TCP の状態管理で重要なのは主に以下のフィールドです。

  • lifetime
    • そのエントリの有効期限が切れる時刻のタイムスタンプ(以降、生存期間と記述します)
    • 生存期間が切れていたら、後述の GC で削除されます
  • tx_closing
    • 送信側の TCP FIN を観測したら立ちます
  • rx_closing
    • 受信側の TCP FIN を観測したら立ちます
  • seen_no_syn
    • TCP SYN 以外のフラグのついた TCP パケットを観測したら立ちます

tx_closing/rx_closing

tx_closingrx_closing は TCP の終了処理に関連するフラグで、送信側、受信側それぞれで TCP FIN フラグを観測した場合に 1 がセットされます。
ct_tcp_select_action 関数で TCP パケットのフラグを解析して、対応する CT エントリにどのような操作を行うかを決定します。FINRST を観測すると ACTION_CLOSE が返ります。

cilium/bpf/lib/conntrack.h at v1.16.12 · cilium/cilium · GitHub

ACTION_CLOSE が選択されると、__ct_lookup 関数で CT エントリを取得したときに、それぞれの CT タイプ(ここでは dir 変数)に応じて、tx_closingrx_closing のフラグがセットされます。

CT_SERVICE のエントリについては ClusterIP の時と LoadBalancer(NodePort) + DSR の時でパケットの向きは異なりますが、どちらも TCP フラグを片方向からしか観測できません。
そのため、片方の FIN を観測した時に tx_closingrx_closing 共にセットしています。

  • ClusterIP
  • LoadBalancer(NodePort) + DSR

CT エントリがまだ有効かどうかは ct_entry_alive 関数で判定されます。
逆に、これが false を返した時は、そのエントリのコネクションは終了したとみなされます。

cilium/bpf/lib/conntrack.h at v1.16.12 · cilium/cilium · GitHub

lifetime

前述の通り、lifetime はそのエントリの生存期間です。TCP や UDP のプロトコル、追跡している TCP の状態などにより細かく生存期間が定められています。
その値は cilium-agent のフラグで指定できます。v1.16.12 現在は以下のようになっています。

--bpf-ct-timeout-regular-any duration                       Timeout for entries in non-TCP CT table (default 1m0s)
--bpf-ct-timeout-regular-tcp duration                       Timeout for established entries in TCP CT table (default 2h13m20s)
--bpf-ct-timeout-regular-tcp-fin duration                   Teardown timeout for entries in TCP CT table (default 10s)
--bpf-ct-timeout-regular-tcp-syn duration                   Establishment timeout for entries in TCP CT table (default 1m0s)
--bpf-ct-timeout-service-any duration                       Timeout for service entries in non-TCP CT table (default 1m0s)
--bpf-ct-timeout-service-tcp duration                       Timeout for established service entries in TCP CT table (default 2h13m20s)
--bpf-ct-timeout-service-tcp-grace duration                 Timeout for graceful shutdown of service entries in TCP CT table (default 1m0s)

cilium/Documentation/cmdref/cilium-agent.md at v1.16.12 · cilium/cilium · GitHub

これらのフラグの値は最終的に、以下の定数に埋め込まれて BPF プログラム上で利用されます。

#define CT_CONNECTION_LIFETIME_TCP  21600
#define CT_CONNECTION_LIFETIME_NONTCP  60
#define CT_SERVICE_LIFETIME_TCP        21600
#define CT_SERVICE_LIFETIME_NONTCP 60
#define CT_SERVICE_CLOSE_REBALANCE 30
#define CT_SYN_TIMEOUT         60
#define CT_CLOSE_TIMEOUT       10

cilium/bpf/node_config.h at v1.16.12 · cilium/cilium · GitHub

lifetime も様々な箇所で更新されますが、主に更新するのは ct_update_timeout 関数です。

cilium/bpf/lib/conntrack.h at v1.16.12 · cilium/cilium · GitHub

この関数から呼び出される __ct_update_timeout 関数で lifetime の値を 現在時刻 + その状態のタイムアウト値 で上書きします。

cilium/bpf/lib/conntrack.h at v1.16.12 · cilium/cilium · GitHub

ct_update_timeout は上述の __ct_lookup 関数の中で呼び出されるため、パケット毎に生存期間は更新されます。

このようにして、BPF プログラム上ではエントリの生存期間を管理していて、期限切れのエントリの削除は後述の GC により実施されます。

ガーベジコレクション(GC)

これまでは、BPF プログラム上で Cilium の conntrack がどのように実装されているかを見てきました。

Cilium は conntrack の GC を cilium-agent 内のコンポーネントとして実装しており、CT map のエントリ削除を全て GC で実施しています。
GC がトリガーされる要因は以下の 2 つです。

  • インターバル
    • 生存期間(lifetime)が切れたエントリを全て削除します
  • endpoint regeneration (本記事ではスコープ外です)
    • Pod が削除されるなど、様々なイベントによりトリガーされます
    • 対象の Pod に関連するエントリを全て削除します
    • この時、対象 Pod と無関係な生存期間切れのエントリは削除されません

ここではインターバルによりトリガーされる GC を解説します。

GC インターバル計算

以下に示す goroutine が cilium-agent 起動時に起動して、GC インターバル毎に GC が実行されます。

cilium/pkg/maps/ctmap/gc/gc.go at v1.16.12 · cilium/cilium · GitHub

Cilium はデフォルトでは GC インターバルはあらかじめ設定された最大、最小値の範囲内で自動計算された値を利用します。

cilium/pkg/maps/ctmap/ctmap.go at v1.16.12 · cilium/cilium · GitHub

func calculateInterval(prevInterval time.Duration, maxDeleteRatio float64) (interval time.Duration) {
    interval = prevInterval

    if maxDeleteRatio == 0.0 {
        return
    }

    switch {
    case maxDeleteRatio > 0.25:
        if maxDeleteRatio > 0.9 {
            maxDeleteRatio = 0.9
        }
        
        interval = time.Duration(float64(interval) * (1.0 - maxDeleteRatio)).Round(time.Second)

        if interval < defaults.ConntrackGCMinInterval {
            interval = defaults.ConntrackGCMinInterval
        }

    case maxDeleteRatio < 0.05:
        
        
        
        
        interval = time.Duration(float64(interval) * 1.5).Round(time.Second)
        if interval > defaults.ConntrackGCMaxLRUInterval {
            interval = defaults.ConntrackGCMaxLRUInterval
        }
    }

    cachedGCInterval = interval

    return
}

前回の GC のエントリ削除率 maxDeleteRatio と、前回の GC インターバル prevInterval から次の GC インターバル interval を算出します。

maxDeleteRatio が 25% より大きい時は以下の式に従って interval を計算します。

interval = prevInterval * (1.0 – maxDeleteRatio)

つまり、次の GC は前回の GC よりも短い間隔で実行されます。

一方、maxDeleteRatio が 5% より小さい時は以下の式に従います。

interval = prevInterval * 1.5

つまり、次の GC は前回の GC よりも 1.5 倍長い間隔で実行されます。
最後に、計算した値が設定した最大、最小値の範囲内にあるかどうかを確かめて、次回の GC インターバルが決定します。

GC の内部処理

GC のエントリーポイントは runGC 関数です。

cilium/pkg/maps/ctmap/gc/gc.go at v1.16.12 · cilium/cilium · GitHub

cilium_ct4_globalcilium_ct_any4_global それぞれに対して生存期間切れのエントリの削除が実行されます。
各マップのエントリの削除は doGC4 関数内で処理されます。

cilium/pkg/maps/ctmap/ctmap.go at v1.16.12 · cilium/cilium · GitHub

最終的に、Map.DumpReliablyWithCallback(cb DumpCallback, stats *DumpStats) という関数が実行されて、マップ内の全てのエントリを Lookup して、それらに対して引数に渡している DumpCallback 型のコールバック関数 cb がそれぞれのエントリに対して呼び出されます。

cilium/pkg/bpf/map_linux.go at v1.16.12 · cilium/cilium · GitHub

ここに渡すコールバック関数は doGC4 内で定義されていて、このコールバック関数の中で、削除対象であればエントリを Remove します。

cilium/pkg/maps/ctmap/ctmap.go at v1.16.12 · cilium/cilium · GitHub

生存期間切れかどうかは以下で検査しています。

cilium/pkg/maps/ctmap/ctmap.go at v1.16.12 · cilium/cilium · GitHub

ここで、Lookup、Remove を強調したのは、これらが個別のシステムコールとして呼び出されるためです。

つまり、GC では 各マップの総エントリ数 + 削除対象エントリ数 分の bpf システムコールが発行されます。数百万エントリ存在するような CT map の GC にはかなりの時間と CPU 負荷がかかります。

※ Cilium 1.18 から、CT map の Lookup が Batch Lookup に変更されました。
これは bpf_map_lookup_batch を利用したもので、1 回のシステムコールで指定した数のエントリを一気に Lookup できます。
これにより、GC の負荷低減が期待されます。

Neco では過去に CT map 使用率が 80% まで増大したことがあります。
その時の状況や対処について、過去にブログを書いているので興味のある方は読んでみてください。

blog.cybozu.io

まとめ

以上、Kubernetes ネットワークにおける Cilium の conntrack の挙動と実装を解説しました。

eBPF によるパケット処理の実装では、基本的には Egress/Ingress それぞれの Pod が存在するノード上に、それぞれの方向の CT エントリが作成されます。また、それに応じて ICMP 用のエントリも作成されます。
それに加えて、Service 経由の通信は、その通信が Service のものであることを示すために特別な CT エントリが別途作成されることになります。
それらの CT エントリを通信の状態に応じて更新しています。

また、cilium-agent が定期的に GC して、CT map を健全な状態に保っています。

このように、Cilium は eBPF を用いて conntrack を実装しており、これを基礎として高度な通信制御を実現しています。




Source link

関連記事

コメント

この記事へのコメントはありません。