執行人: YSRossi
專題解說錄影
依據 ktcp 的指示,我們可在 sysprog21/khttpd 的程式碼基礎之上,打造出高效且穩定的網頁伺服器,不過 Linux 核心模組建構和維護的成本極高,本任務嘗試以 eBPF 來建構 TCP 伺服器。需要確保在 Linux v5.15+ 運作。
相關資訊:
需要自行安裝 libbpf
研讀 BPF Documentation,留意 Linux 核心封包的處理機制,特別是 sockmap,對照閱讀 How to use eBPF for accelerating Cloud Native applications。
修改 ebpf-summit-2020 並實作簡易的 echo 伺服器,並比較 kecho 及使用 epoll 系統的應用程式。
Extended Berkeley Packet Filter (eBPF) 是從 Berkeley Packet Filter (BPF) 發展而來。BPF 最初是爲了過濾封包而創造的,但隨着功能的擴充變成 eBPF 後,功能已不限於過濾封包。
一般來說,想要在核心增加新功能,需要寫 kernel module 或修改核心程式碼來達成。而 BPF 可以不透過上述方法就能辦到,BPF 允許在 user mode 撰寫 BPF 程式碼後,編譯成 BPF bytecode 位於核心執行。BPF 提出了一種在核心內完成過濾封包的方法。BPF 可以說是在核心內的虛擬機器,根據虛擬機器判斷該封包是否符合條件,符合條件的話將其從 kernel space 複製到 user space,藉此在核心過濾封包。
封包過濾工具 tcpdump
底下是基於 BPF 來完成封包過濾,tcpdump
藉由 libpcap 轉譯封包過濾條件給位於核心內的 BPF , BPF 依照條件過濾出來的封包,從 kernel space 複製到 user space 被 libpcap 接收後,再傳給 tcpdump
。
透過以下指令捕捉 IP 相關的封包,並觀察 ip
過濾條件編譯的情形
(000)
: 從封包位移量 12
的地方載入一個 half word (2 bytes),對應到下圖的 Ether Type 欄位。
Ethernet header (14 bytes)
|––––––––––––– Offset 12 bytes –––––––––––––>|
Desetination MAC Address (6 bytes) |
Source MAC Address (6 bytes) |
Ether Type (2 bytes) |
Payload |
---|
(001)
: 檢查暫存器的值是否為 0x0800
,即 IP 的 Ether Type,如果是就跳到 (002)
,否則跳到 (003)
。(002), (003)
: BPF 根據回傳值決定是否要過濾該封包,非 0 值代表要擷取的封包長度,0 的話表示不要該封包。主要通用的資料結構是 eBPF map (key/value pair),提供不同的儲存類型,可以在核心中或 kernel space 和 user space 之間傳遞資料。
使用 bpf()
system call 來執行 command
指定的操作。該操作採用 attr
中提供的參數。 size
參數是 attr
的大小。
從 user space 呼叫 bpf()
,藉由指定 bpf()
中的 command
來建立 maps 或對元素做操作。
BPF maps are accessed from user space via the bpf syscall, which provides commands to create maps, lookup elements, update elements and delete elements.
BPF Documentation: eBPF Syscall bpf()
的 command
可以有以下選擇(部分列出)
BPF_MAP_CREATE
close(fd)
可以刪除 map。BPF_MAP_LOOKUP_ELEM
BPF_MAP_UPDATE_ELEM
BPF_MAP_DELETE_ELEM
BPF_OBJ_PIN
BPF_OBJ_GET
BPF_LINK_CREATE
當接收到一個封包時,可以透過 socket lookup 判斷這個封包該由哪個 socket 接收。
觀察核心的 socket lookup 程式碼 linux/include/net/inet_hashtables.h 與 linux/net/ipv4
/inet_hashtables.c
INADDR_ANY
尋找 listening 狀態的 socket其中下列程式碼的第 33 至 39 行,可以發現 eBPF 會在這裡 socket lookup,也就是在上述第二步驟前執行,也可以從 BPF sk_lookup program 得知,在上述步驟二尋找 listening 狀態的 socket 時,會執行 BPF sk_lookup 程式。
The attached BPF sk_lookup programs run whenever the transport layer needs to find a listening (TCP) or an unconnected (UDP) socket for an incoming packet.
由於 ebpf-summit-2020 版本過舊無法執行,YSRossi/ebpf-socket-lookup 為修改後可執行的版本。
宣告 BPF map 儲存所需資訊
echo_dispatch()
將封包分派到 socket
echo_dispatch()
回傳有兩種
根據 BPF sk_lookup program 所述 :
The attached BPF programs must return with either
SK_PASS
orSK_DROP
verdict code. As for other BPF program types that are network filters,SK_PASS
signifies that the socket lookup should continue on to regular hashtable-based lookup, whileSK_DROP
causes the transport layer to drop the packet.
bpf(BPF_OBJ_PIN, ...)
可以將 object pin 到 BPF 虛擬檔案系統,成功執行此 system call 時,能在 /sys/fs/bpf 下看到新建立的路徑,之後就能從 user space 透過 bpf(BPF_OBJ_GET, ...)
打開此路徑從虛擬檔案系統獲得該 object 的 file descriptor 做後續操作。因此只要 object 被 pin,就算終止建立該 object 的程式或關閉該 file descriptor,object 也不會因此消失,可稱為 Persistent BPF objects (內文為較早期的名稱,概念可參考)。此程式碼會建立 eBPF link 將 sk_lookup 程式 attach 到 network namespace
bpf(BPF_OBJ_GET, ...)
從 prog_path
獲取 sk_lookup 程式的 file descriptorbpf(BPF_LINK_CREATE, ...)
建立 link,將程式 attach 到 network namespace,attach 類型為 sk_lookup
link_path
主要功能為插入 socket 到先前已建立的 map
target_pid
) 與 file descriptor (target_fd
),以及該 map 的路徑 (map_path
)pidfd_open(target_pid, 0)
獲取 target_pid
的 pid_fd
pidfd_getfd(pid_fd, target_fd, 0)
複製位於別的行程的 file descriptor (target_fd
) 到目前的行程,獲得一個 local file descriptor sock_fd
。bpf(BPF_MAP_UPDATE_ELEM, ...)
將 sock_fd
存入 map 中一般封包的傳輸過程會如下方左圖所示,經過 socket layer 與 network stack 傳送給對方。當傳送的來源端與目的端都在同一台機器上時,整個傳輸流程都在同個網路空間,事實上可以省略底層 network stack 的處理,如下方右圖所示,透過 eBPF 的 socket redirect 直接在兩個 socket 間傳輸,藉此來提高效率。
想要在兩個 socket 之間直接傳輸,需要一個 map 來紀錄 socket。
宣告 struct sockmap_key
以建立連線雙方的 ip 與 port 作為 map (sockmap_ops
) 的 key, 其中 struct sockmap_key
的成員名稱根據 bpf.h 的命名方式以方便對應。
設定當有 socket 操作 (TCP 三向交握、建立連線等等) 時會執行下方程式,透過此程式監聽 socket。
struct bpf_sock_ops 的註解提到可以從 struct bpf_sock_ops
中獲取 socket 資訊。
User bpf_sock_ops struct to access socket values and specify request ops and their replies.
首先檢查 remote_ip4
與 local_ip4
是否相同,來判斷是否符合傳送的來源端與目的端都在同一台機器上的目標情境,如果相同則繼續執行。
struct bpf_sock_ops 的註解提到 BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB
與 BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB
的意義,若為這兩種情況,分別執行 update_sockmap_ops()
將雙方的 socket 紀錄至 map。
BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB: The SYNACK that concludes the 3WHS.
BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB: The ACK that concludes the 3WHS.
update_sockmap_ops()
利用 struct bpf_sock_ops
中獲取所需的 socket 資訊作為 key,使用 bpf_sock_hash_update
將該 socket 存入 map 供後續操作使用。
獲取 socket 資訊並紀錄至 map 後,接下來要控制傳送目標,因此需要在 socket 呼叫 sendmsg()
時,執行下方程式,將封包直接傳送給目標 socket。
透過 struct sk_msg_md 獲取封包的 metadata 作為 key,若來源端與目的端都在同一台機器上,透過 key 尋找 map 中要 redirect 的目標 socket 並傳送給該 socket。若不在同一台機器上,封包依舊可以用原本的方式傳送。
使用 kecho/user-echo-server.c 作為 echo server,並使用 kecho/bench.c 建立 1000 個 thread 對 echo server 傳送訊息,比較有使用 socket redirect 與沒有使用 socket redirect 的 response time,結果顯示的確使用 socket redirect 的表現較好。
在 user space 建構伺服器,呼叫 recv
時,會從 kernel buffer 複製資料到 user buffer;呼叫 send
時,會從 user buffer 複製資料到 kernel buffer。處理一個封包會有兩次的 memory copy,相較於在核心建構伺服器,顯然還有改進的空間。
接下來會利用 eBPF 建構簡易的 echo server,在 kernel space 中達到接收與傳送封包的功能,節省兩次的 memory copy (亦即 zero-copy),且完全不用撰寫核心模組,更不用修改核心程式碼。
先在 user space 建立一個 socket,按照基本流程呼叫 bind
, listen
, accept
等待 client 的連線,與一般伺服器不同的地方可以看到下方程式碼,完全不用呼叫 recv
與 send
搭配 eBPF 程式就可以建構簡易的 echo server。
與先前介紹 socket redirect 的作法一樣,遇到BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB
與 BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB
這兩種情況,分別執行 update_sockmap_ops()
將雙方的 socket 紀錄至 map。
下方 eBPF 程式碼的類型為 BPF_PROG_TYPE_sk_skb (簡稱 sk_skb),該類型載入核心後,可以針對 socket 的 ingress traffic 做處理,因此可以對即將進入 socket 的封包做處理。
struct __sk_buff
為即將被 socket 接收的封包,sk_skb 類型的程式碼可以存取 struct __sk_buff
的成員,選擇 sockmap key 的所需資訊,紀錄到 skm_key
中。if (skb->local_port != SERVER_PORT)
過濾屬於 server 的封包,若屬於 server 的封包,繼續後續操作,否則依照正常流程傳遞封包。bpf_sk_redirect_hash
將該封包 redirect 到 server socket 的 egress interface,也就是傳送出去的方向,因此可以在 kernel space 中達到相當於在 user space 呼叫 recv
與 send
的功能,減少 user space 與 kernel space 之間 memory copy 的成本。經過測試觀察,接收 SYN 與 ACK 封包時,不會執行 sk_skb 類型的 eBPF 程式碼,但接收 FIN 封包則會執行。為了不將 FIN 封包也 redirect,使伺服器能夠順利接收,將 eBPF 程式碼 (bpf_redir
) 更改成下方程式碼,skb->len
表示封包 payload 的大小,FIN 封包的 skb->len
為 0,因此當 skb->len
不為 0 時,才將該封包 redirect。
先判斷 client 與 server 是否在同台電腦