一 前言
好久没写文章了,最近忙于抉择,搞的心好累,左右不知道哪条路对自己是最好的,风险与收益并存,是稳扎稳打还是冒一次险,换来的后面的顺畅,我不知道怎么选,左右想法一直在打架。
言归正传,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 容器间网络发包
![5e919c987fb000db09ce1570100973ed.png](https://i-blog.csdnimg.cn/blog_migrate/4eecf6e48490267c9b443b0d5dc5d727.png)
如上图所示那样,正常情况下,负载均衡器会把报文发送到套接字所关联的队列中,经过协议栈,再通过虚拟网卡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; //协议
};
![95e56c72b6ba71d4eeb395f71bdcbdde.png](https://i-blog.csdnimg.cn/blog_migrate/29b89467dc0697904841c47abe3682f7.png)
有了这个数据之后,新来的发送数据,我们把五元组的信息调个个,即源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