如何使用 eBPF 加速雲原生應用程序

近年來,eBPF 的使用量急劇上升。因爲 eBPF 運行在內核態,且基於事件觸發,所以代碼的運行速度極快(不涉及上下文切換)更準確。eBPF 被廣泛用於內核跟蹤(kprobes/tracecing)的可觀測性,此外還被用於一些基於 IP 地址的傳統安全監控和訪問控制不足的環境(例如,在基於容器的環境,如 Kubernetes )中。我們會在後續文章討論如何使用 eBPF 進行 k8s 的 network policy 過濾,以及 APP 無侵入的可觀測性。 

在圖 1 中,可以看到 Linux 內核中的各種鉤子,其中可以鉤住 eBPF 程序以執行。在 linux-master\tools\include\uapi\linux\bpf.h 可以看到所有 sock_ops 相關的監聽事件。

enum {
  BPF_TCP_ESTABLISHED = 1,
  BPF_TCP_SYN_SENT,
  BPF_TCP_SYN_RECV,
  BPF_TCP_FIN_WAIT1,
  ...、

1 使用 eBPF 進行網絡加速

通常,eBPF 程序有兩個部分:

(1) 內核空間組件,其中需要根據某些內核事件進行決策或數據收集,例如 NIC 上的數據包 Rx、生成 shell 的系統調用等。
(2) 用戶空間組件,可以訪問內核代碼共享的數據結構(映射等)中寫入的數據。

我們本次重點解釋的代碼是內核空間組件,我們使用 bpftool 命令行工具將代碼加載到內核中,然後卸載它。

Linux 內核支持不同類型的 eBPF 程序,每個程序都可以附加到內核中可用的特定鉤子(參見圖 1)。當與這些鉤子關聯的事件被觸發時,這些程序將執行,例如,進行諸如 setsockopt() 之類的系統調用,進入數據包緩衝區 DMA 之後的網絡驅動程序鉤子 XDP 等。

enum bpf_prog_type {
  BPF_PROG_TYPE_UNSPEC,
  BPF_PROG_TYPE_SOCKET_FILTER,
  BPF_PROG_TYPE_KPROBE,
  ...
  BPF_PROG_TYPE_XDP,
  BPF_PROG_TYPE_SOCK_OPS,
  BPF_PROG_TYPE_SK_MSG,
  BPF_PROG_TYPE_SK_REUSEPORT,
  ...

所有類型都在 UAPI bpf.h 頭文件中枚舉,其中包含 eBPF 程序所需的面向用戶的定義。 

在這篇博文中,我們對 BPF_PROG_TYPE_SOCK_OPS 和 BPF_PROG_TYPE_SK_MSG 類型的 eBPF 程序感興趣,它們允許我們將 BPF 程序連接到套接字操作。

