在如下網絡層面下,代理(比如Envoy nginx )執行額外的L7策略(Health checks, service discovery, load balancing, mutual TLS),其開銷比較大,主要體現在傳統的TCP/IP協議棧路徑比較冗余,導致其開銷比較大;就像同一主機上unix域 socket比 udp socket 快一樣。
為了解決此問題;目前最新內核中引入了ebpf-socket-map 來加速數據包的轉發
最后實現類似下面數據包轉發流程;非常類似於socketpair unix域這些設計理念
什么是sockmap?
- SOCKMAP or specifically "BPF_MAP_TYPE_SOCKMAP", is a type of an eBPF map ,A sockmap is a BPF map type that holds references to sock structs. Then with a new sk redirect bpf helper BPF programs can use the map to redirect skbs between sockets,
也就是sockmap是ebpf中BPF_PROG_TYPE_SK_SKB的一種, 是BPF_PROG_TYPE_SK_SKB程序類型中的一個應用BPF_MAP_TYPE_SOCKMAP;
https://lwn.net/Articles/731133/ functionality is used in conjunction with a sockmap - a special-purpose BPF map that contains references to socket structures and associated values. sockmaps are used to support redirection. The program is attached and the bpf_sk_redirect_map() helper can be used to carry out the redirection,
那怎么理解eBPF
- (e)BPF能夠在內核態執行用戶提供的程序
The bpf() system call
使用bpf()這個系統調用函數配合BPF_PROG LOAD
命令來加載程序。它的原型是:
int bpf(int cmd, union bpf_attr *attr, unsigned int size);
bpf_attr union
允許在內核和用戶空間之間傳遞數據;確切的格式取決於 cmd
這個參數。
size
這個參數表示bpf_attr union
這個對象以字節為單位的大小。
可以使用命令創建和修改eBPF maps數據結構,這個數據結構一個通用鍵值對數據結構,用於在eBPF程序和內核或用戶空間之間通信。附加命令允許將eBPF程序附加到控制組目錄或套接字文件描述符,遍歷所有map鍵值對和程序,並將eBPF對象保存到文件中,以便加載它們的進程終止時,不會銷毀它們(后者使用了分類器tc的代碼,因此eBPF程序無需加載過程持續運行就可以持久化。、
全部命令列表可以在bpf() man手冊中找到。雖然有許多不同的命令,但它們可以被分成三類:
- 使用eBPF程序的命令
- 使用eBPF maps的命令
- 同時使用程序和maps的命令(統稱為對象)。
eBPF程序類型
函數BPF_PROG_LOAD
加載的程序類型規定了四件事:
- 程序可以附加在哪里
- 驗證器允許調用內核中的哪些幫助函數
- 網絡包的數據是否可以直接訪問
- 作為第一個參數傳遞給程序的對象類型實際上
程序類型本質上定義了一個API。甚至還創建了新的程序類型,以區分允許調用的不同的函數列表(比如BPF_PROG_TYPE_CGROUP_SKB
對比 BPF_PROG_TYPE_SOCKET_FILTER
)。
目前內核支持的eBPF程序類型列表如下所示:
-
BPF_PROG_TYPE_SOCKET_FILTER,
一種網絡數據包過濾器;attach一個bpf程序到socket上,你可以獲取到被socket處理的所有數據包。socket過濾不允許你修改這些數據包以及這些數據包的目的地。僅僅是提供給你觀察這些數據包。在你的程序中可以獲取到諸如protocol type類型等。
- BPF_PROG_TYPE_KPROBE,
kprobes是內核提供的動態探測的功能;BPF kprobe program 類型允許你使用BPF程序作為一個kprobe的執行程序。定義為BPF_PROG_TYPE_KPROBE,BPF虛擬機確定你的kprobe程序是否合法。當你寫一個kprobe的bpf程序類型時,你需要確定kprobe是在程序的第一條指令執行還是在最后完成時執行。例如:如果你想檢查exec系統調用的參數,你就需要把它attach到程序的開始:SEC(“kprobe/sys_exec”)。當你需要檢查exec的返回值時,你需要這樣指定:SEC(“kretprobe/sys_exec”).。理論上/proc/kallsyms下面的方法都是可以被probe程序執行的。
-
BPF_PROG_TYPE_SCHED_CLS, 一種網絡流量控制分類器
-
BPF_PROG_TYPE_SCHED_ACT, 一種網絡流量控制動作
-
BPF_PROG_TYPE_TRACEPOINT,
bpf-tracepoint類型的程序attach到kernel預先定義好的traceponit上。相比於krobe,它是不靈活的,因為需要kernel預先定義好tracepoints。但是他們是很穩定的。所有的traceponits在內核的/sys/kernel/debug/tracing/events下面可以看到。比較有意思的是:BPF還定義了自己的tracepoints,因此你可以寫BPF程序來檢查另一個bpf程序的行為。bPF的tracepoints定義在/sys/kernel/debug/tracing/events/bpf下面。例如:有一個tracepoints叫做bpf_prog_load。這就意味着你可以寫bpf的代碼去檢查bpf程序load的過程。
-
BPF_PROG_TYPE_XDP,從設備驅動程序接收路徑運行的網絡數據包過濾器
-
BPF_PROG_TYPE_PERF_EVENT,確定是否應該觸發perf事件處理程序
-
BPF_PROG_TYPE_CGROUP_SKB,一種用於控制組的網絡數據包過濾器
-
BPF_PROG_TYPE_CGROUP_SOCK,一種由於控制組的網絡包篩選器,它被允許修改套接字選項
-
BPF_PROG_TYPE_LWT_IN,用於輕量級隧道的網絡數據包過濾器
-
BPF_PROG_TYPE_LWT_OUT,用於輕量級隧道的網絡數據包過濾器
-
BPF_PROG_TYPE_LWT_XMIT,用於輕量級隧道的網絡數據包過濾器
-
BPF_PROG_TYPE_SOCK_OPS,
一個用於設置套接字參數的程序
-
BPF_PROG_TYPE_SK_SKB,
一個用於套接字之間轉發數據包的網絡包過濾器
-
BPF_PROG_TYPE_CGROUP_DEVICE,
確定是否允許設備操作
-
BPF_PROG_TYPE_SK_MSG
These types of programs let you controlwhether a message sent to a socket should be delivered 當內核創建了一個socket,它會被存儲在前面提到的map中。當你attach一個程序到這個socket map的時候,所有的被發送到那些socket的message都會被filter.在filter message之前,內核拷貝了這些data,因此你可以讀取這些message,而且可以給出你的決定:例如,SK_PASS和SK_DROP。
eBPF 數據結構
eBPF程序使用的主要數據結構是eBPF map(鍵值對)數據結構,這是一種通用的數據結構,允許在內核內部或內核與用戶空間之間來回傳遞數據。正如名稱“map”所暗示的,數據是使用鍵存儲和檢索的。
使用bpf()系統調用創建和操作map數據結構。成功創建map后,將返回與該map關聯的文件描述符。每個map由四個值定義:類型、元素的最大個數、值大小(以字節為單位)和鍵大小(以字節為單位)。有不同的map類型,每種類型都提供不同的行為和一些權衡:
BPF_MAP_TYPE_HASH
: 一種哈希表BPF_MAP_TYPE_ARRAY
: 一種為快速查找速度而優化的數組類型map鍵值對,通常用於計數器BPF_MAP_TYPE_PROG_ARRAY
: 與eBPF程序相對應的一種文件描述符數組;用於實現跳轉表和處理特定(網絡)包協議的子程序BPF_MAP_TYPE_PERCPU_ARRAY
: 一種基於每個cpu的數組,用於實現展現延遲的直方圖BPF_MAP_TYPE_PERF_EVENT_ARRAY
: 存儲指向perf_event
數據結構的指針,用於讀取和存儲perf事件計數器BPF_MAP_TYPE_CGROUP_ARRAY
: 存儲指向控制組的指針BPF_MAP_TYPE_PERCPU_HASH
: 一種基於每個CPU的哈希表BPF_MAP_TYPE_LRU_HASH
: 一種只保留最近使用項的哈希表BPF_MAP_TYPE_LRU_PERCPU_HASH
: 一種基於每個CPU的哈希表,只保留最近使用項BPF_MAP_TYPE_LPM_TRIE
: 一個匹配最長前綴的字典樹數據結構,適用於將IP地址匹配到一個范圍BPF_MAP_TYPE_STACK_TRACE
: 存儲堆棧跟蹤信息BPF_MAP_TYPE_ARRAY_OF_MAPS
: 一種map-in-map數據結構BPF_MAP_TYPE_HASH_OF_MAPS
: 一種map-in-map數據結構BPF_MAP_TYPE_DEVICE_MAP
: 用於存儲和查找網絡設備的引用BPF_MAP_TYPE_SOCKET_MAP
: 存儲和查找套接字,並允許使用BPF幫助函數進行套接字重定向
可以使用bpf_map_lookup_elem()
函數和bpf_map_update_elem()
函數從eBPF程序或用戶空間程序訪問所有map對象
如何編寫一個eBPF程序
目前可以將C語言寫的程序通過LLVM Clang
編譯器,編譯成字節碼。然后可以使用bpf()系統調用函數和BPF_PROG_LOAD
命令,直接加載包含這個字節碼的對象文件。
通過使用Clang編譯器,配合-march=bpf
參數,就可以用C語言編寫自己的eBPF程序了。在內核代碼的 samples/bpf/ 目錄下有很多eBPF程序的示例,它們的文件名稱大部分都具有「_kern.c
」的后綴。Clang編譯出來的目標文件(eBPF字節碼),需要由在本機運行的一個程序進行加載(這些示例的文件名稱中通常具有「_user.c
」)。為了更容易地編寫eBPF程序,內核提供了libbpf庫,其中包括用於加載程序、創建和操作eBPF對象的幫助函數。舉個例子,一個eBPF程序和使用libbpf庫的用戶程序的抽象的工作流程一般像如下這樣的:
- 讀取eBPF字節碼到用戶應用程序中的緩沖區,並將其傳遞給
bpf_load_program()
函數 - eBPF程序,當在內核運行時,它將調用
bpf_map_lookup_elem()
函數來查找map中的元素,並存儲新值給這個元素。 - 用戶應用程序調用
bpf_map_lookup_elem()
函數來讀取eBPF程序存儲在內核中的值。
但是,上面提到的所有的樣例代碼都有一個主要缺點:您需要從內核源代碼樹中編譯你的eBPF程序。幸運的是,BCC項目就是為了解決這個問題而誕生的。它包括一個完整的工具鏈,用於編寫eBPF程序,並在不不要鏈接內核源代碼樹的情況下加載它們。
簡化版BPF Map創建方式
相對於直接使用bpf系統調用函數來創建BPF Map,在實際場景中常用的是一個簡化版:
struct bpf_map_def SEC("maps") my_bpf_map = { .type = BPF_MAP_TYPE_HASH, .key_size = sizeof(int), .value_size = sizeof(int), .max_entries = 100, .map_flags = BPF_F_NO_PREALLOC, };
這個簡化版看起來就是一個BPF Map聲明,它是如何做到聲明即創建的呢?關鍵點就是SEC("maps"),學名ELF慣例格式(ELF convention),它的工作原理是這樣的:
聲明ELF Section屬性 SEC("maps") (之前的博文里有對Section作用的描述)
內核代碼bpf_load.crespect目標文件中所有Section信息,它會掃描目標文件里定義的Section,其中就有用來創建BPF Map的SEC("maps"),我們可以到相關代碼里看到說明:
// https://elixir.bootlin.com/linux/v4.15/source/samples/bpf/bpf_load.h#L41 /* parses elf file compiled by llvm .c->.o * . parses 'maps' section and creates maps via BPF syscall // 就是這里 * . parses 'license' section and passes it to syscall * . parses elf relocations for BPF maps and adjusts BPF_LD_IMM64 insns by * storing map_fd into insn->imm and marking such insns as BPF_PSEUDO_MAP_FD * . loads eBPF programs via BPF syscall * * One ELF file can contain multiple BPF programs which will be loaded * and their FDs stored stored in prog_fd array * * returns zero on success */ int load_bpf_file(char *path);
- bpf_load.c掃描到SEC("maps")后,對BPF Map相關的操作是由load_maps函數完成,其中的bpf_create_map_node()和bpf_create_map_in_map_node()就是創建BPF Map的關鍵函數,它們背后都是調用了定義在內核代碼tools/lib/bpf/bpf.c中的方法,而這個方法就是使用上文提到的BPF_MAP_CREATE命令進行的系統調用。
- 最后在編譯程序時,通過添加
bpf_load.o
作為依賴庫,並合並為最終的可執行文件中,這樣在程序運行起來時,就可以通過聲明SEC("maps")
即可完成創建BPF Map的行為了。
從上面梳理的過程可以看到,這個簡化版雖然使用了“語法糖”,但最后還是會去使用bpf()函數完成系統調用。
如何操作BPF Map
BPF Map也有自己的CRUD,除了bpf_map_create
是創建BPF Map操作之外,下面列出了其他主要操作,
bpf_map_lookup_elem(map, key)
函數,通過key查詢BPF Map,得到對應valuebpf_map_update_elem(map, key, value, options)
函數,通過key-value更新BPF Map,如果這個key不存在,也可以作為新的元素插入到BPF Map中去bpf_map_get_next_key(map, lookup_key, next_key)
函數,這個函數可以用來遍歷BPF Map,下文有具體的介紹。
參考文檔:
https://blogs.oracle.com/linux/notes-on-bpf-1
https://lwn.net/Articles/740157/
https://lwn.net/Articles/731133/
https://lwn.net/Articles/810297/
https://blog.cloudflare.com/sockmap-tcp-splicing-of-the-future/
https://www.ibm.com/developerworks/cn/linux/l-lo-eBPF-history/index.html
https://blog.csdn.net/hbhgyu/article/details/108854003
http://arthurchiao.art/blog/ebpf-and-k8s-zh/