在集群網絡使用cilium之后,最明顯的情況就是:服務暴露vip+port,在集群內怎么測試都正常,但集群外訪問可能是有問題的。而這就在于cilium所使用的ebpf科技。
相對底層一點的語言,比如c語言,在創建一個tcp連接時,主要分兩步(其它語言可能會更簡單):
int socket_desc; struct sockaddr_in server; //Create socket socket_desc = socket(AF_INET , SOCK_STREAM , 0); server.sin_addr.s_addr = inet_addr("1.1.1.1"); server.sin_family = AF_INET; server.sin_port = htons( 80 ); //Connect to remote server if (connect(socket_desc , (struct sockaddr *)&server , sizeof(server)) < 0)
一個連接的創建,主要分兩個步驟:
創建socket對象
發起connect連接
而實際上,在內核層,它經歷的步驟會非常多。可以通過perf工具來查看:
perf trace -e 'net:*' -e 'sock:*' -e 'syscalls:*' curl 1.1.1.1 -s >& /dev/stdout
上面的輸出很多,而syscalls:sys_enter_socket前面的很長一段,是curl程序打開本身加載動態鏈接庫需要的系統調用。
而本次需要關心的是以下這部分(截取的部分內容):
108.294 curl/15819 syscalls:sys_enter_socket(family: INET, type: STREAM) 108.351 curl/15819 syscalls:sys_exit_socket(__syscall_nr: 41, ret: AX25) 108.939 curl/15819 syscalls:sys_enter_connect(fd: 3, uservaddr: { .family: UNSPEC }, addrlen: 16) 108.991 curl/15819 sock:inet_sock_set_state(skaddr: 0xffff902527424c80, oldstate: 7, newstate: 2, dport: 80, family: 2, protocol: 6, saddr: 0x7f176658943b, daddr: 0x7f176658943f, saddr_v6: 0x7f1766589443, daddr_v6: 0x7f1766589453) 109.090 curl/15819 net:net_dev_queue(skbaddr: 0xffff9024f0a2d4e8, len: 74, name: "enp1s0") 109.140 curl/15819 net:net_dev_start_xmit(name: "enp1s0", skbaddr: 0xffff9024f0a2d4e8, protocol: 2048, ip_summed: 3, len: 74, network_offset: 14, transport_offset_vali d: 1, transport_offset: 34, gso_segs: 1, gso_type: 1)
從上面可以看出,在定義socket后,接著就是connect連接,而在sock:inet_sock_set_state這一步,有輸出地址相關信息,但輸出的是內存地址,無法直接查看。能通過bcc工具集中的tcplife來查看。
# 一個終端中運行: tcplife -D 12345 # 另一個終端中運行: curl 1.1.1.1:12345
雖然訪問的是不存在的地址,但內核也會基于默認路由,走默認網關,將報文發送到enp1s0網卡上。而在sock:inet_sock_set_state可以抓取到源地址與目的地址信息。 既然我們能在sock:inet_sock_set_state點掛載程序,抓取報文信息,那我們是否可以在掛載點,修改socket的目的地址與目的端口信息?
答案是肯定的。但cilium是在cgroup/connect4進行修改的(和上面從perf查出來的不同,但可以通過bcc的工具來驗證。cgroup是高版本內核才有的特殊,具體可參考鏈接,里面有標識內核版本的特性。
那么,這是如何查到的呢?
[root@c7-1 ~]# bpftool prog |grep sock 1653: type 18 name sock6_connect tag d526fd1cb49a372e gpl 1657: cgroup_sock name sock6_post_bind tag e46a7916c9c72e67 gpl 1661: type 18 name sock6_sendmsg tag 19094f9c26d4dddf gpl 1665: type 18 name sock6_recvmsg tag 282bf4c10eff7f73 gpl 1669: type 18 name sock4_connect tag 57eae2cf019378cc gpl 1673: cgroup_sock name sock4_post_bind tag ddd7183184f2e6e9 gpl 1677: type 18 name sock4_sendmsg tag 570ef9d580ce0589 gpl 1681: type 18 name sock4_recvmsg tag 0bdebe7409ceb49f gpl [root@c7-1 ~]# bpftool prog |grep connect 1653: type 18 name sock6_connect tag d526fd1cb49a372e gpl 1669: type 18 name sock4_connect tag 57eae2cf019378cc gpl
在有運行cilium的機器上,使用bpftool工具查詢掛載的程序,發現與socket相關的就是這些。
再到cilium的源代碼中,查看對應的代碼段定義:
github.com/cilium/cilium/bpf$ grep -i "__section(" *.c bpf_host.c:__section("from-netdev") bpf_host.c:__section("from-host") bpf_host.c:__section("to-netdev") bpf_host.c:__section("to-host") bpf_lxc.c:__section("from-container") bpf_lxc.c:__section("mydebug1") bpf_lxc.c:__section("mydebug2") bpf_lxc.c:__section("to-container") bpf_network.c:__section("from-network") bpf_overlay.c:__section("from-overlay") bpf_overlay.c:__section("to-overlay") bpf_sock.c:__section("cgroup/connect4") bpf_sock.c:__section("cgroup/post_bind4") bpf_sock.c:__section("cgroup/bind4") bpf_sock.c:__section("cgroup/sendmsg4") bpf_sock.c:__section("cgroup/recvmsg4") bpf_sock.c:__section("cgroup/getpeername4") bpf_sock.c:__section("cgroup/post_bind6") bpf_sock.c:__section("cgroup/bind6") bpf_sock.c:__section("cgroup/connect6") bpf_sock.c:__section("cgroup/sendmsg6") bpf_sock.c:__section("cgroup/recvmsg6") bpf_sock.c:__section("cgroup/getpeername6") bpf_xdp.c:__section("from-netdev")
由此,cilium使用的科技就很明顯了。
在看cilium源碼實現之前,先手寫一個最簡單的修改目的地址與端口的程序。因為cilium本身框架很復雜,代碼也有相關,所以先以最簡單的(寫死的)程序入手。代碼可以參考cilium源碼。
#include <bpf/ctx/unspec.h> #include <bpf/api.h> #define SKIP_POLICY_MAP 1 #define SKIP_CALLS_MAP 1 #define SYS_REJECT 0 #define SYS_PROCEED 1 # define printk(fmt, ...) \ ({ \ const char ____fmt[] = fmt; \ trace_printk(____fmt, sizeof(____fmt), \ ##__VA_ARGS__); \ }) __section("cgroup/connect4") int sock4_connect(struct bpf_sock_addr *ctx ) { if (ctx->user_ip4 != 0x04030201) { // des ip is 1.2.3.4 return SYS_PROCEED; } printk("aa %x ", ctx->user_ip4); ctx->user_ip4=0x19280a0a; // set to 10.10.40.25 printk("set ok %x,%x", ctx->user_ip4, ctx->user_port); return SYS_PROCEED; } BPF_LICENSE("Dual BSD/GPL");
程序說明:
判斷目標ip是1.2.3.4才處理(對應16進制順序相反,是因為系統為小端模式)。
輸出目的ip,方便debug。
修改目的ip為指定的ip。
輸出設置的結果。
入參bpf_sock_addr,可從cilium的源碼中找到相關定義。
mysock.c
/* User bpf_sock_addr struct to access socket fields and sockaddr struct passed * by user and intended to be used by socket (e.g. to bind to, depends on * attach type). */ struct bpf_sock_addr { __u32 user_family; /* Allows 4-byte read, but no write. */ __u32 user_ip4; /* Allows 1,2,4-byte read and 4-byte write. * Stored in network byte order. */ __u32 user_ip6[4]; /* Allows 1,2,4,8-byte read and 4,8-byte write. * Stored in network byte order. */ __u32 user_port; /* Allows 1,2,4-byte read and 4-byte write. * Stored in network byte order */ __u32 family; /* Allows 4-byte read, but no write */ __u32 type; /* Allows 4-byte read, but no write */ __u32 protocol; /* Allows 4-byte read, but no write */ __u32 msg_src_ip4; /* Allows 1,2,4-byte read and 4-byte write. * Stored in network byte order. */ __u32 msg_src_ip6[4]; /* Allows 1,2,4,8-byte read and 4,8-byte write. * Stored in network byte order. */ __bpf_md_ptr(struct bpf_sock *, sk); };
基于k8s部署cilium后,cilium會在容器中初始化好環境,我們可以直接使用,省去編譯環境、cgroupv2配置的麻煩。
將上面的文件,復制到cilium的容器中(本樣例中使用的cilium版本為1.12.7)。
file=./mysock.c clang -O2 -target bpf -std=gnu89 -nostdinc -emit-llvm -g -Wall -Wextra -Werror -Wshadow -Wno-address-of-packed-member -Wno-unknown-warning-option -Wno-gnu-variable-sized-type-not-at-end -Wdeclaration-after-statement -Wimplicit-int-conversion -Wenum-conversion -I. -I/run/cilium/state/globals -I/var/lib/cilium/bpf -I/var/lib/cilium/bpf/include -D__NR_CPUS__=8 -DENABLE_ARP_RESPONDER=1 -DCALLS_MAP=cilium_calls_lb -c $file -o - | llc -march=bpf -mcpu=v2 -mattr=dwarfris -filetype=obj -o mysock.o bpftool cgroup detach /run/cilium/cgroupv2 connect4 pinned /sys/fs/bpf/tc/globals/mytest rm -f /sys/fs/bpf/tc/globals/mytest tc exec bpf pin /sys/fs/bpf/tc/globals/mytest obj mysock.o type sockaddr attach_type connect4 sec cgroup/connect4 bpftool cgroup attach /run/cilium/cgroupv2 connect4 pinned /sys/fs/bpf/tc/globals/mytest
開啟四個終端,分別執行如下命令(直接在主機上執行):
# command 1 cat /sys/kernel/debug/tracing/trace_pipe # command 2 tcpconnect -P 80 # command 3 tcplife -D 80 # command 4 curl 1.2.3.4:80
因為我們會變更目的ip,所以就基于端口來抓包。
用tcplife抓包,抓的是上面perf的sock:inet_sock_set_state時的狀態。
用tcpconnect抓的是connect() syscall時的狀態。
mkdir -p /run/cilium/cgroupv2 mount -t cgroup2 none /run/cilium/cgroupv2/
因為centos8自帶的tc與bpftool版本有點低,所以使用cilium中已經適配好的版本。
docker run -it --name=mytest --network=host --privileged -v $PWD:/hosts/ -v /sys/fs/bpf:/sys/fs/bpf -v /run/cilium/cgroupv2/:/run/cilium/cgroupv2 cilium:v1.12.7 bash cd /hosts/ # 可以直接用之前編譯好的文件 tc exec bpf pin /sys/fs/bpf/tc/globals/mytest obj mysock.o type sockaddr attach_type connect4 sec cgroup/connect4 bpftool cgroup attach /run/cilium/cgroupv2 connect4 pinned /sys/fs/bpf/tc/globals/mytest
很香!你會發現,功能已經實現了。
框架已定型,通過在ebpf中,獲取目的ip與目的端口,然后基于映射規則將目的ip與端口進行修改,從而實現vip到目的地址的轉換。
由于它是在connect階段做的轉換,類似在調用connect函數時注冊一個回調函數,和dnat是不同的,所以不需要在回包時轉換還原。
cilium service list
這個命令可以查看cilium基于service配置的映射規則,ebpf程序再從這個規則中找到合適的bacend,并修改目的地址,然后完成轉換。
服務暴露關心的主要是兩點:1. vip的高可用。2.負載均衡。而這兩點,通過本文所介紹的方式都是可以實現的。
1.vip的高可用
vip的高可用,其本質就是在服務異常時,可以切換到服務b(這里暫不考慮有狀態服務分主備的情況)。
當我們在客戶端運行ebpf程序時,就不需要這個vip了。在應用時可以配置一個虛擬的地址,比如1.2.3.4,由ebpf程序來決定轉換到哪個實際的后端服務。而且當服務a異常后,可以變更映射規則,切換到服務b。這一切對應用都是透明的。
2.負載均衡
既然可以將目的地址映射到服務a,那么基于服務a,b,c之間做負載均衡也是可行的。包括設置權重、熔斷等。
如istio,需要在客戶端注入sidecar,運行envoy程序,其實也是相類似的邏輯,只不過它是通過代理實現。除了解析目的地址外,它還支持解析數據包,比如解析http協議,在異常時自動重試,實現服務切換應用無感知。相對于應用來說,只是卡頓了一下。
ebpf程序能夠滿足大部分場景,而且很高效。
作者:
沃趣科技產品研發部
服務電話: 400-678-1800 (周??周五 09:00-18:00)
商務合作: 0571-87770835
市場反饋: marketing@woqutech.com
地址: 杭州市濱江區濱安路1190號智匯中?A座1101室