    圖 2 socket 重定向的整體流程

當發生 TCP 連接事件時,sockops 程序被執行,如上圖紅色框圖;在套接字的 sendmsg 調用時,SK_MSG 將執行套接字數據重定向,如上圖藍色框圖。

1.1 套接字數據重定向 -- 引入 BPF_PROG_TYPE_SK_MSG 類型的 eBPF 程序

sk_msg 程序在用戶態執行 sendmsg 時執行,但必須附加到套接字映射,特別是 BPF_MAP_TYPE_SOCKMAP 或 BPF_MAP_TYPE_SOCKHASH。這些映射是鍵值存儲,其中主鍵是五元組信息(下文詳述),值只能是套接字。一旦 SK_MSG 程序和 MAP 進行綁定,MAP 中的所有套接字都將繼承 SK_MSG 程序。本實例中,sk_msg 的部分邏輯如下:

__section("sk_msg")
int bpf_tcpip_bypass(struct sk_msg_md *msg)
{
 struct sock_key key = {};
 sk_msg_extract4_key(msg, &key);
 msg_redirect_hash(msg, &sock_ops_map, &key, BPF_F_INGRESS);return SK_PASS;
}

因此,當在附加了此程序的套接字上執行 sendmsg 時,內核將執行此程序。然後 bpf_socket_redirect_hash() 根據標誌將數據包重定向到套接字的相應隊列(僅定義 BPF_F_INGRESS 1 用於 消息入口 rx 隊列;  0 意味着轉發消息 tx 隊列)。

1.2 填充 sockmap hash -- 引入 BPF_PROG_TYPE_SOCK_OPS 類型的 eBPF 程序

現在讓我們把注意力轉向如何填充 sk_msg 程序使用的 sockhash 映射。我們希望在發生 TCP 連接事件時填充此 sockhash。現在,它是第二種 eBPF 程序類型 SOCK_OPS,它在 TCP 事件(如連接建立、TCP 重傳等)時被調用, 可以捕獲套接字的詳細信息。

在下面的代碼片段中,我們創建了這個程序,該程序在套接字操作時觸發,並處理主動(源套接字發送 SYN)和被動(目標套接字響應 SYN 的 ACK)TCP 連接的事件。

__section("sockops")
int bpf_sockops_v4(struct bpf_sock_ops *skops)
{
 uint32_t family, op;family = skops->family;op = skops->op;
 switch (op) {case BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB:
 case BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB:
  if (family == 2) { //AF_INET
   bpf_sock_ops_ipv4(skops);
  }
  break;
  ...

eBPF sockops 程序駐留在 ELF 部分 sockops 中,因此代碼使用編譯器部分屬性放置在 sockops 部分中。當 TCP 連接事件發生時,調用 bpf_sock_ops_ipv4 方法來存儲數據結構 sock_ops_map,其定義如下:

struct sock_key {
        uint32_t sip4;
        uint32_t dip4;
        uint8_t  family;
        uint32_t sport;
        uint32_t dport;
} __attribute__((packed));
struct bpf_map_def __section("maps") sock_ops_map = {
        .type           = BPF_MAP_TYPE_SOCKHASH,
        .key_size       = sizeof(struct sock_key),
        .value_size     = sizeof(int),
        .max_entries    = 65535,
        .map_flags      = 0,
};

其中的 sock_key 作爲 sock_ops_map 的 key。因此,對於同一主機上的兩個應用程序之間建立的 TCP 連接,我們存儲源套接字(客戶端)以及目標套接字(服務端)的 key 和 value,key 對應五元組信息,value 就是 socket。

圖 3 BPF_MAP_TYPE_SOCKHASH 存儲的 key-value 數據示意圖

1.3 SK_MSG 程序引用 sock_ops_map 中的套接字

static inline
void sk_msg_extract4_key(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;
}

現在,上面描述的 SK_MSG 程序可以通過 sk_msg_extract4_key 將源 ip 和目的 ip 互換,源端口和目的端口互換,這樣就得到了對端的五元組信息,並以此信息作爲 key,通過 msg_redirect_hash 重定向到新的套接字。下圖虛線部分不再執行。

圖 4 socket 重定向示意圖

總結起來就是:

(1) sockops 負責記錄客戶端和服務端的 socket 映射信息,填充到 sock_ops_map;

(2) sk_msg 引用 sock_ops_map 查找對端 socket,進行轉發。

2 功能驗證

2.1 socket 鏈接時打印建鏈信息

爲了快速驗證 sockhash 映射被填充並被 SK_MSG 程序查找的數據路徑,我們可以使用 SOCAT 啓動 TCP 偵聽器並使用 netcat 發送 TCP 連接請求。SOCK_OPS 程序在獲得 TCP 連接事件時將打印日誌,可以從內核的跟蹤文件中查找 trace_pipe:

# start a TCP listener at port 1004, and echo back the received data
sudo socat TCP4-LISTEN:1004,fork exec:cat
# connect to the local TCP listener at port 1004
nc localhost 1004
sudo cat /sys/kernel/debug/tracing/trace_pipe
Output:
nc-212660  <<< ipv4 op = 4, port 57642 --> 1004 //客戶端主動請求鏈接
nc-212660  <<< ipv4 op = 5, port 1004 --> 57642  //客戶端收到服務端的鏈接信息

2.2 socket 轉發時,繞過內核(源代碼功能)

nc-212660 #1 bpf_tcpip_bypass before:src port:57642,dst port:1004
nc-212660 #2 bpf_tcpip_bypass transfer:src port:1004,dst port:57642
socat-212661 #1 bpf_tcpip_bypass before:src port:1004,dst port:57642
socat-212661 #2 bpf_tcpip_bypass transfer:src port:57642,dst port:1004

sk_msg 的 sk_msg_extract4_key 處理,從 57642->1004 的轉發 socket,找到了 1004->57642 的 socket 並將消息掛載到該 socket 的處理隊列,省去了原有 L4 層以下的網絡協議棧的的轉發(如圖 3 所示)。原文對 socket 轉發繞過 tcp/ip 內核底層協議棧做了多種性能對比, 這裏展示的是禁用 Nagle 算法的對比。

圖 5 吞吐量:TCP/IP(禁用 Nagle 算法)與使用 eBPF 繞過 TCP/IP

2.3 客戶端 socket 轉發時,直接 DROP 對特定端口的訪問

修改 bpf_tcpip_bypass 函數,假設增加對端口 1000 的拒絕訪問。這裏爲了驗證功能,沒有指明 ingress 的 label,實際 k8s 配置 policy 時,會指定 ingress 的 label,match 的入口請求才會 drop。

int bpf_tcpip_bypass(struct sk_msg_md *msg)
{
 struct sock_key key = {};
 int dstport=bpf_ntohl((msg->remote_port) >> 16)>>16;
 //增加對端口1000的拒絕訪問
 if ( dstport == 1000 )
 {
  printk("#bpf_tcpip_bypass sk_drop:src port:%d,dst port:%d\n",msg->local_port,dstport);
  return SK_DROP;
 }
 printk("#1 bpf_tcpip_bypass before:src port:%u,dst port:%u\n",msg->local_port,bpf_ntohl((msg->remote_port) >> 16)>>16);
 sk_msg_extract4_key(msg, &key);
 int srcport=bpf_ntohl(key.sport)>>16;
 printk("#2 bpf_tcpip_bypass transfer:src port:%u,dst port:%u\n",srcport,bpf_ntohl(key.dport)>>16);
 msg_redirect_hash(msg, &sock_ops_map, &key, BPF_F_INGRESS);return SK_PASS;
}

客戶端嘗試發送內容

nc localhost 1000
123

看到客戶端輸出如下信息:

nc-219949 <<< ipv4 op = 4, port 40046 --> 1000
nc-219949 <<< ipv4 op = 5, port 1000 --> 40046
nc-219949 #bpf_tcpip_bypass sk_drop:src port:40046,dst port:1000

在客戶端側,對目的端口 1000 進行了 drop 處理,

圖 6 socket DROP 示意圖

eBPF 代碼、加載和卸載 eBPF 程序、安裝 bpftool 的方法可以參考 https://github.com/grafanafans/club/tree/master/ebpf 執行。

參考:

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

2、原文代碼 https://github.com/cyralinc/os-eBPF

3、本文代碼 https://github.com/grafanafans/club/tree/master/ebpf

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/bqymmJpDyrcD01fOqMOBmA