利用 ebpf sockmap/redirection 提升 socket 效能(2020)
譯者序
本文翻譯自 2020 年的一篇英文部落格 How to use eBPF for accelerating Cloud Native applications。
原文標題非常寬泛,但內容其實很技術:展示瞭如何編寫簡單的 BPF 程式做 socket level 重定向(redirection)。對於源和目的端都在同一臺機器的應用來說,這樣可以 繞過整個 TCP/IP 協議棧,直接將資料傳送到 socket 對端。效果如右下圖(懶得畫圖 ,直接從 Cilium 分享 截個圖,所以其中 Cilium 字樣,但本文不需要 Cilium):
實現這個功能依賴兩個東西:
-
sockmap:這是一個儲存 socket 資訊的對映表。作用:
- 一段 BPF 程式監聽所有的核心 socket 事件,並將新建的 socket 記錄到這個 map;
- 另一段 BPF 程式攔截所有
sendmsg
系統呼叫,然後去 map 裡查詢 socket 對端,之後 呼叫 BPF 函式繞過 TCP/IP 協議棧,直接將資料傳送到對端的 socket queue。
-
cgroups:指定要監聽哪個範圍內的 sockets 事件,進而決定了稍後要對哪些 socket 做重定向。
sockmap 需要關聯到某個 cgroup,然後這個 cgroup 內的所有 socket 就都會執行加 載的 BPF 程式。
執行本文中的例子一臺主機就夠了,非常適合 BPF 練手。譯文所用的完整程式碼, 原文用的完整程式碼。
由於譯者水平有限,本文不免存在遺漏或錯誤之處。如有疑問,請查閱原文。
以下是譯文。
- 譯者序
- 1 引言
- 1.1 BPF 基礎
- 1.2 本文 BPF 程式總體設計
- 2 BPF 程式一:監聽 socket 事件,更新 sockmap
- 2.1 監聽 socket 事件
- 2.2 將 socket 資訊寫入 sockmap
- 2.2.1 從 socket metadata 中提取 sockmap key
- 2.2.2 插入 sockmap
- 2.3 小結
- 3 BPF 程式二:攔截
sendmsg
系統呼叫,socket 重定向- 3.1 攔截
sendmsg
系統呼叫 - 3.2 從 socket message 中提取 key
- 3.3 Socket 重定向
- 3.1 攔截
- 4 編譯、載入、執行
- 4.1 編譯
- 4.2 載入(load)和 attach
sockops
程式- 載入到核心
- Attach 到 cgroups
- 檢視 map ID
- 4.3 載入和 attach
sk_msg
程式- 載入到核心
- Attach
- 檢視
- 4.4 測試
- 4.5 清理
- 5 結束語
- 附錄:BPF 開發環境搭建
很多使用者基於我們提供的服務來構建實時應用(real time applications),這些應用對效能 有著嚴格的要求,因而促使我們不斷探索各種提升效能的方式,eBPF 就是嘗試之一 ,用於加速應用之間的通訊。由於這方面資料尚少,因此我們整理成兩篇文章分享給大家: 本篇講實現,下一篇 是一些效能測試和問題討論。
1 引言
1.1 BPF 基礎
通常情況下,eBPF 程式由兩部分構成:
- 核心空間部分:核心事件觸發執行,例如網路卡收到一個包、系統呼叫建立了一個 shell 程序等等;
- 使用者空間部分:透過某種共享資料的方式(例如 BPF maps)來讀取核心部分產生的資料;
本文主要關注核心部分。核心支援不同型別的 eBPF 程式,它們各自可以 attach 到不同的 hook 點,如下圖所示:
當核心中觸發了與這些 hook 相關的事件(例如,發生 setsockopt()
系統呼叫)時, attach 到這裡的 BPF 程式就會執行。
使用者側需要用到的所有 BPF 型別都定義在 UAPI bpf.h。 本文將主要關注下面兩種能攔截到 socket 操作(例如 TCP connect
、sendmsg
等)的型別:
BPF_PROG_TYPE_SOCK_OPS
:socket operations 事件觸發執行。BPF_PROG_TYPE_SK_MSG
:sendmsg()
系統呼叫觸發執行。
本文將
- 用 C 編寫 eBPF 程式碼
- 用 LLVM Clang 前端來生成 ELF bytecode
- 用 bpftool 將程式碼載入到核心(以及從核心解除安裝)
下面看程式碼實現。
1.2 本文 BPF 程式總體設計
首先建立一個全域性的對映表(map)來記錄所有的 socket 資訊。基於這個 sockmap,編寫兩段 BPF 程式分別完成以下功能:
- 程式一:攔截所有 TCP connection 事件,然後將 socket 資訊儲存到這個 map;
- 程式二:攔截所有
sendmsg()
系統呼叫,然後從 map 中查 詢這個socket 資訊,之後直接將資料重定向到對端。
2 BPF 程式一:監聽 socket 事件,更新 sockmap
2.1 監聽 socket 事件
程式功能:
-
系統中有 socket 操作時(例如 connection establishment、tcp retransmit 等),觸發執行;
- 指定載入位置來實現:
__section("sockops")
- 指定載入位置來實現:
-
執行邏輯:提取 socket 資訊,並以 key & value 形式儲存到 sockmap。
程式碼如下:
__section("sockops") // 載入到 ELF 中的 `sockops` 區域,有 socket operations 時觸發執行
int bpf_sockmap(struct bpf_sock_ops *skops)
{
switch (skops->op) {
case BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB: // 被動建連
case BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB: // 主動建連
if (skops->family == 2) { // AF_INET
bpf_sock_ops_ipv4(skops); // 將 socket 資訊記錄到到 sockmap
}
break;
default:
break;
}
return 0;
}
對於兩端都在本節點的 socket 來說,這段程式碼會執行兩次:
- 源端傳送 SYN 時會產生一個事件,命中 case 2
- 目的端傳送 SYN+ACK 時會產生一個事件,命中 case 1
因此對於每一個成功建連的 socket,sockmap 中會有兩條記錄(key 不同)。
提取 socket 資訊以儲存到 sockmap 是由函式 bpf_sock_ops_ipv4()
完成的,接下 來看下它的實現。
2.2 將 socket 資訊寫入 sockmap
static inline
void bpf_sock_ops_ipv4(struct bpf_sock_ops *skops)
{
struct sock_key key = {};
int ret;
extract_key4_from_ops(skops, &key);
ret = sock_hash_update(skops, &sock_ops_map, &key, BPF_NOEXIST);
if (ret != 0) {
printk("sock_hash_update() failed, ret: %d\n", ret);
}
printk("sockmap: op %d, port %d --> %d\n", skops->op, skops->local_port, bpf_ntohl(skops->remote_port));
}
三個步驟:
- 呼叫
extract_key4_from_ops()
從struct bpf_sock_ops *skops
(socket metadata)中提取 key; - 呼叫
sock_hash_update()
將 key:value 寫入全域性的 sockmapsock_ops_map
,這 個變數定義在我們的標頭檔案中。 - 列印一行日誌,方面我們測試用,後面會看到效果。
2.2.1 從 socket metadata 中提取 sockmap key
map 的型別可以是:
BPF_MAP_TYPE_SOCKMAP
BPF_MAP_TYPE_SOCKHASH
本文用的是第二種,sockmap 定義如下,
struct bpf_map_def __section("maps") sock_ops_map = {
.type = BPF_MAP_TYPE_SOCKHASH,
.key_size = sizeof(struct sock_key),
.value_size = sizeof(int), // 儲存 socket
.max_entries = 65535,
.map_flags = 0,
};
key 定義如下:
struct sock_key {
uint32_t sip4; // 源 IP
uint32_t dip4; // 目的 IP
uint8_t family; // 協議型別
uint8_t pad1; // this padding required for 64bit alignment
uint16_t pad2; // else ebpf kernel verifier rejects loading of the program
uint32_t pad3;
uint32_t sport; // 源埠
uint32_t dport; // 目的埠
} __attribute__((packed));
下面是提取 key 的實現,非常簡單:
static inline
void extract_key4_from_ops(struct bpf_sock_ops *ops, struct sock_key *key)
{
// keep ip and port in network byte order
key->dip4 = ops->remote_ip4;
key->sip4 = ops->local_ip4;
key->family = 1;
// local_port is in host byte order, and remote_port is in network byte order
key->sport = (bpf_htonl(ops->local_port) >> 16);
key->dport = FORCE_READ(ops->remote_port) >> 16;
}
2.2.2 插入 sockmap
sock_hash_update()
將 socket 資訊寫入到 sockmap,這個函式是我們定義的一個宏, 會展開成核心提供的一個 hash update 函式,不再詳細展開。
2.3 小結
至此,第一段程式碼就完成了,它能確保我們攔截到 socket 建連事件,並將 socket 資訊寫入一個全域性的對映表(sockmap)。
3 BPF 程式二:攔截 sendmsg
系統呼叫,socket 重定向
第二段 BPF 程式的功能:
- 攔截所有的
sendmsg
系統呼叫,從訊息中提取 key; - 根據 key 查詢 sockmap,找到這個 socket 的對端,然後繞過 TCP/IP 協議棧,直接將 資料重定向過去。
要完成這個功能,需要:
-
在 socket 發起
sendmsg
系統呼叫時觸發執行,- 指定載入位置來實現:
__section("sk_msg")
- 指定載入位置來實現:
-
關聯到前面已經建立好的 sockmap,因為要去裡面查詢 socket 的對端資訊。
- 透過將 sockmap attach 到 BPF 程式實現:map 中的所有 socket 都會繼承這段程式, 因此其中的任何 socket 觸發 sendmsg 系統呼叫時,都會執行到這段程式碼。
3.1 攔截 sendmsg
系統呼叫
__section("sk_msg") // 載入目標檔案(ELF )中的 `sk_msg` section,`sendmsg` 系統呼叫時觸發執行
int bpf_redir(struct sk_msg_md *msg)
{
struct sock_key key = {};
extract_key4_from_msg(msg, &key);
msg_redirect_hash(msg, &sock_ops_map, &key, BPF_F_INGRESS);
return SK_PASS;
}
當 attach 了這段程式的 socket 上有 sendmsg 系統呼叫時,核心就會執行這段程式碼。它會:
- 從 socket metadata 中提取 key,
- 呼叫
bpf_socket_redirect_hash()
尋找對應的 socket,並根據 flag(BPF_F_INGRESS
), 將資料重定向到 socket 的某個 queue。
3.2 從 socket message 中提取 key
static inline
void extract_key4_from_msg(struct sk_msg_md *msg, struct sock_key *key)
{
key->sip4 = msg->remote_ip4;
key->dip4 = msg->local_ip4;
key->family = 1;
key->dport = (bpf_htonl(msg->local_port) >> 16);
key->sport = FORCE_READ(msg->remote_port) >> 16;
}
3.3 Socket 重定向
msg_redirect_hash()
也是我們定義的一個宏,最終呼叫的是 BPF 內建的輔助函式。
最終需要用的其實是核心輔助函式
bpf_msg_redirect_hash()
,但後者無法直接訪問, 只能透過 UAPI linux/bpf.h 預定義的BPF_FUNC_msg_redirect_hash
來訪問,否則校驗器無法透過。
msg_redirect_hash(msg, &sock_ops_map, &key, BPF_F_INGRESS)
幾個引數:
struct sk_msg_md *msg
:使用者可訪問的待傳送資料的元資訊(metadata)&sock_ops_map
:這個 BPF 程式 attach 到的 sockhash mapkey
:在 map 中索引用的 keyBPF_F_INGRESS
:放到對端的哪個 queue(rx 還是 tx)
4 編譯、載入、執行
bpftool
是一個使用者空間工具,能用來載入 BPF 程式碼到核心、建立和更新 maps,以及收集 BPF 程式和 maps 資訊。其原始碼位於 Linux 核心樹中:tools/bpf/bpftool。
4.1 編譯
用 LLVM Clang frontend 來編譯前面兩段程式,生成目的碼(object code):
$ clang -O2 -g -target bpf -c bpf_sockops.c -o bpf_sockops.o
$ clang -O2 -g -target bpf -c bpf_redir.c -o bpf_redir.o
4.2 載入(load)和 attach sockops
程式
載入到核心
$ sudo bpftool prog load bpf_sockops.o /sys/fs/bpf/bpf_sockops type sockops
- 這條命令將 object 程式碼載入到核心(但還沒 attach 到 hook 點)
- 載入之後的程式碼會 pin 到一個 BPF 虛擬檔案系統 來持久儲存,這樣就能獲得一個指向這個程式的檔案控制代碼(handle)供稍後使用。
- bpftool 會在 ELF 目標檔案中建立我們宣告的 sockmap(
sock_ops_map
變數,定 義在標頭檔案中)。
Attach 到 cgroups
$ sudo bpftool cgroup attach /sys/fs/cgroup/unified/ sock_ops pinned /sys/fs/bpf/bpf_sockops
- 這條命令將載入之後的
sock_ops
程式 attach 到指定的 cgroup, - 這個 cgroup 內的所有程序的所有 sockets,都將會應用這段程式。如果使用的是 cgroupv2 時,systemd 會在
/sys/fs/cgroup/unified
自動建立一個 mount 點。
下面的命令說明 /sys/fs/cgroup/unified/ 確實是 cgroupv2 掛載點:
$ mount | grep cgroup cgroup2 on /sys/fs/cgroup/unified type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate) ...
如果想自定義 cgroupv2 掛載點,可參考 Cracking kubernetes node proxy (aka kube-proxy),其中的第五種方式。
譯註。
檢視 map ID
至此,目的碼已經載入(load)和附著(attach)到 hook 點了,接下來檢視 sock_ops
程式所使用的 map ID,因為後面要用這個 ID 來 attach sk_msg
程式:
MAP_ID=$(sudo bpftool prog show pinned /sys/fs/bpf/bpf_sockops | grep -o -E 'map_ids [0-9]+'| cut -d '' -f2-)
$ sudo bpftool map pin id $MAP_ID /sys/fs/bpf/sock_ops_map
4.3 載入和 attach sk_msg
程式
載入到核心
$ sudo bpftool prog load bpf_redir.o /sys/fs/bpf/bpf_redir \
map name sock_ops_map \
pinned /sys/fs/bpf/sock_ops_map
- 將程式載入到核心
- 將程式 pin 到 BPF 檔案系統的
/sys/fs/bpf/bpf_redir
位置 - 重用已有的 sockmap,指定了 sockmap 的名字為
sock_ops_map
並且檔案路徑為/sys/fs/bpf/sock_ops_map
Attach
將已經載入到核心的 sk_msg
程式 attach 到 sockmap,
$ sudo bpftool prog attach pinned /sys/fs/bpf/bpf_redir msg_verdict pinned /sys/fs/bpf/sock_ops_map
從現在開始,sockmap 內的所有 socket 在 sendmsg
時都將觸發執行這段 BPF 程式碼。
檢視
檢視系統中已經載入的所有 BPF 程式:
$ sudo bpftool prog show
...
38: sock_ops name bpf_sockmap tag d9aec8c151998c9c gpl
loaded_at 2021-01-28T22:52:06+0800 uid 0
xlated 672B jited 388B memlock 4096B map_ids 13
btf_id 20
43: sk_msg name bpf_redir tag 550f6d3cfcae2157 gpl
loaded_at 2021-01-28T22:52:06+0800 uid 0
xlated 224B jited 156B memlock 4096B map_ids 13
btf_id 24
檢視系統中所有的 map,以及 map 詳情:
$ sudo bpftool map show
13: sockhash name sock_ops_map flags 0x0
key 24B value 4B max_entries 65535 memlock 5767168B
# -p/--pretty:人類友好格式列印
$ sudo bpftool -p map show id 13
{
"id": 13,
"type": "sockhash",
"name": "sock_ops_map",
"flags": 0,
"bytes_key": 24,
"bytes_value": 4,
"max_entries": 65535,
"bytes_memlock": 5767168,
"frozen": 0
}
列印 map 內的所有內容:
$ sudo bpftool -p map dump id 13
[{
"key":
["0x7f", "0x00", "0x00", "0x01", "0x7f", "0x00", "0x00", "0x01", "0x01", "0x00", "0x00", "0x00", "0x00", "0x00", "0x00", "0x00", "0x03", "0xe8", "0x00", "0x00", "0xa1", "0x86", "0x00", "0x00"
],
"value": {
"error":"Operation not supported"
}
},{
"key":
["0x7f", "0x00", "0x00", "0x01", "0x7f", "0x00", "0x00", "0x01", "0x01", "0x00", "0x00", "0x00", "0x00", "0x00","0x00", "0x00", "0xa1", "0x86", "0x00", "0x00", "0x03", "0xe8", "0x00", "0x00"
],
"value": {
"error":"Operation not supported"
}
}
]
其中的 error 是因為 sockhash map 不支援從使用者空間獲取 map 內的值(values)。
4.4 測試
在一個視窗中啟動 socat
作為服務端,監聽在 1000 埠:
# start a TCP listener at port 1000, and echo back the received data
$ sudo socat TCP4-LISTEN:1000,fork exec:cat
另一個視窗用 nc
作為客戶端來訪問服務端,建立 socket:
# connect to the local TCP listener at port 1000
$ nc localhost 1000
觀察我們在 BPF 程式碼中列印的日誌:
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
nc-13227 [002] .... 105048.340802: 0: sockmap: op 4, port 50932 --> 1001
nc-13227 [002] ..s1 105048.340811: 0: sockmap: op 5, port 1001 --> 50932
4.5 清理
從 sockmap 中 detach 第二段 BPF 程式,並將其從 BPF 檔案系統中 unpin:
$ sudo bpftool prog detach pinned /sys/fs/bpf/bpf_redir msg_verdict pinned /sys/fs/bpf/sock_ops_map
$ sudo rm /sys/fs/bpf/bpf_redir
當 BPF 檔案系統中某個檔案的 reference count 為零時,該就會自動從 BPF 檔案系統中刪除。
同理,從 cgroups 中 detach 第一段 BPF 程式,並將其從 BPF 檔案系統中 unpin:
$ sudo bpftool cgroup detach /sys/fs/cgroup/unified/ sock_ops pinned /sys/fs/bpf/bpf_sockops
$ sudo rm /sys/fs/bpf/bpf_sockops
最後刪除 sockmaps:
$ sudo rm /sys/fs/bpf/sock_ops_map
5 結束語
本文展示瞭如何利用 sockmap/cgroups BPF 程式加速兩端都在同一臺機器的 socket 的通 信。下一篇 會給出一些效能測試,有興趣可以前往檢視。
最後,希望本文能給大家帶來一些幫助。有任何問題,可以郵件聯絡我們:product@cyral.com
。
附錄:BPF 開發環境搭建
-
原文測試環境:Ubuntu Linux 18.04 with kernel 5.3.0-40-generic.
已經有點老,搭建步驟見 原文附錄。
-
譯文測試環境:Ubuntu Linux 20.04 with kernel 5.8.0-38-generic.
已經用了很久,具體搭建步驟忘了。建議參考 Cilium 開發環境搭建步驟,或自行 google。