eBPF ソケット レベル リダイレクトのカーネル実装の詳細を図解します。

eBPF ソケット レベル リダイレクトのカーネル実装の詳細を図解します。

みなさんこんにちは。私は次男です。最近eBPFを勉強しています。研究が深まるにつれ、以前書いた記事にいくつか問題点があることがわかったので、修正しました。図も再描画され、サイドカーレスに関する追加コンテンツもいくつか追加されました。

以下、本文です。

前回の記事「 eBPF を使用してソケット レベル リダイレクトを実装する」では、eBPF、ソケット レベル リダイレクトのアプリケーション シナリオについて全体的に紹介しました。マシン上にループバック デバイスを介して相互にデータを送受信する必要がある 2 つのプロセスがある場合、ebpf を使用して、送信プロセス側のローカル マシンの基礎となる TCP/IP プロトコル スタックをスキップし、宛先プロセスのソケットに直接渡すことで、カーネル内のデータ処理パスと時間を短縮できます。

このプロセスは図 1 に示されています。この記事では、図 1 の右側のカーネルの実装の詳細を詳しく見ていきます。

図1: ebpfを使用してソケットレベルのリダイレクトを行い、TCP/IPスタックとloデバイスをスキップする

まずは全体像を見て、その全体像から重要な知識ポイントを一つずつ分析していきます。

図 2: ebpf を使用したソケット レベル リダイレクトのグローバル詳細

1. 準備

(1)bpf_sock_opsをsock_hashマップに挿入する

私たちの話は、図 3 に示すように、bpf_sock_ops を sock ハッシュ マップに挿入することから始まります。次に、いくつかのコード スニペットを示します。完全なコンパイル可能、インストール可能、実行可能なコードは、https://github.com/LanceHBZhang/socket-acceleration-with-ebpf にあります。さらに、ebpf プログラムの完全なインストール プロセスには cgroup も含まれますが、これについては詳しく説明しません。

次のコードでは、特別なマップ タイプ BPF_MAP_TYPE_HASH を使用します。この記事では、これを sock_hash とも呼びます。 KV タイプのデータを格納し、その値は実際にはデータ構造 struct bpf_sock_ops に対応します。 bpf_sock_ops を保存するだけでなく、ユーザーが作成した sk_msg タイプの bpf プログラムをこのタイプのマップにアタッチして、データを受信するソケットを見つけることもできます。アタッチステートメントについては、github コードを参照してください。

静的インライン
void bpf_sock_ops_ipv4 (構造体bpf_sock_ops * skops )
{
構造体 sock_keyキー= {};
整数戻り値;

extract_key4_from_ops ( skops& key ) を実行します。

ret = sock_hash_update ( skops& sock_ops_map& keyBPF_NOEXIST );
戻り値が0場合
printk ( "sock_hash_update() が失敗しました、戻り値: %d\n" , ret );
}

printk ( "sockmap: op %d、port %d --> %d\n" ,
skops -> opskops -> local_portbpf_ntohl ( skops -> Remote_port ));
}

__section ( "sockops" )
int bpf_sockmap (構造体bpf_sock_ops * skops )
{
スイッチ( skops -> op ) {
BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CBの場合:
BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CBの場合:
if ( skops -> family == 2 ) { //AF_INET
bpf_sock_ops_ipv4 (スコップ);
}
壊す;
デフォルト
壊す;
}
0を返します
}

上記のコードで sock_ops タイプの bpf 関数 bpf_sockmap() を cgroupv2 のルート パスにアタッチすると、アクティブまたはパッシブ接続の確立などのソケット イベントが発生したときに、bpf 関数 bpf_sockmap() が呼び出されます。このプロセスは、図 3 の実行ポイント 1.1 に示されています。より具体的には、1.1 で行われるのは 3 ウェイ ハンドシェイク (SYN / SYNC-ACK / ACK) です。これは 3 ウェイ ハンドシェイクなので、当然通信する両方の当事者に関係し、bpf_sock_ops_ipv4(skops) は bpf_sockmap() 関数内で 2 回呼び出されます。

