轉載 ebpf sockmap/redirection 提升 socket 效能(2020)

codestacklinuxer發表於2024-04-11

利用 ebpf sockmap/redirection 提升 socket 效能(2020)

轉自:https://arthurchiao.art/blog/socket-acceleration-with-ebpf-zh/

譯者序

本文翻譯自 2020 年的一篇英文部落格 How to use eBPF for accelerating Cloud Native applications

原文標題非常寬泛,但內容其實很技術:展示瞭如何編寫簡單的 BPF 程式做 socket level 重定向(redirection)。對於源和目的端都在同一臺機器的應用來說,這樣可以 繞過整個 TCP/IP 協議棧,直接將資料傳送到 socket 對端。效果如右下圖(懶得畫圖 ,直接從 Cilium 分享 截個圖,所以其中 Cilium 字樣,但本文不需要 Cilium):

轉載 ebpf sockmap/redirection 提升 socket 效能(2020)

實現這個功能依賴兩個東西:

  1. sockmap:這是一個儲存 socket 資訊的對映表。作用:

    1. 一段 BPF 程式監聽所有的核心 socket 事件,並將新建的 socket 記錄到這個 map;
    2. 另一段 BPF 程式攔截所有 sendmsg 系統呼叫,然後去 map 裡查詢 socket 對端,之後 呼叫 BPF 函式繞過 TCP/IP 協議棧,直接將資料傳送到對端的 socket queue。
  2. 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 重定向
  • 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 程式由兩部分構成:

  1. 核心空間部分:核心事件觸發執行,例如網路卡收到一個包、系統呼叫建立了一個 shell 程序等等;
  2. 使用者空間部分:透過某種共享資料的方式(例如 BPF maps)來讀取核心部分產生的資料;

本文主要關注核心部分。核心支援不同型別的 eBPF 程式,它們各自可以 attach 到不同的 hook 點,如下圖所示:

轉載 ebpf sockmap/redirection 提升 socket 效能(2020)

當核心中觸發了與這些 hook 相關的事件(例如,發生 setsockopt()系統呼叫)時, attach 到這裡的 BPF 程式就會執行。

使用者側需要用到的所有 BPF 型別都定義在 UAPI bpf.h。 本文將主要關注下面兩種能攔截到 socket 操作(例如 TCP connectsendmsg 等)的型別:

  • BPF_PROG_TYPE_SOCK_OPS:socket operations 事件觸發執行。
  • BPF_PROG_TYPE_SK_MSGsendmsg() 系統呼叫觸發執行。

本文將

  • 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 事件

程式功能:

  1. 系統中有 socket 操作時(例如 connection establishment、tcp retransmit 等),觸發執行;

    • 指定載入位置來實現__section("sockops")
  2. 執行邏輯:提取 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));
}

三個步驟:

  1. 呼叫 extract_key4_from_ops()struct bpf_sock_ops *skops(socket metadata)中提取 key;
  2. 呼叫 sock_hash_update() 將 key:value 寫入全域性的 sockmap sock_ops_map,這 個變數定義在我們的標頭檔案中。
  3. 列印一行日誌,方面我們測試用,後面會看到效果。

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 程式的功能:

  1. 攔截所有的 sendmsg 系統呼叫,從訊息中提取 key;
  2. 根據 key 查詢 sockmap,找到這個 socket 的對端,然後繞過 TCP/IP 協議棧,直接將 資料重定向過去。

要完成這個功能,需要:

  1. 在 socket 發起 sendmsg 系統呼叫時觸發執行

    • 指定載入位置來實現__section("sk_msg")
  2. 關聯到前面已經建立好的 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 系統呼叫時,核心就會執行這段程式碼。它會:

  1. 從 socket metadata 中提取 key,
  2. 呼叫 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 map
  • key:在 map 中索引用的 key
  • BPF_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 目標檔案中建立我們宣告的 sockmapsock_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。

相關文章