Merbridge - eBPFでメッシュを高速化
iptablesルールをeBPFに置き換えることで、データをインバウンドソケットからアウトバウンドソケットに直接転送し、サイドカーとサービス間のデータパスを短縮できます。
Istioのトラフィック管理、セキュリティ、可観測性、ポリシーにおける能力の秘密は、すべてEnvoyプロキシにあります。IstioはEnvoyを「サイドカー」として使用してサービストラフィックをインターセプトし、カーネルのnetfilter
パケットフィルター機能はiptablesによって設定されます。
iptablesを使用してこのインターセプトを実行するには欠点があります。netfilterはパケットをフィルタリングするための非常に汎用性の高いツールであるため、宛先ソケットに到達する前に、いくつかのルーティングルールとデータフィルタリングプロセスが適用されます。たとえば、ネットワーク層からトランスポート層まで、netfilterはpre_routing
、post_routing
などの定義済みルールで複数回処理に使用されます。パケットがTCPパケットまたはUDPパケットになり、ユーザースペースに転送されると、パケット検証、プロトコルポリシー処理、宛先ソケット検索など、いくつかの追加手順が実行されます。サイドカーがトラフィックをインターセプトするように設定されている場合、重複した手順が複数回実行されるため、元のデータパスが非常に長くなる可能性があります。
過去2年間で、eBPFはトレンドのテクノロジーとなり、eBPFに基づく多くのプロジェクトがコミュニティにリリースされました。CiliumやPixieなどのツールは、可観測性とネットワークパケット処理におけるeBPFの優れたユースケースを示しています。eBPFのsockops
およびredir
機能を使用すると、データパケットをインバウンドソケットからアウトバウンドソケットに直接転送することで、効率的に処理できます。Istioメッシュでは、eBPFを使用してiptablesルールを置き換え、データパスを短縮することでデータプレーンを高速化できます。
Merbridgeというオープンソースプロジェクトを作成しました。Istioで管理されているクラスターに次のコマンドを適用することで、eBPFを使用してこのようなネットワークの高速化を実現できます。
$ kubectl apply -f https://raw.githubusercontent.com/merbridge/merbridge/main/deploy/all-in-one.yaml
Merbridgeを使用すると、パケットデータパスをあるソケットから別の宛先ソケットに直接短縮できます。仕組みは次のとおりです。
eBPF sockops
を使用したパフォーマンスの最適化
ネットワーク接続は本質的にソケット通信です。eBPFは、アプリケーションがインバウンドソケットで送信したパケットをアウトバウンドソケットに直接転送する関数bpf_msg_redirect_hash
を提供します。前述の関数に入ると、開発者はパケットの宛先を決定するためのロジックを実行できます。この特性により、パケットのデータパスをカーネル内で顕著に最適化できます。
sock_map
は、パケット転送の情報を記録する上で重要な部分です。パケットが到着すると、sock_map
から既存のソケットが選択され、パケットが転送されます。そのため、転送プロセスが適切に機能するように、すべてのパケットのソケット情報を保存する必要があります。新しいソケットの作成など、新しいソケット操作がある場合、sock_ops
関数が実行されます。ソケットメタデータが取得され、パケットの処理時に使用されるsock_map
に保存されます。sock_map
の一般的なキータイプは、送信元と宛先のアドレスとポートの「4つ組」です。キーとマップに保存されているルールを使用すると、新しいパケットが到着したときに宛先ソケットが見つかります。
Merbridgeのアプローチ
実際のシナリオを使用して、Merbridgeの詳細な設計と実装の原則を段階的に紹介しましょう。
iptablesに基づくIstioサイドカーのトラフィックインターセプト
外部トラフィックがアプリケーションのポートに到達すると、iptablesのPREROUTING
ルールによってインターセプトされ、サイドカーコンテナのポート15006に転送され、Envoyに処理が渡されます。これは、上記の図の赤いパスの手順1〜4として示されています。
Envoyは、Istioコントロールプレーンによって発行されたポリシーを使用してトラフィックを処理します。許可されている場合、トラフィックはアプリケーションコンテナの実際のコンテナポートに送信されます。
アプリケーションが他のサービスにアクセスしようとすると、iptablesのOUTPUT
ルールによってインターセプトされ、Envoyがリッスンしているサイドカーコンテナのポート15001に転送されます。これは赤いパスの手順9〜12であり、インバウンドトラフィックの処理に似ています。
アプリケーションポートへのトラフィックはサイドカーに転送され、次にサイドカーポートからコンテナポートに送信される必要があります。これはオーバーヘッドです。さらに、iptablesの汎用性により、さまざまなフィルタリングルールが適用されるため、データパス全体に遅延が必然的に追加されるため、パフォーマンスが常に理想的とは限りません。iptablesはパケットフィルタリングを行うための一般的な方法ですが、Envoyプロキシの場合、データパスが長くなると、カーネルでのパケットフィルタリングプロセスのボトルネックが増幅されます。
sockops
を使用してサイドカーのソケットをアプリケーションのソケットに直接接続すると、トラフィックはiptablesルールを通過する必要がなくなり、パフォーマンスが向上します。
アウトバウンドトラフィックの処理
上記のように、eBPFのsockops
を使用してiptablesをバイパスし、ネットワークリクエストを高速化します。同時に、Istioのどの部分も変更したくないため、Merbridgeはコミュニティバージョンに完全に適応します。そのため、eBPFでiptablesの動作をシミュレートする必要があります。
iptablesでのトラフィックリダイレクトは、そのDNAT
機能を利用します。eBPFを使用してiptablesの機能をシミュレートしようとすると、主に2つのことを行う必要があります
- 接続が開始されたときに宛先アドレスを変更して、トラフィックを新しいインターフェイスに送信できるようにします。
- Envoyが元の宛先アドレスを識別して、トラフィックを識別できるようにします。
最初の部分については、eBPFのconnect
プログラムを使用して、user_ip
とuser_port
を変更することで処理できます。
2番目の部分については、カーネルのnetfilter
モジュールに属するORIGINAL_DST
の概念を理解する必要があります。
アプリケーション(Envoyを含む)が接続を受信すると、get_sockopt
関数を呼び出してORIGINAL_DST
を取得します。iptables DNAT
プロセスを経由する場合、iptablesはこのパラメータを「元のIP +ポート」値で現在のソケットに設定します。したがって、アプリケーションは接続に従って元の宛先アドレスを取得できます。
eBPFのget_sockopts
関数を使用して、この呼び出しプロセスを変更する必要があります。(このパラメータは現在SO_ORIGINAL_DST
のoptnameをサポートしていないため、bpf_setsockopt
はここでは使用されません)。
下の図を参照すると、アプリケーションがリクエストを開始すると、次の手順が実行されます。
- アプリケーションが接続を開始すると、
connect
プログラムは宛先アドレスを127.x.y.z:15001
に変更し、cookie_original_dst
を使用して元の宛先アドレスを保存します。 sockops
プログラムでは、現在のソケット情報と4つ組がsock_pair_map
に保存されます。同時に、同じ4つ組とそれに対応する元の宛先アドレスがpair_original_dest
に書き込まれます。(get_sockopt
プログラムでは取得できないため、Cookieはここでは使用されません)。- Envoyが接続を受信すると、
get_sockopt
関数を呼び出して、現在の接続の宛先アドレスを読み取ります。get_sockopt
は、4つ組の情報に基づいて、pair_original_dest
から元の宛先アドレスを抽出して返します。したがって、接続は完全に確立されます。 - データ転送ステップでは、
redir
プログラムは4つ組の情報に基づいてsock_pair_map
からソック情報をを読み取り、bpf_msg_redirect_hash
を介して直接転送してリクエストを高速化します。
宛先アドレスを127.0.0.1
ではなく127.x.y.z
に設定するのはなぜですか?異なるポッドが存在する場合、競合する4つ組が存在する可能性があり、これは競合を適切に回避します。(ポッドのIPは異なり、いつでも競合状態にはなりません。)
インバウンドトラフィックの処理
インバウンドトラフィックの処理は基本的にアウトバウンドトラフィックと似ていますが、唯一の違いは、宛先のポートを15006に修正することです。
eBPFはiptablesのように特定の名前空間で有効にすることができないため、変更はグローバルになります。つまり、Istioによって元々管理されていないPod、または外部IPアドレスを使用すると、接続がまったく確立されないなど、深刻な問題が発生します。
そのため、ノード上のポッドを監視するkubeletと同様に、すべてのポッドを監視する小さなコントロールプレーン(DaemonSetとしてデプロイ)を設計し、サイドカーに挿入されたポッドIPアドレスをlocal_pod_ips
マップに書き込みます。
インバウンドトラフィックを処理するときに、宛先アドレスがマップにない場合、トラフィックに対して何も行いません。
それ以外の場合、手順はアウトバウンドトラフィックと同じです。
同一ノードの高速化
理論的には、同じノード上のEnvoyサイドカー間の高速化は、インバウンドトラフィック処理によって直接実現できます。ただし、このシナリオで現在のポッドのアプリケーションにアクセスすると、Envoyはエラーを発生させます。
Istioでは、Envoyは現在のポッドIPとポート番号を使用してアプリケーションにアクセスします。上記のシナリオでは、ポッドIPがlocal_pod_ips
マップにも存在し、インバウンドトラフィックの送信元と同じアドレスであるため、トラフィックがポート15006のポッドIPに再びリダイレクトされることがわかりました。同じインバウンドアドレスにリダイレクトすると、無限ループが発生します。
ここで疑問が生じます。eBPFで現在の名前空間のIPアドレスを取得する方法はありますか?答えはイエスです!
フィードバックメカニズムを設計しました。Envoyが接続を確立しようとすると、ポート15006にリダイレクトします。ただし、sockops
ステップでは、送信元IPと宛先IPが同じかどうかを判断します。同じである場合、それは間違ったリクエストが送信されたことを意味し、sockops
プロセスでこの接続を破棄します。その間、現在のProcessID
とIP
情報がprocess_ip
マップに書き込まれ、eBPFがプロセスとIP間の対応をサポートできるようにします。
次のリクエストが送信されると、同じプロセスを再度実行する必要はありません。宛先アドレスが現在のIPアドレスと同じかどうかをprocess_ip
マップから直接確認します。
接続関係
Merbridge を適用する前の Pod 間のデータパスは次のようになります。
Merbridge を適用した後、送信トラフィックは多くのフィルタステップをスキップし、パフォーマンスを向上させます。
2 つの Pod が同じマシン上にある場合、接続はさらに高速になります。
パフォーマンス結果
iptables の代わりに eBPF を使用した場合の全体的なレイテンシへの影響を見てみましょう(低いほど良い)。
eBPF を使用した後の全体的な QPS も確認できます(高いほど良い)。テスト結果は `wrk` を使用して生成されています。
まとめ
この記事では、Merbridge の中核となるアイデアを紹介しました。iptables を eBPF に置き換えることで、メッシュシナリオでのデータ転送プロセスを高速化できます。同時に、Istio はまったく変更されません。つまり、eBPF を使用したくない場合は、DaemonSet を削除するだけで、データパスは問題なく従来の iptables ベースのルーティングに戻ります。
Merbridge は完全に独立したオープンソースプロジェクトです。まだ初期段階にあり、より多くのユーザーと開発者の参加を期待しています。この新しいテクノロジーを試してメッシュを高速化し、フィードバックを提供していただければ幸いです。
関連項目
- GitHub 上の Merbridge
- Tencent の Liu Xu 氏による「Using eBPF instead of iptables to optimize the performance of service grid data plane」
- Tetrate の Jimmy Song 氏による「Sidecar injection and transparent traffic hijacking process in Istio explained in detail」
- Intel の Yizhou Xu 氏による「Accelerate the Istio data plane with eBPF」
- Envoy の Original Destination フィルター