前文分析了 LVS 作為負載均衡的原理。隨著 eBPF 的發展,我們已經可以將 eBPF/XDP 程式直接部署在普通伺服器上來實現負載均衡,從而節省掉用於專門部署 LVS 的機器。
本文不打算直接到這一步,而是首先看看如何用 eBPF/XDP 按照常規模式來替代 LVS,也就是說我們還是將負載均衡程式(software load balance 簡稱 SLB)部署在專用機器上,只不過不用 LVS,而是用 eBPF/XDP 來實現。
實驗步驟
建立網路環境
# 不同發行版命令不一樣
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
# 當然,如果是 offload 的模式,則除錯確實不易,需要嗅探本地網路的資料包並抓取了
# 在容器網路裡,我們尚有宿主機這個上帝視角,在裸機網路裡,則可能得去捯飭路由器了
建立兩個RS
echo "rs-1" > rs1.html
echo "rs-2" > rs2.html
docker run -itd --name rs1 --hostname rs1 --privileged=true --net south -p 8888:80 --ip 172.19.0.2 --mac-address="02:42:ac:13:00:02" -v "$(pwd)"/rs1.html:/usr/share/nginx/html/index.html:ro nginx:stable
docker run -itd --name rs2 --hostname rs2 --privileged=true --net south -p 9999:80 --ip 172.19.0.3 --mac-address="02:42:ac:13:00:03" -v "$(pwd)"/rs2.html:/usr/share/nginx/html/index.html:ro nginx:stable
# check on host
curl 127.0.0.1:8888
curl 127.0.0.1:9999
另:
即使是 nginx 對於我們除錯負載均衡也不是足夠簡單,除錯階段可以用 nc 來進行除錯dnf install nc or apt install netcat
server side nc -l -vv -p 5000
client side nc 172.19.0.2 5000
實現SLB
為了不影響 RS,本文采用 NAT 模式的進一步:Full-NAT 模式實現 SLB。這種模式有缺陷:rs 不能獲得真實的 client ip,但是對部署環境要求相對較少(網路相通,無需設定預設閘道器)。
實現分析
原始碼都在 https://github.com/MageekChiu/xdp4slb。歡迎大家提出缺陷和建議!
核心框架如下:
if (dest_ip = vip && dest_port = vport){
ingress,包來源於 client,要轉發給 rs
挑選本地一個可用的 port1-ip1 作為新包的 src
使用負載均衡演算法挑選一個 rs,並將其 port2-ip2 作為新包的 dst
相應的修改 src mac 和 dst mac
此外儲存 client 的 port3-ip3 和 port1-ip1 的雙向對映關係
便於後續 ingress 和 egress 使用
}else{
egress,包來源於 rs, 要轉發給 client
根據包的 dst 找到 port1-ip1
根據 ingress 裡面的對映找到對應的 client 的 port3-ip3 作為新包的 dst
使用 vip 和 vport 作為新包的 src
相應的修改 src mac 和 dst mac
}
重新計算校驗和
使用 XDP_TX 將包從本網路卡重新扔回去
這裡面還有些校驗細節就不講了,大家可以直接看程式碼
本地測試
開發完成後,可以先在本地進行編譯和load,以提前暴露問題,沒問題後,在將目標檔案放到容器裡進行測試
# CORE, if you want to include vmlinux.h
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
# local compile and test
rm -f /sys/fs/bpf/slb \
&& rm -f slb.bpf.o \
&& clang -target bpf -g -O2 -c slb.bpf.c -o slb.bpf.o \
&& bpftool prog load slb.bpf.o /sys/fs/bpf/slb \
&& ll /sys/fs/bpf/slb \
# for testing, you can cp newly compiled object to container
docker cp slb.bpf.o slb:/tmp/
部署和配置SLB
Dockerfile 如下
FROM debian:bullseye
# modify source to get faster installation
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& apt-get update -y && apt-get upgrade -y \
&& apt install -y procps bpftool iproute2 net-tools telnet kmod curl tcpdump
WORKDIR /tmp/
COPY slb.bpf.o /tmp/
構建映象並執行
docker build -t mageek/slb:0.1 .
docker run -itd --name slb --hostname slb --privileged=true --net south --ip 172.19.0.5 --mac-address="02:42:ac:13:00:05" mageek/slb:0.1
進入容器載入 xdp 目標檔案
docker exec -it slb bash
# 在SLB中啟用VIP
# reuse mac addr from slb ip
# ifconfig eth0:0 172.19.0.10/32 up
# add new mac for vip
ifconfig eth0:0 172.19.0.10/32 hw ether 02:42:ac:13:00:10 up
# to delete
# ifconfig eth0:0 down
bpftool net detach xdpgeneric dev eth0
rm /sys/fs/bpf/slb
bpftool prog load slb.bpf.o /sys/fs/bpf/slb
# ls -l /sys/fs/bpf
bpftool prog list
# bpftool prog show name xdp_lb --pretty
# bpftool net attach xdpgeneric name xdp_lb dev eth0
# or
bpftool net attach xdpgeneric id 211 dev eth0
# check with
ip link
cat /sys/kernel/debug/tracing/trace_pipe
# better use code bellow
bpftool prog tracelog
# won't get any result, cause the packets haven't got there
tcpdump host 172.19.0.10
注意,雖然官方文件上說,attach xdp 會自己選擇合適的模式,但是我們在虛擬網路卡下面,只能選擇 attach xdpgeneric,前者不會生效,估計是個bug。
測試
新起一個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" nginx:stable
進入 client
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
curl 172.19.0.10:80
rs-1
curl 172.19.0.10:80
rs-2
可見確實實現了 round_robin 演算法。
slb 中 bpftool prog tracelog
的輸出
curl-10670 [001] d.s31 1315.377007: bpf_trace_printk: Got a packet
curl-10670 [001] d.s31 1315.377597: bpf_trace_printk: Not IPV4, pass
<...>-10671 [000] d.s31 1315.391432: bpf_trace_printk: Got a packet
curl-10671 [000] d.s31 1315.392076: bpf_trace_printk: Not IPV4, pass
<...>-10672 [000] d.s31 1320.770259: bpf_trace_printk: Got a packet
<...>-10672 [000] d.s31 1320.770375: bpf_trace_printk: Not IPV4, pass
<...>-10672 [000] d.s31 1320.770424: bpf_trace_printk: Got a packet
<...>-10672 [000] d.s31 1320.770428: bpf_trace_printk: Got a TCP packet of tuple
from 150999980|172.19.0.9:4276|46096 to 167777196|172.19.0.10:20480|80,
iph->daddr: 167777196|172.19.0.10, vip.ip_int: 167777196|172.19.0.10
<...>-10672 [000] d.s31 1320.770430: bpf_trace_printk: NAT IP 83891116
<...>-10672 [000] d.s31 1320.770431: bpf_trace_printk: NAT cur:1 ,t:30100 ,r:38005
<...>-10672 [000] d.s31 1320.770433: bpf_trace_printk: origin- 02:42:ac:13:00:09
<...>-10672 [000] d.s31 1320.770433: bpf_trace_printk: to----- 02:42:ac:13:00:10
<...>-10672 [000] d.s31 1320.770434: bpf_trace_printk: now---- 02:42:ac:13:00:05
<...>-10672 [000] d.s31 1320.770435: bpf_trace_printk: to----- 02:42:ac:13:00:02
<...>-10672 [000] d.s31 1320.770436: bpf_trace_printk: Ingress a nat packet of tuple
from 83891116|172.19.0.5:38005|30100 to 33559468|172.19.0.2:20480|80,
<...>-10672 [000] d.s31 1320.770437: bpf_trace_printk: ip_sum from 488 to 3560,tcp_sum from 26712 to 12691,action:3
<...>-10672 [000] d.s31 1320.770508: bpf_trace_printk: Got a packet
<...>-10672 [000] d.s31 1320.770882: bpf_trace_printk: Not IPV4, pass
<...>-10672 [000] d.s31 1320.771063: bpf_trace_printk: Got a packet
<...>-10672 [000] d.s31 1320.771067: bpf_trace_printk: Got a TCP packet of tuple
from 33559468|172.19.0.2:20480|80 to 83891116|172.19.0.5:38005|30100,
iph->daddr: 83891116|172.19.0.5, vip.ip_int: 167777196|172.19.0.10
<...>-10672 [000] d.s31 1320.771068: bpf_trace_printk: origin- 02:42:ac:13:00:02
<...>-10672 [000] d.s31 1320.771069: bpf_trace_printk: to----- 02:42:ac:13:00:10
<...>-10672 [000] d.s31 1320.771070: bpf_trace_printk: now---- 02:42:ac:13:00:10
<...>-10672 [000] d.s31 1320.771070: bpf_trace_printk: to----- 02:42:ac:13:00:09
<...>-10672 [000] d.s31 1320.771072: bpf_trace_printk: Egress a nat packet of tuple
from 167777196|172.19.0.10:20480|80 to 150999980|172.19.0.9:4276|46096,
<...>-10672 [000] d.s31 1320.771073: bpf_trace_printk: ip_sum from 36578 to 33506,tcp_sum from 23640 to 9274,action:3
限制
TCP的負載均衡是比較複雜的,還有各種條件需要考慮,比如:多例項 SLB 之間的狀態同步、conntrack 條目的回收、埠自動管理、arp動態處理等等。完整的實現是非常複雜和體系化的,本文作為一個簡單的實現,目的是體驗ebpf/xdp,生產級別的實現請自行完成(工作量較大)或參考社群已有版本(雖然不多)。
參考
- https://github.com/torvalds/linux/blob/master/net/netfilter/nf_nat_core.c#L504
- https://github.com/lizrice/lb-from-scratch/blob/main/README.MD
- https://github.com/xdp-project/xdp-tutorial
- https://blog.csdn.net/hbhgyu/article/details/109600180
- https://lists.iovisor.org/g/iovisor-dev/topic/30315706
- https://github.com/iovisor/bcc/issues/2463
- https://github.com/facebookincubator/katran/blob/master/katran/lib/bpf/balancer_helpers.h
- https://man.archlinux.org/man/bpftool-net.8.en
- https://stackoverflow.com/questions/75849176/why-my-xdp-program-with-xdp-tx-not-working
- https://www.kernel.org/doc/html/latest/core-api/printk-format...
下文預告
本文采用了 bpftool 來手動載入 eBPF 程式,並且 VIP 和 RIP 都是 hard code。後面可以使用 libbpf 來支援 eBPF 的程式化載入和 VIP 配置。
另,本文體驗了 xdp 如何替換 LVS 實現負載均衡功能,但是並沒有充分體現 xdp 的優勢,下回將分析 xdp 的真正優勢場景:直接部署在普通伺服器上,去掉專用的 LVS 伺服器。