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


更多奇技淫巧歡迎訂閱博客:https://fuckcloudnative.io

譯者序

本文翻譯自 2020 年的一篇英文博客 How to use eBPF for accelerating Cloud Native applications[1]

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

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

  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 練手。譯文所用的完整代碼[3], 原文用的完整代碼[4]

由於譯者水平有限,本文不免存在遺漏或錯誤之處。如有疑問,請查閱原文。

以下是譯文。

很多用戶基於我們提供的服務來構建實時應用(real time applications),這些應用對性能 有着嚴格的要求,因而促使我們不斷探索各種提升性能的方式,eBPF 就是嘗試之一 ,用於加速應用之間的通信。由於這方面資料尚少,因此我們整理成兩篇文章分享給大家:本篇講實現,下一篇[5] 是一些性能測試和問題討論。

1. 引言

BPF 基礎

通常情況下,eBPF 程序由兩部分構成:

  1. 內核空間部分: 內核事件觸發執行,例如網卡收到一個包、系統調用創建了一個 shell 進程等等;
  2. 用戶空間部分:通過某種 共享數據的方式(例如 BPF maps)來 讀取內核部分產生的數據;

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

當內核中觸發了與這些 hook 相關的事件(例如,發生 setsockopt()系統調用)時, attach 到這裏的 BPF 程序就會執行。

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

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

本文將

  • C 編寫 eBPF 代碼
  • 用 LLVM Clang 前端來生成 ELF bytecode
  • bpftool [7] 將代碼加載到內核(以及從內核卸載)

下面看代碼實現。

本文 BPF 程序總體設計

首先創建一個全局的映射表(map)來記錄所有的 socket 信息。基於這個 sockmap,編寫兩段 BPF 程序分別完成以下功能:

  • 程序一:攔截所有 TCP connection 事件,然後將 socket 信息存儲到這個 map;
  • 程序二:攔截所有 sendmsg() 系統調用,然後從 map 中查 詢這個 socket 信息,之後直接將數據 重定向到對端

2. BPF 程序一:監聽 socket 事件,更新 sockmap

監聽 socket 事件

程序功能:

  1. 系統中有 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() 完成的,接下 來看下它的實現。

    將 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. 打印一行日誌,方面我們測試用,後面會看到效果。

    從 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;
    }

    插入 sockmap

    sock_hash_update() 將 socket 信息寫入到 sockmap,這個函數是我們定義的一個宏, 會展開成內核提供的一個 hash update 函數,不再詳細展開。

    小結

    至此,第一段代碼就完成了,它能確保我們攔截到 socket 建連事件,並將 socket 信息寫入一個全局的映射表(sockmap)。

    3. BPF 程序二:攔截 sendmsg 系統調用,socket 重定向

    第二段 BPF 程序的功能:

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

    要完成這個功能,需要:

    1. 在 socket 發起 sendmsg 系統調用時 觸發執行
    • 指定加載位置來實現__section("sk_msg")
  • 關聯到前面已經創建好的 sockmap,因爲要去裏面查詢 socket 的對端信息。
    • 通過將 sockmap attach 到 BPF 程序實現:map 中的所有 socket 都會繼承這段程序, 因此其中的任何 socket 觸發 sendmsg 系統調用時,都會執行到這段代碼。

    攔截 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。

    從 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;
    }

    Socket 重定向

    msg_redirect_hash() 也是我們定義的一個宏,最終調用的是 BPF 內置的輔助函數。

    最終需要用的其實是內核輔助函數 bpf_msg_redirect_hash(),但後者無法直接訪問, 只能通過 UAPI linux/bpf.h[8] 預定義的 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[9]

    編譯

    用 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

    加載(load)和 attach sockops 程序

    加載到內核

    $ sudo bpftool prog load bpf_sockops.o /sys/fs/bpf/bpf_sockops type sockops
    • 這條命令將 object 代碼加載到內核(但還沒 attach 到 hook 點)
    • 加載之後的代碼會 pin 到一個 BPF 虛擬文件系統 [10] 來持久存儲,這樣就能獲得一個 指向這個程序的文件句柄(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 [11] 時,systemd 會在 /sys/fs/cgroup/unified 自動創建一個 mount 點。

    查看 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

    加載和 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)。

    測試

    在一個窗口中啓動 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

    清理

    從 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[12] 爲零時,該就會自動從 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

    結束語

    本文展示瞭如何利用 sockmap/cgroups BPF 程序加速兩端都在同一臺機器的 socket 的通信。下一篇[13] 會給出一些性能測試,有興趣可以前往查看。

    最後,希望本文能給大家帶來一些幫助。有任何問題,可以郵件聯繫我們:[email protected]

    附錄:BPF 開發環境搭建

    • 原文測試環境:Ubuntu Linux 18.04 with kernel 5.3.0-40-generic.

      已經有點老,搭建步驟見 原文附錄[14]

    • 譯文測試環境:Ubuntu Linux 20.04 with kernel 5.8.0-38-generic.

      已經用了很久,具體搭建步驟忘了。建議參考 Cilium 開發環境搭建步驟,或自行 google。

    參考資料

    [1]

    How to use eBPF for accelerating Cloud Native applications: https://cyral.com/blog/how-to-ebpf-accelerating-cloud-native/

    [2]

    Cilium 分享: https://www.slideshare.net/ThomasGraf5/accelerating-envoy-and-istio-with-cilium-and-the-linux-kernel

    [3]

    完整代碼: https://github.com/ArthurChiao/socket-acceleration-with-ebpf

    [4]

    完整代碼: https://github.com/cyralinc/os-eBPF/

    [5]

    下一篇: https://cyral.com/blog/lessons-using-ebpf-accelerating-cloud-native/

    [6]

    bpf.h: https://github.com/torvalds/linux/blob/master/include/uapi/linux/bpf.h

    [7]

    bpftool: https://manpages.ubuntu.com/manpages/focal/man8/bpftool-prog.8.html

    [8]

    linux/bpf.h: https://github.com/torvalds/linux/blob/master/include/uapi/linux/bpf.h

    [9]

    tools/bpf/bpftool: https://github.com/torvalds/linux/tree/master/tools/bpf/bpftool

    [10]

    BPF 虛擬文件系統: https://lwn.net/Articles/664688/

    [11]

    cgroupv2: http://man7.org/conf/osseu2018/cgroups_v2-OSS.eu-2018-Kerrisk.pdf

    [12]

    reference count: https://facebookmicrosites.github.io/bpf/blog/2018/08/31/object-lifetime.html

    [13]

    下一篇: https://cyral.com/blog/lessons-using-ebpf-accelerating-cloud-native/

    [14]

    原文附錄: https://cyral.com/blog/how-to-ebpf-accelerating-cloud-native/


    原文鏈接:https://arthurchiao.art/blog/socket-acceleration-with-ebpf-zh/



    你可能還喜歡

    點擊下方圖片即可閱讀

    k8s集羣內的節點,可能沒你想象的那麼健壯!(磁盤管理篇)

    雲原生是一種信仰 🤘



    碼關注公衆號

    後臺回覆◉k8s◉獲取史上最方便快捷的 Kubernetes 高可用部署工具,只需一條命令,連 ssh 都不需要!



    點擊 "閱讀原文" 獲取更好的閱讀體驗!

    
             
             
             
    ❤️ 給個 「在看」 ,是對我最大的支持❤️

    本文分享自微信公衆號 - 雲原生實驗室(cloud_native_yang)。
    如有侵權,請聯繫 [email protected] 刪除。
    本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

    發表評論
    所有評論
    還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
    相關文章