一 前言
好久沒寫文章了,最近忙于抉擇,搞的心好累,左右不知道哪條路對自己是最好的,風險與收益并存,是穩扎穩打還是冒一次險,換來的后面的順暢,我不知道怎么選,左右想法一直在打架。
言歸正傳,ebpf學了一段時間了,開始覺得自己還是了解一些,但是其實差距還有點大,這篇文章是學習ebpf的課程的一篇試驗文章,主要是基于ebpf的網絡程序,難度比以前學的大,加之新學,只能從模仿試驗開始了,試驗來源于極客時間中倪鵬飛老師的《ebpf核心技術與實戰》.
二 環境準備
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 還提供了自己的包管理工具 apk,可以通過 https://pkgs.alpinelinux.org/packages 網站上查詢包信息,也可以直接通過 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 ,經過了兩次協議棧的處理,其實完全沒必要,可以像圖中紫色箭頭的流程一樣繞過協議棧,從而提升下同一個宿主機器的容器間的網路轉發性能問題。
3.2 程序原理說明
按照我的理解,簡單來說,首先我們對新建立的套接字保存到一個叫BPF_MAP_TYPE_SOCKHASH 類型的映射表中,如下圖所示,key是五元組,value是套接字的文件描述符。
key定義如下:
struct sock_key
{
__u32 sip; //源IP
__u32 dip; //目的IP
__u32 sport; //源端口
__u32 dport; //目的端口
__u32 family; //協議
};
有了這個數據之后,新來的發送數據,我們把五元組的信息調個個,即源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類型的程序,可以使用的幫助函數是不一樣的,為了方便操作,這里面使用了兩種不同的ebpf程序類型:
- BPF_PROG_TYPE_SOCK_OPS 此類型是為了構建五元組和套接字的映射的ebfp程序類型。(socket operations 事件觸發執行)
- BPF_PROG_TYPE_SK_MSG 此類型為了捕獲套接字中發送的數據包,并根據上述套接字映射轉發出去。(sendmsg 系統調用觸發執行)
不同類型的ebpf程序hook點說明:
四 代碼匯總
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__ */
創建套接字和socket映射的程序,文件名為: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 socket數據的轉發
利用保存好的socket映射數據,結合bpf helper 函數實現報文的轉發。
文件名: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
通過兩行命令將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中的所有程序都生效,真是個神奇的玩意。
兩步:
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
和上面的掛載命令還是有不少的差別,包括bpf的類型不同一個為sockops 類型一個是sk_msg 類型;兩個程序還進行了通信,通過sock_ops_map進行通信,sock_ops_map是通過路徑映射進行綁定的。
五 運行優化負載均衡器性能對比
5.1 沒優化前
為了驗證是否有提升,有必要在原來沒做任何修改的負載均衡架構下測試下性能情況:
在client端下載測試工具和測試:
# 進入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.81ms,平均每秒請求數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.81ms降到了29.21ms,每秒平均請求數量從3407提升到4010,提升了17%,還是可以的。
curl范圍也是正常的:
/ # curl "http://172.17.0.5"
Hostname: http1
/ # curl "http://172.17.0.5"
Hostname: http2
在執行測試過程中,我們可以查看map中的值:
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
忽略No space left on device,這是ebpf版本問題,key的值即對應五元組的值,測試結束也看不到了。
六數據清理
# 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