bpf_sockmap() が行うことは非常にシンプルです。ソース IP / ソース ポート / 宛先 IP / 宛先ポート / ファミリをキーとして使用し、struct bpf_sock_ops オブジェクトを sock_hash に格納します。このプロセスは、図 3 の実行ポイント 1.2 に示されています。 bpf_sockmap() が ebpf に関連していることを示すために、1.2 で ebpf のロゴを描きました。

上記のコードでは、 sock_hash_update() 関数呼び出しが sock_has マップを更新しているように見えますが、実際にはカーネル内で実行される処理の方が重要であり、TCP プロトコル関連の関数を正確かつ動的に置き換えます。

図3: sock_hashマップにsockを挿入する

(2)プロトコールの正確な動的置換

カーネル プロトコル スタックに関連するデータ構造に注目していれば、次の図に示すように、struct file / struct socket / struct sock / struct proto などのいくつかの重要な役割に必ず遭遇するでしょう。

ソケットは、デザインパターンでよく使用されるアダプタのようなものです。一方では、アプリケーション層に面した struct ファイルに適応し、他方では、 struct sock を参照してネットワーク プロトコル スタックを直列に接続します。

この図をよく見ると、 struct sock が魂であり、その内容からそれを垣間見ることができることがわかります。 struct sock には非常に重要な部分があります。それは、ネットワーク プロトコルへの参照であり、先ほど見た sk_prot です。なぜそれが重要なのでしょうか? sk_prot がポインターとして指す構造体 tcp_prot には、recvmsg や sendmsg など、TCP プロトコルに不可欠な一連の関数が含まれているため、この記事ではこれらに焦点を当てる必要があります。名前から使用シナリオもわかります。TCP 層でデータの送受信に使用されます。

もちろん、struct tcp_prot に加えて、sk_prot は struct udp_prot/ping_prot/raw_prot を指すこともできます。

図4: ファイル/ソケット/sock/操作 (画像出典: 開発内部パワー育成WeChatアカウント)

では、ebpf はここで何をするのでしょうか?それはとても賢いですね。これは、struct proto 内の recvmsg/sendmsg 関数を動的に置き換えます。たとえば、recvmsg は元の tcp_recvmsg から tcp_bpf_recvmsg に置き換えられ、tcp_sendmsg は tcp_bpf_sendmsg に置き換えられます。

静的void tcp_bpf_rebuild_protos (構造体 proto prot [ TCP_BPF_NUM_CFGS ]、構造体proto *ベース)
{
prot [ TCP_BPF_BASE ] = *ベース;
プロトコル[ TCP_BPF_BASE ]。閉じる= sock_map_close ;
プロトコル[ TCP_BPF_BASE ]。受信メッセージ= tcp_bpf_recvmsg ;
プロトコル[ TCP_BPF_BASE ]。 sock_is_readable = sk_msg_is_readable ;

prot [ TCP_BPF_TX ] = prot [ TCP_BPF_BASE ];
prot [ TCP_BPF_TX ] .sendmsg = tcp_bpf_sendmsg ;
prot [ TCP_BPF_TX ] .sendpage = tcp_bpf_sendpage ;

prot [ TCP_BPF_RX ] = prot [ TCP_BPF_BASE ];
プロトコル[ TCP_BPF_RX ]。 tcp_bpf_recvmsg_parserは、次のようになります。

prot [ TCP_BPF_TXRX ] = prot [ TCP_BPF_TX ];
プロトコル[ TCP_BPF_TXRX ]。 tcp_bpf_recvmsg_parserは、次のようになります。
}

静的int __init tcp_bpf_v4_build_proto ( void )
{
tcp_bpf_rebuild_protos ( tcp_bpf_prots [ TCP_BPF_IPV4 ]、およびtcp_prot );
0を返します
}
遅い_initcall ( tcp_bpf_v4_build_proto );

