利用ebpf优化负载均衡器

一 前言

好久没写文章了,最近忙于抉择,搞的心好累,左右不知道哪条路对自己是最好的,风险与收益并存,是稳扎稳打还是冒一次险,换来的后面的顺畅,我不知道怎么选,左右想法一直在打架。

言归正传,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程序类型:

  1. BPF_PROG_TYPE_SOCK_OPS 此类型是为了构建五元组和套接字的映射的ebfp程序类型。(socket operations 事件触发执行)
  2. BPF_PROG_TYPE_SK_MSG 此类型为了捕获套接字中发送的数据包,并根据上述套接字映射转发出去。(sendmsg 系统调用触发执行)

不同类型的ebpf程序hook点说明:


不同类型的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)
  1. 挂载:

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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,490评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,581评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,830评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,957评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,974评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,754评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,464评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,847评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,995评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,137评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,819评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,482评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,023评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,149评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,409评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,086评论 2 355

推荐阅读更多精彩内容