I.はじめに
久しぶりに記事を書きました.最近決断に追われてとても疲れています.どの方法が自分にとって最善なのかわかりません.リスクとメリットが共存しています.安全にプレイできますか.選択する方法を知っていると、右派と左派の考えが戦ってきました。
身近なところで、私はebpfをしばらく学んでいて、まだ少し知っているように感じますが、ギャップはまだ少し大きいです. この記事はebpfコースを学ぶための実験的な記事です. 主にebpfに基づいています.ネットワークプログラムは、以前よりも難しくなり、新しい学習と相まって、模倣実験からのみ開始できます.実験は、Ni Pengfei氏がGeek Timeに書いた「ebpfコアテクノロジーと実際の戦闘」から来ています.
第 2 環境の準備
2.1 テスト環境のインストール
次のように、ネットワーク アーキテクチャ図全体を展開します。
Docker 環境のインストール スクリプト:
# Webserver (响应是hostname,如 http1 或 http2)
docker run -itd --name=http1 --hostname=http1 feisky/webserver
docker run -itd --name=http2 --hostname=http2 feisky/webserver
# Client
docker run -itd --name=client alpine
# Nginx
docker run -itd --name=nginx nginx
説明:
docker alpine とは何ですか? Alpine オペレーティング システムは、セキュリティ指向の軽量 Linux ディストリビューションです。通常の Linux ディストリビューションとは異なり、Alpine は Musl libc と busybox を使用してシステム サイズ (5M サイズ) と実行時のリソース消費を削減していますが、その機能は busybox よりもはるかに充実しているため、オープンな支持を得ています。ソースコミュニティ。Alpine はスリムな状態を保ちながら、https://pkgs.alpinelinux.org/packages Web サイトからパッケージ情報をクエリしたり、apk コマンドを使用してさまざまなソフトウェアを直接クエリしてインストールしたりできる独自のパッケージ管理ツール apk も提供しています。
Docker コンテナーの IP アドレス情報を確認します。
root@ubuntu-lab:/home/miao# IP1=$(docker inspect http1 -f '{
{range .NetworkSettings.Networks}}{
{.IPAddress}}{
{end}}')
root@ubuntu-lab:/home/miao# IP2=$(docker inspect http2 -f '{
{range .NetworkSettings.Networks}}{
{.IPAddress}}{
{end}}')
root@ubuntu-lab:/home/miao# echo $IP1
172.17.0.2
root@ubuntu-lab:/home/miao# echo $IP2
172.17.0.3
root@ubuntu-lab:/home/miao# IP3=$(docker inspect nginx -f '{
{range .NetworkSettings.Networks}}{
{.IPAddress}}{
{end}}')
root@ubuntu-lab:/home/miao# echo $IP3
172.17.0.5
root@ubuntu-lab:/home/miao
2.2 nginx 構成の更新
# 生成nginx.conf文件
cat>nginx.conf <<EOF
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
upstream webservers {
server $IP1;
server $IP2;
}
server {
listen 80;
location / {
proxy_pass http://webservers;
}
}
}
EOF
構成の更新:
# 更新Nginx配置
docker cp nginx.conf nginx:/etc/nginx/nginx.conf
docker exec nginx nginx -s reload
三原則
3.1 コンテナ間のネットワーク送信
上の図に示すように、通常の状況では、ロード バランサーはソケットに関連付けられたキューにメッセージを送信し、プロトコル スタックを通過してから仮想ネットワーク カード 1 を通過し、それを仮想ネットワーク カード 2 に転送してから、再度プロトコルスタックを通過処理、ヘッダー情報を削除、データパケットはソケット2に送信、2回のプロトコルスタック処理後、実は全く不要、紫の処理のようにプロトコルスタックをバイパスできる図の矢印で、同じホスト マシンをアップグレードします。コンテナ間のネットワーク フォワーディング パフォーマンスの問題。
3.2 プログラムの原則の説明
私の理解によると、簡単に言えば、最初に新しく作成したソケットを BPF_MAP_TYPE_SOCKHASH タイプと呼ばれるマッピング テーブルに保存します。下の図に示すように、キーは 5 倍、値はソケットのファイル記述子です。キーは次のように定義されます。
struct sock_key
{
__u32 sip; //源IP
__u32 dip; //目的IP
__u32 sport; //源端口
__u32 dport; //目的端口
__u32 family; //协议
};
このデータを取得した後、新しく送信されたデータの 5 タプル情報を転送します。つまり、送信元 IP と宛先 IP が交換され、送信元ポートと宛先ポートが交換されるため、反対側が取得され、関数 bpf_msg_redirect_hash を介して完了します。簡単に言えば、現在のソケットのメッセージは、魔法のようにプロトコル スタックをバイパスするソケット マッピングのソケットに転送されます。
long bpf_msg_redirect_hash(struct sk_msg_buff *msg, struct bpf_map *map, void *key, u64 flags)
Description
This helper is used in programs implementing policies at the socket level. If the
message msg is allowed to pass (i.e. if the verdict eBPF program returns SK_PASS),
redirect it to the socket referenced by map (of type BPF_MAP_TYPE_SOCKHASH) using
hash key. Both ingress and egress interfaces can be used for redirection. The
BPF_F_INGRESS value in flags is used to make the distinction (ingress path is se‐
lected if the flag is present, egress path otherwise). This is the only flag sup‐
ported for now.
Return SK_PASS on success, or SK_DROP on error.
3.3 ebpf タイプを使用する
異なる ebpf タイプのプログラムには、使用できる異なるヘルパー関数があります. 操作の便宜上、ここでは 2 つの異なるタイプの ebpf プログラムが使用されています:
BPF_PROG_TYPE_SOCK_OPS このタイプは、クインタプルとソケットのマッピングを構築するための ebfp プログラム タイプです。(ソケット操作イベントが実行をトリガーします)
BPF_PROG_TYPE_SK_MSG このタイプは、ソケットで送信されたデータ パケットをキャプチャし、上記のソケット マッピングに従ってそれらを転送するために使用されます。(sendmsg システムコールが実行のトリガー)
さまざまなタイプの ebpf プログラム フック ポイントの説明:
4 つのコードのまとめ
4.1 ソケットマッピングデータの保存
ヘッダー ファイルは sockops.h を定義します
#ifndef __SOCK_OPS_H__
#define __SOCK_OPS_H__
#include <linux/bpf.h>
struct sock_key {
__u32 sip;
__u32 dip;
__u32 sport;
__u32 dport;
__u32 family;
};
struct bpf_map_def SEC("maps") sock_ops_map = {
.type = BPF_MAP_TYPE_SOCKHASH,
.key_size = sizeof(struct sock_key),
.value_size = sizeof(int),
.max_entries = 65535,
.map_flags = 0,
};
#endif /* __SOCK_OPS_H__ */
ソケットとソケット マッピングを作成するためのプログラム。ファイル名は sockops.bpf.c です。
#include <linux/bpf.h>
#include <bpf/bpf_endian.h>
#include <bpf/bpf_helpers.h>
#include <sys/socket.h>
#include "sockops.h"
SEC("sockops")
int bpf_sockmap(struct bpf_sock_ops *skops)
{
/* 包如果不是ipv4的则忽略*/
if (skops->family != AF_INET) {
return BPF_OK;
}
/* 只有新创建的主动连接或被动连接才更新 */
if (skops->op != BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB
&& skops->op != BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB) {
return BPF_OK;
}
struct sock_key key = {
.dip = skops->remote_ip4,
.sip = skops->local_ip4,
/* convert to network byte order */
.sport = bpf_htonl(skops->local_port),
.dport = skops->remote_port,
.family = skops->family,
};
bpf_sock_hash_update(skops, &sock_ops_map, &key, BPF_NOEXIST);
return BPF_OK;
}
char LICENSE[] SEC("license") = "Dual BSD/GPL";
キーは次のとおりです。
bpf_sock_hash_update(skops, &sock_ops_map, &key, BPF_NOEXIST);
4.2 ソケットデータの転送
保存されたソケット マッピング データを使用し、bpf ヘルパー関数を組み合わせてメッセージを転送します。ファイル名: sockredir.bpf.c
#include <linux/bpf.h>
#include <bpf/bpf_endian.h>
#include <bpf/bpf_helpers.h>
#include <sys/socket.h>
#include "sockops.h"
SEC("sk_msg")
int bpf_redir(struct sk_msg_md *msg)
{
// 源和目标要反转,因为我们先对端发的
struct sock_key key = {
.sip = msg->remote_ip4,
.dip = msg->local_ip4,
.dport = bpf_htonl(msg->local_port),
.sport = msg->remote_port,
.family = msg->family,
};
// 将套接字收到的消息转发
bpf_msg_redirect_hash(msg, &sock_ops_map, &key, BPF_F_INGRESS);
return SK_PASS;
}
char LICENSE[] SEC("license") = "Dual BSD/GPL";
コンパイル コマンド:
clang -g -O2 -target bpf -D__TARGET_ARCH_x86 -I/usr/include/x86_64-linux-gnu -I. -c sockops.bpf.c -o sockops.bpf.o
clang -g -O2 -target bpf -D__TARGET_ARCH_x86 -I/usr/include/x86_64-linux-gnu -I. -c sockredir.bpf.c -o sockredir.bpf.o
2 行のコマンドで bpf プログラムを bpf バイトコードに変換します。
4.3 ebpf プログラムのロード
以前は、BCC の python コードまたは libbpf ライブラリによって提供されていた機能、今回は bpftool を使用して ebpf プログラムをロードおよびマウントするというエキサイティングな方法で、ebpf プログラムを長時間実行する方法がようやくわかりました。フロントエンドで実行すると、プログラムが停止するとドロップしますが、そうではありません。
sockops プログラムをロードします。
sudo bpftool prog load sockops.bpf.o /sys/fs/bpf/sockops type sockops pinmaps /sys/fs/bpf
sockops.bpf.o をカーネルにロードし、BPF ファイル システムに修正します。コマンドの終了後、ebpf プログラムはバックグラウンドで実行され続けます。見られます:
root@ubuntu-lab:/home/miao/jike-ebpf/balance# bpftool prog show
992: sock_ops name bpf_sockmap tag e37ef726a3a85a2e gpl
loaded_at 2022-06-12T10:43:09+0000 uid 0
xlated 256B jited 140B memlock 4096B map_ids 126
btf_id 149
上記は ebpf プログラムのみをロードしますが、カーネル イベントにバインドされていません. sockops プログラムは cgroup サブシステムにマウントできるため、cgroup で実行されているすべてのプログラムに対して有効になります. これは本当に魔法のようなものです. 2 つの手順: 1. 現在のシステムのマウントされた cgroup パスを表示します。
root@ubuntu-lab:/home/miao/jike-ebpf/balance# mount | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)
マウント:
sudo bpftool cgroup attach /sys/fs/cgroup/ sock_ops pinned /sys/fs/bpf/sockops
フォワーダーのロードとマウント:
sudo bpftool prog load sockredir.bpf.o /sys/fs/bpf/sockredir type sk_msg map name sock_ops_map pinned /sys/fs/bpf/sock_ops_map
sudo bpftool prog attach pinned /sys/fs/bpf/sockredir msg_verdict pinned /sys/fs/bpf/sock_ops_map
1 つは sockops タイプで、もう 1 つは sk_msg タイプという異なるタイプの bpf を含め、上記のマウント コマンドにはまだ多くの違いがあります。2 つのプログラムは、パス マッピングによってバインドされる sock_ops_map を介して通信します。
5 回実行の最適化されたロード バランサーのパフォーマンス比較
5.1 最適化前
改善があるかどうかを確認するには、変更を加えずに元の負荷分散アーキテクチャでパフォーマンスをテストする必要があります。テスト ツールをダウンロードして、クライアント側でテストします。
# 进入client容器终端,安装curl之后访问Nginx
docker exec -it client sh
# 安装和验证
/ # apk add curl wrk --update
/ # curl "http://172.17.0.5"
正常と判断された場合は、性能テストツールwrkをインストールし、以下のようにテストしてください。
/ # apk add wrk --update
/ # wrk -c100 "http://172.17.0.5"
出力は次のとおりです。
/ # wrk -c100 "http://172.17.0.5"
Running 10s test @ http://172.17.0.5
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 32.81ms 28.30ms 252.86ms 87.21%
Req/Sec 1.75k 612.19 3.26k 67.35%
34406 requests in 10.10s, 5.41MB read
Requests/sec: 3407.42
Transfer/sec: 549.05KB
平均遅延は 32.81 ミリ秒、1 秒あたりの平均リクエスト数は 3407.42、平均リクエスト サイズは 1.75 です。
5.2 最適化後
docker exec -it client sh
/# wrk -c100 "http://172.17.0.5"
結果は次のとおりです。
Running 10s test @ http://172.17.0.5
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 29.21ms 27.98ms 294.16ms 89.78%
Req/Sec 2.06k 626.54 3.25k 68.23%
40389 requests in 10.07s, 6.36MB read
Requests/sec: 4010.77
Transfer/sec: 646.27KB
比較すると、遅延は 32.81 ミリ秒から 29.21 ミリ秒に減少し、1 秒あたりの平均リクエスト数は 3407 から 4010 に増加し、17% 増加しましたが、これはまだ許容範囲内です。
curl スコープも正常です。
/ # curl "http://172.17.0.5"
Hostname: http1
/ # curl "http://172.17.0.5"
Hostname: http2
テストの実行中に、マップに値を表示できます。
root@ubuntu-lab:/home/miao/jike-ebpf/hello# sudo bpftool map dump name sock_ops_map
key:
ac 11 00 05 ac 11 00 03 00 00 c7 60 00 00 00 50
02 00 00 00
value:
No space left on device
key:
ac 11 00 05 ac 11 00 04 00 00 00 50 00 00 e0 86
02 00 00 00
value:
No space left on device
key:
ac 11 00 05 ac 11 00 04 00 00 00 50 00 00 e0 88
02 00 00 00
無視 デバイスにスペースが残っていません。これは ebpf バージョンの問題です。キーの値は 5 倍の値に対応しており、テスト後には表示されません。
6つのデータクリーニング
# cleanup skops prog and sock_ops_map
sudo bpftool cgroup detach /sys/fs/cgroup/ sock_ops name bpf_sockmap
sudo rm -f /sys/fs/bpf/sockops /sys/fs/bpf/sock_ops_map
# cleanup sk_msg prog
sudo bpftool prog detach pinned /sys/fs/bpf/sockredir msg_verdict pinned /sys/fs/bpf/sock_ops_map
sudo rm -f /sys/fs/bpf/sockredir
元のマウント ポイントをアンインストールし、いくつかのファイルを削除して ebpf プログラムを削除します。
Docker コンテナーを削除します。
docker rm -f http1 http2 client nginx