int tcp_bpf_update_proto (構造体sock * sk構造体sk_psock * psockbool復元)
{
// ...
/* sk_clone_lock() のロックレス読み取りとペアになります */
WRITE_ONCE ( sk -> sk_prot& tcp_bpf_prots [ファミリ][構成] );
0を返します
}

単純な置き換えは実際には賢明ではありません。私の次兄は、ここでの置き換えは「正確な動的置き換え」なので巧妙だと言いました。

まず、なぜ精密置換と呼ばれるのでしょうか?考えてみてください。すべてのサービスがローカルプロセス間の通信にループバックを使用する必要はありません。さらに、プロセス間通信がこのように実行される場合でも、すべてのシナリオでソケット レベルのリダイレクトを使用する必要があるわけではありません。そのため、交換操作は広く使用できず、必要な場合にのみ交換できます。いわゆる「動的置換」とは、カーネルのコンパイル時に直接置換されるのではなく、必要なときに置換されることを意味します。

では、この「必要な時」とはいつなのでしょうか?

答えは、bpf_sock_ops が sock_hash に格納されるときです。これは、図 3 に関係するプロセスです。システム関数 bpf_sock_hash_update が呼び出されると、カーネルは net/core/sock_map.c にある sock_hash_update_common 関数を呼び出し、これにより、呼び出しチェーン内の置換関数 tcp_bpf_update_proto() の呼び出しが完了します。実際の置換の結果、sk->sk_prot は置換されたバージョン、つまり tcp_bpf_prots[family][config] を保存しますが、tcp_bpf_prots は非常に早い段階で初期化されています。

ここでの置換操作は、実際にソケット レベルのリダイレクトを使用する必要がある sock にのみ関連し、他の sock には影響しないことを再度強調します。もちろん、交換した靴下は実際には一足です。図 3 では、envoy プロセスとプロセス B はそれぞれ通信プロセスに参加する独自の sock を持っているため、独自の recvmsg/sendmsg を置き換える必要があることがお分かりでしょう。

2. sk_psock

図 3 では、TX キューと RX キューから独立した新しいキュー ingress_msg も確認できます。通信する双方のソケット層にはこのようなキューがあります。キューに一時的に保存されるデータは、構造体 struct sk_msg によって表されます。 sk_msg には、これまでよく知られている skb が含まれているため、その具体的な定義は省略します。次のデータ送受信プロセスの説明では、ingress_msg キューがどのように機能するかを確認します。

このキューは、構造体 struct sk_psock {} にあります。 sk_psock には、その仲間の sock/eval/cork なども含まれています。

カーネル コードには、sk_psock の読み取りと書き込みを行う psock->eval ステートメントが多数あります。さらに、この構造体には関数ポインタ psock_update_sk_prot があり、これは前のセクションで説明した関数 tcp_bpf_update_proto() を指していることがわかります。

構造体sk_psock {
構造体sock * sk ; // 現在のソックス
構造体sock * sk_redir ; // ピア ソック (リダイレクト ターゲットとも呼ばれる)
u32バイトを適用します
u32コルクバイト;
u32評価;
構造体sk_msg *コルク;
構造体sk_psock_progsプログラム;
#IS_ENABLEDの場合( CONFIG_BPF_STREAM_PARSER )
構造体strparser strp ;
#終了
構造体sk_buff_head ingress_skb ;
構造体list_head ingress_msg ;
spinlock_tイングレスロック;
符号なしロング状態;
構造体 list_headリンク;
spinlock_tリンクロック;
refcount_t参照カウント;
void ( * saved_unhash )( struct sock * sk );
void ( * saved_close )( struct sock * sk , long timeout );
void ( * saved_write_space )( struct sock * sk );
void ( * saved_data_ready )(構造体sock * sk );
int ( * psock_update_sk_prot )( struct sock * skstruct sk_psock * psockbool復元);
構造体proto * sk_proto ;
構造体ミューテックスwork_mutex ;
構造体sk_psock_work_state作業状態;
構造体work_struct作業;
構造体 rcu_work rwork ;
}

