利用ebpf优化负载均衡器

一 前言

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

言归正传,ebpf学了一段时间了,开始觉得自己还是了解一些,但是其实差距还有点大,这篇文章是学习ebpf的课程的一篇试验文章,主要是基于ebpf的网络程序,难度比以前学的大,加之新学,只能从模仿试验开始了,试验来源于极客时间中倪鹏飞老师的《ebpf核心技术与实战》.

二 环境准备

2.1 安装测试环境

部署整个网络架构图如下:450432a48435bf14be257236548511a1.png

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
容器间包的发送

如上图所示那样,正常情况下,负载均衡器会把报文发送到套接字所关联的队列中,经过协议栈,再通过虚拟网卡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
映射示意图

有了这个数据之后,新来的发送数据,我们把五元组的信息调个个,即源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点说明:f1e70a384b8945cc6ed8f168a00fcc09.png

四 代码汇总

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
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值