随着 eBPF 的倒退,咱们曾经能够将 eBPF/XDP 程序间接部署在一般服务器上来实现负载平衡,从而节俭掉用于专门部署 LVS 的机器。
前文 分享了如何应用 xdp/ebpf 替换 lvs 来实现 slb,采纳的是 slb 独立机器部署模式,并且采纳 bpftool 和硬编码配置的模式来进行加载 xdp 程序,这是 版本 0.1。
版本 0.2 在 0.1 根底上,批改为基于 bpf skeleton 的程序化加载模式,要想简略地体验下这种工作流而不改变 版本 0.1 中整体部署模式的,能够去看看 https://github.com/MageekChiu/xdp4slb/tree/dev-0.2。
版本 0.3 在 0.2 根底上,反对以配置文件和命令行参数的模式动静加载 slb 配置
本文属于 版本 0.4,反对 slb 和 application 混布的模式,去除了专用的 slb 机器。
混布模式使得一般机器也能间接做负载平衡,同时不影响利用(off load 模式下能够体现),有老本效益;另外,在路由到本地的场景下,缩小了路由跳数,整体性能更好。
创立网络环境
# 不同发行版命令不一样
systemctl start docker
docker network create south --subnet 172.19.0.0/16 --gateway 172.19.0.1
# check
docker network inspect south
# or
ip link
# 先用 ifconfig 取得刚创立的 network 应的 bridge
# 后续则能够在宿主机上抓取这个 network 的所有 IP 包
tcpdump -i br-3512959a6150 ip
# 也能够取得某个容器的 veth , 抓取这个容器进出的所有包
tcpdump -i vethf01d241 ip
原理解析
SLB 集群路由
slb 为了高可用,个别都会集群化部署,那么申请怎么路由到每一台 slb 上呢?个别由(动静)路由协定(ospf bgp)实现 ecmp,使得各个 slb 实例从 router/switch 那里平均地取得流量。
因为配置动静路由协定十分繁冗,不在本文思考范畴之内,这里采纳一个简略的脚本来模仿 ecmp。
#!/bin/bash
dst="172.19.0.10"
rs1="172.19.0.2"
rs2="172.19.0.3"
ip route del $dst/32
ip route add $dst/32 nexthop via $rs1 dev eth0 weight 1
while true; do
nexthop=$(ip route show $dst/32 | awk '{print $3}')
# nexthop=$(ip route show "$dst" | grep -oP "nexthop \K\S+")
echo "to ${dst} via ${nexthop} now!"
sleep 3
# the requirements for blank is crazy!
if ["$nexthop" = "$rs1"]; then
new_nexthop="$rs2"
else
new_nexthop="$rs1"
fi
ip route del $dst/32
ip route add $dst/32 nexthop via $new_nexthop dev eth0 weight 1
done
其实就是将达到 vip 的下一跳在几个 host(混布有 slb 和 app 的机器,以下简称 mix)之间重复批改即可。
NAT 模式
版本 0.1~0.3 都采纳了 full nat 模式,在以后混布的模式下不再适合,可能会导致数据包无穷循环。因为不对数据包做一些标记的话,xdp 程序无奈辨别是来自 client 的数据包还是来自另一个 slb 的包。咱们采纳 DR 模式,除了能防止循环问题以外,性能也更好,因为
- 回包少了一跳
- 包的批改少了,也不必从新计算 ip、tcp 校验和等
架构图如下,这里做了简化,client 和 mix 之间实际上是有 router/switch 的,然而咱们采纳下面的模仿脚本把 router/switch 路由性能也间接放 client 外面了。
深蓝色示意申请,浅蓝色示意响应。
vip 采纳了 ecmp,一次申请只会路由到一个 mix 上,mis 上的 slb 可能会把这个转发到本地 app(本文以 Nginx 为例)或其它 mix,然而响应肯定是从 mix 间接回去的,而不会再次通过其它 mix。
负载平衡算法
目前反对以下几种算法
- random
- round_roubin
- hash
本文不在 slb 集群中同步会话状态,所以只能抉择 hash 算法,也就是不管申请路由到哪个 slb,都会被转发到同一个 backend app。
SLB 路由伪代码
if (dest_ip = local_ip){
// 间接交给本机协定栈
return
}
if (dest_ip = vip && dest_port = vport){
应用负载平衡算法筛选一个 rs
若 RS 就是本机,则 间接交给本机协定栈 并 return
否则,将 rs 的 mac 作为新包的 dst
此外保留 client 和 rs 的双向映射关系
便于后续 路由间接 应用
将本机 mac 作为新包的 src
将新包扔出去从新路由
}else{报错,丢包}
配置 SLB 和利用
Mix 的 Dockerfile 如下
FROM debian:bookworm
RUN apt-get update -y && apt-get upgrade -y \
&& apt install -y nginx procps bpftool iproute2 net-tools telnet kmod curl tcpdump
WORKDIR /tmp/
COPY src/slb /tmp/
COPY slb.conf /tmp/
这里批改镜像是因为我的宿主机 fedora:37 的 libc 版本是 2.36,而 debian:bullseye 对应的版本是 2.31,不能间接运行宿主机编译的可执行文件。
构建镜像并运行 app (这里是 nginx)
docker build -t mageek/mix:0.1 .
# in case you want to run a brand new container
docker rm mix1 mix2 -f
docker run -itd --name mix1 --hostname mix1 --privileged=true \
--net south -p 8888:80 --ip 172.19.0.2 --mac-address="02:42:ac:13:00:02" \
-v "$(pwd)"/rs1.html:/var/www/html/index.html:ro mageek/mix:0.1 nginx -g "daemon off;"
docker run -itd --name mix2 --hostname mix2 --privileged=true \
--net south -p 9999:80 --ip 172.19.0.3 --mac-address="02:42:ac:13:00:03" \
-v "$(pwd)"/rs2.html:/var/www/html/index.html:ro mageek/mix:0.1 nginx -g "daemon off;"
# check on host
docker ps
curl 127.0.0.1:8888
curl 127.0.0.1:9999
别离进入容器,配置 VIP,因为咱们曾经有模仿的路由协定了,所以在 mix 中配置好 vip 当前,要敞开 arp,防止影响 client 的包的路由
docker exec -it mix1 bash
docker exec -it mix2 bash
ifconfig lo:0 172.19.0.10/32 up
echo "1">/proc/sys/net/ipv4/conf/all/arp_ignore
echo "1">/proc/sys/net/ipv4/conf/lo/arp_ignore
echo "2">/proc/sys/net/ipv4/conf/all/arp_announce
echo "2">/proc/sys/net/ipv4/conf/lo/arp_announce
而后运行 slb
# 启动 slb 并指定网卡和配置文件
./slb -i eth0 -c ./slb.conf
# in another terminal
bpftool prog list
# bpftool prog show name xdp_lb --pretty
# check global variables
# bpftool map list
# bpftool map dump name slb_bpf.rodata
# check attaching with
ip link
日志间接在宿主机(整机一份)上看即可,不要开好几个终端(会导致日志不残缺)
bpftool prog tracelog
此外,测试阶段能够间接在宿主机编译实现可执行文件后,拷贝到容器里 (当然,前提是你曾经创立好了这些容器和相干的网络)
docker start mix1 mix2 client
docker cp src/slb mix1:/tmp/ && \
docker cp slb.conf mix1:/tmp/ && \
docker cp src/slb mix2:/tmp/ && \
docker cp slb.conf mix2:/tmp/ && \
docker cp routing.sh client:/tmp/
测试
新起一个 client 容器
docker run -itd --name client --hostname client --privileged=true \
--net south -p 10000:80 --ip 172.19.0.9 --mac-address="02:42:ac:13:00:09" \
-v "$(pwd)"/routing.sh:/tmp/routing.sh mageek/mix:0.1 nginx -g "daemon off;"
进入 client 配置并运行以下路由脚本脚本
docker exec -it client bash
sh routing.sh
另开一个 client terminal 进行申请测试
docker exec -it client bash
# visit rs first
curl 172.19.0.2:80
curl 172.19.0.3:80
# visit slb
curl 172.19.0.10:80
rs-1
curl 172.19.0.10:80
rs-2
咱们能够在 client 外面压测一把,然而要留神压测时不要运行 routing.sh,因为并发场景下存在 “老路由刚被删,新路由尚未建设的两头态” 的问题,导致申请失败。
apt-get install apache2-utils
# 并发 50,总申请数 5000
ab -c 50 -n 5000 http://172.19.0.10:80/
压测后果如下,可见都胜利了
erver Software: nginx/1.22.1
Server Hostname: 172.19.0.10
Server Port: 80
Document Path: /
Document Length: 5 bytes
Concurrency Level: 50
Time taken for tests: 3.141 seconds
Complete requests: 5000
Failed requests: 0
Total transferred: 1170000 bytes
HTML transferred: 25000 bytes
Requests per second: 1591.81 [#/sec] (mean)
Time per request: 31.411 [ms] (mean)
Time per request: 0.628 [ms] (mean, across all concurrent requests)
Transfer rate: 363.75 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 15 3.9 15 31
Processing: 5 16 4.7 16 48
Waiting: 0 11 4.4 10 34
Total: 17 31 3.6 30 60
Percentage of the requests served within a certain time (ms)
50% 30
66% 32
75% 32
80% 33
90% 35
95% 37
98% 40
99% 47
100% 60 (longest request)
还能够增大并发数测试,并发最大理论值是咱们存储 conntrack 条目标 back_map 数量最大值。超过这个并发数,会导致从新路由映射,可能导致 tcp reset。
下文预报
要想打造一个残缺的 slb,还有许多工作要做,比方利用内核能力进行 mac 主动寻址、许多边界查看等。这都是前面要做的工作,欢送大家一起参加 https://github.com/MageekChiu/xdp4slb/。