3. データを送信する

プロセス B との TCP 接続が正常に確立されると、プロセス envoy は図 5 の 2.1 に示すようにデータの書き込みを開始します。

通常の状況では、write() システム コールによって渡されたデータは、最終的に TCP 層の tcp_sendmsg() によって処理されます。しかし、「prot の正確な動的置換」のセクションで、tcp_sendmsg() が tcp_bpf_sendmsg() に置き換えられたと述べたことを覚えていますか?つまり、ここでの主役は実際には tcp_bpf_sendmsg() です。

図5: データ送信プロセス

図 5 に、tcp_bpf_sendmsg() が実行する重要な処理をいくつか示しました。

(1)実行ポイント2.3:ebpプログラムを実行する

ebpf プログラムは実際にはかなり前に準備して sock_hash にアタッチする必要があります (このプロセスについては、上記に添付されている github コードを参照してください)。

プログラムのエントリ ポイントは非常にシンプルです: bpf_redir()。また、struct sk_msg_md から送信元 IP / 送信元ポート / 宛先 IP / 宛先ポート / ファミリを抽出し、それをキーとして通信相手の struct sock である sock_hash 内のリダイレクト先を探し、psock->sk_redir に格納します。

静的インライン
void extract_key4_from_msg (構造体sk_msg_md * msg構造体sock_key *キー)
{
キー-> sip4 =メッセージ-> remote_ip4 ;
キー-> dip4 =メッセージ-> local_ip4 ;
キー- >ファミリー= 1 ;

キー-> dport = ( bpf_htonl ( msg -> local_port ) >> 16 );
key -> sport = FORCE_READ ( msg -> remote_port ) >> 16 ;
}

__section ( "sk_msg" )
int bpf_redir (構造体sk_msg_md *メッセージ)
{
構造体 sock_keyキー= {};
extract_key4_from_msg (メッセージ&キー)
msg_redirect_hash ( msg& sock_ops_map& keyBPF_F_INGRESS );
SK_PASSを返します
}

コード内の msg_redirect_hash() という名前は少し誤解を招きます。一見すると、リダイレクト処理はここで完了したと思いました。実際には、マップの検索とリダイレクトが許可されているかどうかの確認という 2 つの操作のみが実行されます。本当の楽しみはまだこれからです。コードは長くないので、ここにすべて貼り付けます。

 BPF_CALL_4 ( bpf_msg_redirect_hashstruct sk_msg *msgstruct bpf_map *mapvoid *keyu64flags )
{
構造体sock * sk ;

if (ありそうにない(フラグ& ~ ( BPF_F_INGRESS )))
SK_DROPを返します

sk = __sock_hash_lookup_elem (マップキー);
if (可能性は低い( ! sk || ! sock_map_redirect_allowed ( sk )))
SK_DROPを返します

msg- > flags =フラグ;
msg- > sk_redir = sk ;
SK_PASSを返します
}

(2)実行ポイント2.4:sk_msgをキューに入れる

ここで、ingress_msg キューが初めて動作するのを確認します。

struct sk_psock {} には eval というメンバーがあります。このキーワードから、評価結果に関係するものだと推測できるでしょう。では評価の対象は誰でしょうか? 2.3 で実行する必要があるのは ebpf プログラムです。 2.3 での実行結果は、後で使用するために psock->eval に格納されます。

実行結果は __SK_PASS / __SK_REDIRECT / __SK_DROP の 3 つになります。 psock->eval が、注目する __SK_REDIRECT と等しい場合、ポイント 2.4 の実行プロセスが開始され、sk_msg がキュー psock->ingress_msg に配置されます。

