ebpf-go 初體驗
前言
我們在《用 eBPF/XDP 來替代 LVS》系列、《一張圖感受真實的 TCP 狀態轉移》系列,以及《如何終結已存在的 TCP 連接?》系列文章中,均通過純 C 語言和 libbpf1 這個庫來運用 eBPF。
但是很多的場景中(尤其是雲原生場景),我們出於避免重複造輪子、更快的迭代速度、運行時安全等原因,會選擇 go 語言來進行開發,ebpf-go2 這個庫就是當前最好的選擇。
今天,我們就對 ebpf-go 進行一個初體驗,這個體驗不是按部就班的 API 文檔,而是通過一個簡單的需求,讓大家得到一個真切的感受,這個需求就是:統計發向本機的每個 “連接” 的包數量,並且每新增 5 個 “連接” 就進行一次數據展示。
體驗
依賴
本次體驗需要許多前置條件:
-
Linux kernel 版本 5.7 以上,以支持 bpf_link(我是 6.6.5)
-
LLVM 版本 11 以上 (clang and llvm-strip,檢查命令
clang --version
) -
libbpf headers (Debian/Ubuntu 是 libbpf-dev,Fedora 是 libbpf-devel)
-
Linux kernel headers (Debian/Ubuntu 是 linux-headers-amd64,Fedora 是 kernel-devel)
-
Go compiler 版本需要支持 ebpf-go (我安裝了 GO 1.21,檢查命令
go version
)
項目初始化
# 創建項目
mkdir ebpf-go-exp && cd ebpf-go-exp
go mod init ebpf-go-exp
go mod tidy
# 手動引入依賴
go get github.com/cilium/ebpf/cmd/bpf2go
如果依賴下載超時的話,可以設置下代理:
go env -w GOPROXY=https://goproxy.cn,direct
代碼
C 代碼
...
//go:build ignore
struct event {
__u32 count;
};
const struct event *unused __attribute__((unused));
SEC("xdp")
int count_packets(struct xdp_md *ctx) {
__u32 ip;
__u16 sport;
__u16 dport;
if (!parse_ip_src_addr(ctx, &ip, &sport, &dport)){
goto done;
}
__u16 r_sport = bpf_ntohs(sport);
bpf_printk("Process a packet of tuple from %u|%pI4n:%u|%u",ip,&ip,sport,r_sport);
if(8080 != bpf_ntohs(dport)){
goto done;
}
struct tuple key;
__builtin_memset(&key,0,sizeof(key));
key.addr = ip;
key.port = sport;
__u32 *pkt_count = bpf_map_lookup_elem(&pkt_count_map, &key);
if (!pkt_count) {
__u32 init_pkt_count = 1;
bpf_map_update_elem(&pkt_count_map, &key, &init_pkt_count, BPF_NOEXIST);
__u32 key = 0;
__u64 *count = bpf_map_lookup_elem(&tuple_num, &key);
if (count) {
__sync_fetch_and_add(count, 1);
if(*count % 5 == 0){
struct event *e;
e = bpf_ringbuf_reserve(&events, sizeof(struct event), 0);
if (e){
e->count = *count;
bpf_ringbuf_submit(e, 0);
}
}
}
} else {
__sync_fetch_and_add(pkt_count, 1);
}
done:
return XDP_PASS;
}
...
Go 代碼
...
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type event counter counter.c -- -I headers
func main() {
// Load the compiled eBPF ELF and load it into the kernel.
var objs counterObjects
if err := loadCounterObjects(&objs, nil); err != nil {
log.Fatal("Loading eBPF objects:", err)
}
defer objs.Close()
ifname := "lo"
iface, err := net.InterfaceByName(ifname)
if err != nil {
log.Fatalf("Getting interface %s: %s", ifname, err)
}
// Attach count_packets to the network interface.
link, err := link.AttachXDP(link.XDPOptions{
Program: objs.CountPackets,
Interface: iface.Index,
})
if err != nil {
log.Fatal("Attaching XDP:", err)
}
defer link.Close()
rd, err := ringbuf.NewReader(objs.Events)
if err != nil {
log.Fatalf("opening ringbuf reader: %s", err)
}
defer rd.Close()
stopper := make(chan os.Signal, 1)
signal.Notify(stopper, os.Interrupt, syscall.SIGTERM)
go func() {
<-stopper
if err := rd.Close(); err != nil {
log.Fatalf("closing ringbuf reader: %s", err)
}
}()
log.Println("Waiting for events..")
// counterEvent is generated by bpf2go.
var event counterEvent
for {
record, err := rd.Read()
if err != nil {
if errors.Is(err, ringbuf.ErrClosed) {
log.Println("Received signal, exiting..")
return
}
log.Printf("reading from reader: %s", err)
continue
}
if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &event); err != nil {
log.Printf("parsing ringbuf event: %s", err)
continue
}
log.Printf("tuple num: %d", event.Count)
var (
key counterTuple
val uint32
)
iter := objs.PktCountMap.Iterate()
for iter.Next(&key, &val) {
sourceIP := key.Addr
sourcePort := key.Port
packetCount := val
log.Printf("%d/%s:%d => %d\n", sourceIP, int2ip(sourceIP), sourcePort, packetCount)
}
}
}
...
完整代碼在:https://github.com/MageekChiu/epbf-go-exp
運行
# 生成腳手架代碼
go generate
# 利用生成的 GO 代碼,進行編譯和運行
go build && sudo ./ebpf-go-exp
本機運行一個 openresty 並監聽 8080 端口,然後反覆訪問測試
openresty
curl localhost:8080
curl localhost:8080
...
查看日誌
# bpf 輸出
bpftool prog tracelog
bpf_trace_printk: Process a packet of tuple from 16777343|127.0.0.1:31876|33916
...
bpf_trace_printk: Process a packet of tuple from 16777343|127.0.0.1:56449|33244
...
# go 輸出
Waiting for events..
tuple num: 5
16777343/127.0.0.1:31876 => 7
16777343/127.0.0.1:31364 => 7
16777343/127.0.0.1:56449 => 1
16777343/127.0.0.1:10909 => 7
16777343/127.0.0.1:30340 => 7
...
若迭代過程中,C 代碼有變化,則需要執行 go generate
,否則僅執行 Go 編譯部分即可。
重點解析
-
c 代碼主體和我們之前系列的文章類似,就是在 xdp hook 點解析出每個報文的四元組,然後存入
pkt_count_map
。tuple_num
這個 map 作爲全局變量,通過__sync_fetch_and_add
安全地併發,每當新增 5 個 “連接” 就向用戶空間發送事件。 -
c 代碼中
unused
這個event
指針必須聲明,不然用戶態的 go 就拿不到這個數據結構。 -
c 代碼中開始的
ignore
是必須的,避免 go build 報錯:C source files not allowed when not using cgo or SWIG
-
go generate
命令會根據 go 代碼中的go:generate
語句生成腳手架代碼,就像 BPF Skeleton3,然後我們就可以在 go 代碼中便捷地訪問 c 中定義的 map,prog 以及一些數據結構。-type event counter
使得 c 中定義的event
結構體在 go 中就是counterEvent
結構體。 -
內核空間向用戶空間發送數據 / 事件可以通過 perfbuf 和 ringbuf 實現,從而避免用戶空間輪詢數據。這兩者雖然功能有不少差別,但是 api 都差不多 4。當我們收到內核的通知後,通過 Iterate 來遍歷統計數據的 map,實現一種類似於推拉結合的架構。
小插曲
如果我們的 pkt_count_map
這樣寫的話:
struct tuple key = {ip,bpf_ntohs(sport)};
__u32 *pkt_count = bpf_map_lookup_elem(&pkt_count_map, &key);
if (!pkt_count) {
...
}else{
...
}
就可能會得出一個看起來很奇怪的結果:
Waiting for events..
tuple num: 5
16777343/127.0.0.1:60162 => 3
16777343/127.0.0.1:45076 => 3
16777343/127.0.0.1:45082 => 3
16777343/127.0.0.1:60162 => 4
16777343/127.0.0.1:45076 => 4
以及
bpftool map dump name pkt_count_map
[{
"key": {
"addr": 16777343,
"port": 60162
},
"value": 3
},{
"key": {
"addr": 16777343,
"port": 45082
},
"value": 3
},{
"key": {
"addr": 16777343,
"port": 45076
},
"value": 3
},{
"key": {
"addr": 16777343,
"port": 45082
},
"value": 4
},{
"key": {
"addr": 16777343,
"port": 60162
},
"value": 4
},{
"key": {
"addr": 16777343,
"port": 45076
},
"value": 4
}
]
乍一看你會發現 map 的 key 怎麼有些重複了?這裏先賣個關子,後面有機會再來分析。
總結
本文我們瞭解 ebpf-go 的一些常見用法,讓大家對 ebpf-go 有了一個模糊但整體的認識,更多的細節,可以通過官網的文檔 5 以及 examples6 進行了解。
下一篇文章,我們就從實戰的角度,看看 cilium7 是怎麼通過 ebpf-go 來發揮 ebpf 威力的。
關注九零後程序員,瞭解更多有趣技術
參考
-
https://libbpf.readthedocs.io/ ↩
-
https://ebpf-go.dev/about/ ↩
-
https://www.kernel.org/doc/html/next/bpf/libbpf/libbpf_overview.html#bpf-object-skeleton-file ↩
-
https://nakryiko.com/posts/bpf-ringbuf/#bpf-ringbuf-vs-bpf-perfbuf ↩
-
https://ebpf-go.dev/guides/getting-started/ ↩
-
https://github.com/cilium/ebpf/tree/main/examples ↩
-
https://docs.cilium.io/en/stable/overview/intro/ ↩
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/T6PPAs6LAD6YR9Zk2c1A3w