関数呼び出しチェーンは次のようになります。

 tcp_bpf_sendmsg () -> tcp_bpf_send_verdict () -> tcp_bpf_sendmsg_redir ( psock -> sk_redir ) -> bpf_tcp_ingress () -> sk_psock_queue_msg ()

このプロセスでは、ingress_msg キューは ProcessB プロセスに属していることに注意してください。つまり、このステップでは、データ パケットはピア プロセスのキューに直接配置されます。

(3)実行ポイント2.5:プロセスBを起動する

実行ポイント 2.4 で sk_msg を Process B の psock->ingress_msg に格納した後、カーネルは他の関数をさらに呼び出すことはせず、sk->sk_data_ready() を介してピア ソック上のデータを待機しているスリープ状態のプロセス ProcessB を直接起動します。 sk_data_ready は実際には関数ポインターであり、実際に呼び出される関数は sock_def_readable() です。

4. データを受信する

前のセクションの実行ポイント 2.5 では、データ送信プロセスが最終的に sk_data_ready() を通じてプロセス B を起動することを説明しました。ここまででデータを送信する処理は終了し、ここからデータを受信する処理が始まります。プロセス全体に含まれる主要なステップを図 6: 実行ポイント 3.1/3.2/3.3 に示します。

(1)実行ポイント3.2:tcp_bpf_recvmsg

3.1 の read() 関数では、最終的に tcp_recvmsg() ではなく tcp_bpf_recvmsg() が呼び出されるようになります。その理由は、記事の冒頭にある「正確な動的置換」のセクションにあります。このセクションについては、私の次兄がすでに基礎を築いています。 tcp_bpf_recvmsg() によって行われる作業は、実際には複雑ではありません。 ingress_msg キュー内のデータを消費します。

図6: データ受信プロセス

5. スペアタイヤ

図 2 をもう一度見ると、ブラザーが tcp_bpf_sendmsg() と tcp_bpf_recvmsg() からそれぞれ tcp_sendmsg() と tcp_recvmsg() に点線を引いていることがわかります。私は tcp_sendmsg() と tcp_recvmsg() をスペアパーツと呼んでいます。 tcp_bpf_sendmsg() と tcp_bpf_recvmsg() は、異常な状況を処理する際には従来の方法を使用するためです。

静的int tcp_bpf_recvmsg (構造体sock * sk構造体msghdr * msgsize_t len ​​、
int非ブロックintフラグint * addr_len )
{
// ...
psock = sk_psock_get ( sk );
if (ありそうにない( ! pso​​ck ))
tcp_recvmsg ( skmsglennonblockflagsaddr_len )を返します
}

図 7: 代替の tcp_sendmsg() と tcp_recvmsg()

6. ソックレベルの加速 + クロスネットワーク NS = サイドカーレス

図8: ソケットレベルの加速

それでは、この記事で説明されているオブジェクト、つまりソケット レベルのリダイレクトについて見てみましょう。

ネットワーク パケットは、複雑な TCP プロトコル スタックを経由せず、IP 層ルーティング テーブルや iptables などの時間と労力を要するコンポーネントを使用せずに、新しいキューを介して直接相手側に送信されます。

これにより、ソケット レベルの高速化という非常に単純かつ直接的で大まかな利点がもたらされます。つまり、同じホスト上で、プロセス間通信はソケット レベルのリダイレクトを通じて大幅なパフォーマンスの向上を実現できます。

サイドカーベースのサービス メッシュに精通している場合は、Pod 内にループバックベースのプロセス間通信があることをご存知でしょう。また、通信側が仮想デバイス ループバックを使用して、実際のネットワーク デバイスに関連するキュー待機時間と、ネットワーク パケットがローカル マシンから離れた後のネットワーク遅延を排除した場合でも、ネットワーク パケットは TCP/IP プロトコル スタック上のすべてのステップに従う必要があることも説明しました。ルーティング テーブルと iptables の設定がより複雑な場合は、ルーティングとネット フィルターに費やす時間がさらに長くなります。

ソケット レベルのアクセラレーションの出現により、サイドカーが直面する固定的なネットワーク オーバーヘッドの問題は完全に解決されたと言えます。

これがもたらすメリットだけですか?いいえ!ソケット レベルのアクセラレーションには、ネットワーク パケットをネットワーク名前空間間で転送できるという、もう 1 つの強力な機能があります。つまり、図 8 の Envoy とプロセス B が独自のネットワーク NS 内にある場合、ソケット レベルのアクセラレーションを最大限に活用して、高性能な通信を実現することもできます。この点を強調するために、プロセス B が属するネットワーク ns を図 8 の右側の別の灰色のボックスに描画しました。

高性能 + クロスネットワーク ns、なんと美しい組み合わせでしょう。 Cilum はこれをベースにサイドカーレスのサービス メッシュを構築しました。図 9 に示すように、図のレイヤー 7 プロキシは各ポッドから独立しており、このワーク ノード上のすべてのポッドによって共有されることに注意してください。

図9: ソケットレベルのリダイレクトに基づくサイドカーレスサービスメッシュ

<<:  データの課題に対応して、2023年分散ストレージサミットフォーラムが成功裏に開催されました

>>:  Kubernetesガバナンス戦略を確立する方法

推薦する

shockhosting: 大容量の VPS ハードディスクを必要とするユーザーに適しています。$4.99、KVM/1g メモリ/150g ハードディスク/1T トラフィック

shockhosting は新たな動きを見せています。以前の「s」の何とも言えない「時代」型の VP...

Kafka の原則: 図解された Kafka アーキテクチャの原則

多くの学生は、これまでに Kafka の原則に関連する多くの記事を読んだことがあると思いますが、それ...

2345.comの親会社は検察に訴えられ、マイクロソフトに3600万元の支払いを命じられた

「大根園」、もっと大きな「トマト園」?瑞創はマイクロソフトに3600万ドルの賠償金を支払い、検察に起...

123systems-2g メモリ/35g ハードディスク/3T トラフィック/年間 30 ドル/G ポート

123systems が逃げるかどうかは議論する必要はない。逃げるつもりなら、3 年前に逃げるべきだ...

Baidu サイトリンクは肉に脂肪を加え、SEO の狼が本当にやってくる

11月初旬、Google Sitelinkにヒントを得たBaidu Sitelinkが正式にリリース...

この無料 CDN で WordPress サイトの読み込みが遅い問題を解決しましょう

無料のものを嫌いな人がいるでしょうか?自分のウェブサイトをもっと速くしたいと思わない人はいないでしょ...

hostsailor - ルーマニア VPS、2.99 ドルから、KVM/256M RAM/1Gbps 帯域幅

Hostsailor はドバイの富裕層が所有する会社です (登録番号: A224/03/14/815...

ドメインのPR値とGoogleキーワードランキングの関係

中国の検索エンジン市場における Google のシェアは Baidu に大きく遅れをとっているものの...

Google、プライベート検索結果を非表示にするオプションの提供を開始

ご存知のとおり、Google は世界で最も人気のある検索エンジンとして、テクノロジーの面で常に競合他...

NetEase Yanxuan のスクリーンを席巻する広告からブランドマーケティングの 5 つの新しいトレンドを洞察

2018年、社会、経済、文化などの環境が複雑に変化する中、ブランドマーケティングも変化を遂げ、軽薄さ...

zxplay-4.5 ポンド/KVM/2g メモリ/80g SSD/2T トラフィック/ドイツ

zxplay は、ドイツのデータ センターに設置され、1000M ポートと無制限のトラフィックを備え...

素人の目から見て、オンライン採用は投資する価値があるのでしょうか?

卒業シーズンが近づいており、当社は最近採用活動に忙しくしています。当社はさまざまな方法で採用活動を行...

SEOer の最終列車に追いついたかな?

初めて SEO を学んだとき、とても退屈で、理解できないこともいくつかありました。なぜなら、私はそれ...