Cilium 基於 eBPF 實現 socket 加速

隨着雲原生的不斷髮展,越來越多的應用部署在雲上。其中有些應用對實時性要求非常嚴苛,這使得我們必須提升這些應用性能,達到更快的服務速度。

01 場景

爲了達到更快的服務速度,一個場景是:當兩個互相調用的應用部署在同一個節點上的時候,每次請求和返回都需要經過 socket 層、TCP/IP 協議棧、數據鏈路層、物理層。如果請求和返回繞過 TCP/IP 協議棧,直接在 socket 層將數據包重定向到對端 socket,那將大大減少發送數據包耗時,從而加快服務速度。基於這個思路,eBPF 技術通過映射存儲 socket 信息,利用幫助函數實現了將數據包重定向到對端 socket 層的能力。Cilium 正是基於 eBPF 這個能力,實現了 socket 層加速效果。

02 架構

Cilium 整體架構如下:

圖源:https://docs.cilium.io/en/stable/overview/component-overview/

Cilium 在 daemon 組件實現 socket 層加速,daemon 組件會在集羣中每個節點都啓動一個 pod,所以 socket 加速效果會應用在每個節點上

03 能力概述

Cilium 在處理源端和目標端在同一個節點的時候,可以在 socket 層將流量直接重定向到目標端 socket,這樣直接繞過了整個 TCP/IP 協議棧,進而達到加速的效果,如下圖所示。

圖源:https://www.slideshare.net/ThomasGraf5/accelerating-envoy-and-istio-with-cilium-and-the-linux-kernel

04 實現原理

4.1. 原理概述

Cilium 使用以下 eBPF 程序和映射實現上述能力:

bpf_sockmap 需要 attach 到 cgroup 上,在 Cilium 中會被 attach 到 / run/cilium/cgroupv2,所以 bpf_sockmap 可以作用到系統中所有屬於該 cgroup 的進程。

4.2. 代碼實現

以下代碼基於 cilium/cilium v1.13 分支。Cilium 使用 c 語言編寫 eBPF 代碼,並通過 go 語言 compile、load 和 attach eBPF 代碼。

4.2.1. eBPF 映射

cilium_sock_ops: 該映射使用 sock_key 作爲 key,sock_key 存儲源 IP、目標 IP、協議、源端口、目標端口,即 socket 五元組。

bpf/sockops/bpf_sockops.h

struct sock_key {
    union {
        struct {
            __u32       sip4;
            __u32       pad1;
            __u32       pad2;
            __u32       pad3;
        };
        union v6addr    sip6;
    };
    union {
        struct {
            __u32       dip4;
            __u32       pad4;
            __u32       pad5;
            __u32       pad6;
        };
        union v6addr    dip6;
    };
    __u8 family;
    __u8 pad7;
    __u16 pad8;
    __u32 sport;
    __u32 dport;
} __packed;
 
struct {
    __uint(type, BPF_MAP_TYPE_SOCKHASH);
    __type(key, struct sock_key);
    __type(value, int);
    __uint(pinning, LIBBPF_PIN_BY_NAME);
    __uint(max_entries, SOCKOPS_MAP_SIZE);
} SOCK_OPS_MAP __section_maps_btf;

4.2.2. eBPF 程序

bpf_sockmap: BPF_PROG_TYPE_SOCK_OPS 類型的程序和其他類型程序不同,它會在多個地方執行,參數 skops->op 可獲取當前程序執行的地方。這裏會在主動建立連接、被動建立連接完成,並且協議族是 AF_INET6(TCP/IPv6)、AF_INET(TCP/IPv4)時,進入 bpf_sock_ops_ipv4 處理 socket。bpf_sock_ops_ipv6 會判斷目標 IPV4 不爲空,也會進入 bpf_sock_ops_ipv4 做進一步處理。

bpf/sockops/bpf_sockops.c

__section("sockops")
int cil_sockops(struct bpf_sock_ops *skops)
{
    __u32 family, op;
 
    family = skops->family;
    op = skops->op;
 
    switch (op) {
    case BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB:
    case BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB:
#ifdef ENABLE_IPV6
        if (family == AF_INET6)
            bpf_sock_ops_ipv6(skops);
#endif
#ifdef ENABLE_IPV4
        if (family == AF_INET)
            bpf_sock_ops_ipv4(skops);
#endif
        break;
    default:
        break;
    }
 
    return 0;
}

bpf_sock_ops_ipv4: 這裏首先調用 sk_extract4_key 提取出 key,也就是 cilium_sock_ops 的 key;調用 sk_lb4_key 提取出 lb4_key,調用 lb4_lookup_service,根據 lb4_key 查詢映射:cilium_lb4_services_v2 獲取 L4 負載信息,如果找到說明目標是 L4 負載地址,直接 return,結束調用,向下使用 L4/L3 協議棧處理;調用 lookup_ip4_remote_endpoint 根據目標 IP 查詢映射:cilium_ipcache,該映射存儲了所有網絡信息和身份信息的對應關係,找到目標 IP 的身份 ID;調用 policy_sk_egress 進行策略裁決;調用 redirect_to_proxy 判斷是否要重定向到 proxy(verdict > 0),如果是的話,將 key 的源 IP、源端口換成 proxy 的地址,調用 bpf 幫助函數 bpf_sock_hash_update 將 key 存儲到 cilium_sock_ops 中;如果不是的話,調用__lookup_ip4_endpoint,根據目標 IP 查詢映射:cilium_lxc,該映射存儲了所有本地的 POD 信息、主機信息,如果找到,說明是本機內部訪問,調用 bpf 幫助函數 bpf_sock_hash_update 將 key 存儲到 cilium_sock_ops 中。

bpf/sockops/bpf_sockops.c

static inline void bpf_sock_ops_ipv4(struct bpf_sock_ops *skops)
{
    struct lb4_key lb4_key = {};
    __u32 dip4, dport, dst_id = 0;
    struct endpoint_info *exists;
    struct lb4_service *svc;
    struct sock_key key = {};
    int verdict;
 
    sk_extract4_key(skops, &key);
 
    /* If endpoint a service use L4/L3 stack for now. These can be
     * pulled in as needed.
     */
    sk_lb4_key(&lb4_key, &key);
    svc = lb4_lookup_service(&lb4_key, true, true);
    if (svc)
        return;
 
    /* Policy lookup required to learn proxy port */
    if (1) {
        struct remote_endpoint_info *info;
 
        info = lookup_ip4_remote_endpoint(key.dip4);
        if (info != NULL && info->sec_label)
            dst_id = info->sec_label;
        else
            dst_id = WORLD_ID;
    }
 
    verdict = policy_sk_egress(dst_id, key.sip4, (__u16)key.dport);
    if (redirect_to_proxy(verdict)) {
        __be32 host_ip = IPV4_GATEWAY;
 
        key.dip4 = key.sip4;
        key.dport = key.sport;
        key.sip4 = host_ip;
        key.sport = verdict;
 
        sock_hash_update(skops, &SOCK_OPS_MAP, &key, BPF_NOEXIST);
        return;
    }
 
    /* Lookup IPv4 address, this will return a match if:
     * - The destination IP address belongs to the local endpoint manage
     *   by Cilium.
     * - The destination IP address is an IP address associated with the
     *   host itself.
     * Then because these are local IPs that have passed LB/Policy/NAT
     * blocks redirect directly to socket.
     */
    exists = __lookup_ip4_endpoint(key.dip4);
    if (!exists)
        return;
 
    dip4 = key.dip4;
    dport = key.dport;
    key.dip4 = key.sip4;
    key.dport = key.sport;
    key.sip4 = dip4;
    key.sport = dport;
 
    sock_hash_update(skops, &SOCK_OPS_MAP, &key, BPF_NOEXIST);
}

bpf_redir_proxy: BPF_PROG_TYPE_SK_MSG 類型程序會在 socket 上調用 sendmsg 時執行,這裏就是執行 bpf_redir_proxy。該程序首先調用 sk_extract4_key 提取出 key,也就是 cilium_sock_ops 的 key;調用  lookup_ip4_remote_endpoint 根據目標 IP 查詢映射:cilium_ipcache,該映射存儲了所有網絡信息和身份信息的對應關係,找到目標 IP 的身份 ID;調用 policy_sk_egress 進行策略裁決;如果裁決通過,則調用 bpf 幫助函數 bpf_msg_redirect_hash,該函數通過傳入的 key 查詢映射 cilium_sock_ops,獲取對端 socket 信息,進而將消息重定向到對端 socket 的 ingress 方向,完成 socket 層的重定向。

bpf/sockops/bpf_redir.c

__section("sk_msg")
int cil_redir_proxy(struct sk_msg_md *msg)
{
    struct remote_endpoint_info *info;
    __u64 flags = BPF_F_INGRESS;
    struct sock_key key = {};
    __u32 dst_id = 0;
    int verdict;
 
    sk_msg_extract4_key(msg, &key);
 
    /* Currently, pulling dstIP out of endpoint
     * tables. This can be simplified by caching this information with the
     * socket to avoid extra overhead. This would require the agent though
     * to flush the sock ops map on policy changes.
     */
    info = lookup_ip4_remote_endpoint(key.dip4);
    if (info != NULL && info->sec_label)
        dst_id = info->sec_label;
    else
        dst_id = WORLD_ID;
 
    verdict = policy_sk_egress(dst_id, key.sip4, (__u16)key.dport);
    if (verdict >= 0)
        msg_redirect_hash(msg, &SOCK_OPS_MAP, &key, flags);
    return SK_PASS;
}

4.2.3. compile、load 和 attach eBPF 程序

Cilium 使用 go 語言,調用外部命令 clang llc bpftool 進行 compile、load 和 attach。

當 Cilium 配置 sockops-enable: "true",且內核支持 BPF Sock ops,daemon 啓動時進行 init 操作的時候進行 compile、load 和 attach,代碼如下:

daemon/cmd/daemon.go

......
        if option.Config.SockopsEnable {
            eppolicymap.CreateEPPolicyMap()
            if err := sockops.SockmapEnable(); err != nil {
                return fmt.Errorf("failed to enable Sockmap: %w", err)
            } else if err := sockops.SkmsgEnable(); err != nil {
                return fmt.Errorf("failed to enable Sockmsg: %w", err)
            } else {
                sockmap.SockmapCreate()
            }
        }
......

sockops.SockmapEnable: 首先調用 clang... | llc ... 將 bpf_sockops.c 編譯成 bpf_sockops.o;然後將 bpf_sockops.o 加載到內核,加載之後 bpf_sockops.o 會 pin 到 / sys/fs/bpf/bpf_sockops(默認),再調用 bpftool cgroup attach... 將 / sys/fs/bpf/bpf_sockops attach 到 / run/cilium/cgroupv2(默認)cgroup 上,最後將 cilium_sock_ops pin 到 / sys/fs/bpf/tc/globals/cilium_sock_ops。

pkg/sockops/sockops.go

func SockmapEnable() error {
    err := bpfCompileProg(cSockops, oSockops)
    if err != nil {
        return err
    }
    progID, mapID, err := bpfLoadAttachProg(oSockops, eSockops, sockMap)
    if err != nil {
        return err
    }
    log.Infof("Sockmap Enabled: bpf_sockops prog_id %d and map_id %d loaded", progID, mapID)
    return nil
}

sockops.SkmsgEnable: 首先調用 clang... | llc ... 將 bpf_redir.c 編譯成 bpf_redir.o;然後將 bpf_redir.o 加載到內核,加載之後 bpf_redir.o 會 pin 到 / sys/fs/bpf/bpf_redir(默認),根據 bpf_redir 調用 bpftool prog show pinned... 獲取 bpf_redir 程序 ID,調用 bpftool map show 獲取 bpf_sockops 所使用的的映射 ID,最後調用 bpftool prog attach... 根據程序 ID、映射 ID 將 bpf_redir attach 到 cilium_sock_ops。

pkg/sockops/sockops.go

func SkmsgEnable() error {
    err := bpfCompileProg(cIPC, oIPC)
    if err != nil {
        return err
    }
 
    err = bpfLoadMapProg(oIPC, eIPC)
    if err != nil {
        return err
    }
    log.Info("Sockmsg Enabled, bpf_redir loaded")
    return nil
}

4.2.4. 總結

綜上所述,使用 cilium_sock_ops 映射做 socket 信息存儲載體,通過 bpf_sockmap 程序攔截並存儲 socket 信息,通過 bpf_redir_proxy 調用 bpf_msg_redirect_hash 實現 socket 重定向。通過 Cilium daemon 組件,將 eBPF 程序和映射 load 到內核中,並 attach 到 / run/cilium/cgroupv2 上,實現同一節點 socket 通訊加速,由於加速的範圍是基於 cgroup 監聽的所有主機上的 socket,所以只要是同一節點上的 socket 之間通信都是可以完成加速的

05 加速效果

圖源:https://cilium.io/blog/2019/02/12/cilium-14/

如上圖展示了,使用 eBPF socket 加速(藍色)之後,請求數 / s 和吞吐量都成倍增加。

圖源:https://cilium.io/blog/2019/02/12/cilium-14/

如上圖展示了,eBPF socket 加速(藍色)和 TCP/IP 棧延遲對比,eBPF socket 加速之後的性能優於常規 TCP/IP 棧。

接下來看看當發送和接收多種長度的消息時加速效果對比,深入分析加速原理:

圖源:https://cyral.com/blog/lessons-using-eBPF-accelerating-cloud-native/

如上圖展示了,使用 eBPF socket 加速之後,吞吐量和發送消息大小呈線性關係。這是因爲當應用程序發送較大的消息時,幾乎沒有額外的開銷,但是當發送消息比較小時,使用 TCP/IP 協議棧,反而吞吐量會大於 eBPF socket 加速之後的吞吐量,這是由於 TCP/IP 棧默認開啓了 Nagle 算法。Nagle 的算法是用來解決小數據包在慢速網絡中氾濫導致擁塞的問題,在該算法中只要有一個 TCP 段在小於 TCP MSS 大小的情況下未被確認,就會進行批處理傳輸數據操作。這種批處理導致一次傳輸更多的數據並分攤開銷,所以能超過 eBPF socket 加速之後吞吐量。但是隨着發送消息越來越大,超過 MSS,TCP/IP 棧就會失去其批處理優勢,在這些大數據包發送大小下,eBPF socket 加速憑藉其低開銷遠遠超過啓用 Nagle 算法的 TCP/IP 棧的吞吐量。

圖源:https://cyral.com/blog/lessons-using-eBPF-accelerating-cloud-native/

如上圖展示了,在禁用 Nagle 算法的情況下,與 eBPF socket 加速相比,常規 TCP 的吞吐量增益完全消失了。TCP/IP 棧和 eBPF socket 加速的性能都按預期線性增加,由於 eBPF 的每次發送調用的成本開銷是固定的,所以 eBPF 具有比常規 TCP/IP 更大的斜率。對於較大的發送消息大小和較小的 TCP MSS,這種性能差距更爲明顯。

圖源:https://cyral.com/blog/lessons-using-eBPF-accelerating-cloud-native/

如上圖展示了,eBPF socket 加速和 TCP/IP 棧延遲對比,eBPF socket 加速優於常規 TCP/IP 棧。性能優於常規 TCP/IP 棧近 50%。與 TCP/IP 棧相比,eBPF 通過將數據包從源套接字的傳輸隊列重定向到目標套接字的接收隊列,從而消除了任何協議級別的開銷(慢啓動、擁塞避免、流量控制等)。此外,請求消息大小的大小對延遲沒有影響。

參考資料:

  1. https://docs.cilium.io/en/stable/

  2. https://cyral.com/blog/how-to-eBPF-accelerating-cloud-native/

  3. https://cyral.com/blog/lessons-using-eBPF-accelerating-cloud-native/

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

  5. https://tools.ietf.org/html/rfc896

  6. https://cilium.io/blog/2019/02/12/cilium-14/

 本文作者 

宋朋飛

現任「DaoCloud 道客」 後端開發工程師

新一代雲原生操作系統底座 --DCE 5.0 社區版: https://docs.daocloud.io/download/dce5/

任何組織、機構和個人,都能免費體驗企業級雲原生性能

DaoCloud 公司簡介

「DaoCloud 道客」雲原生領域的創新領導者,成立於 2014 年底,擁有自主知識產權的核心技術,致力於打造開放的雲操作系統爲企業數字化轉型賦能。產品能力覆蓋雲原生應用的開發、交付、運維全生命週期,並提供公有云、私有云和混合雲等多種交付方式。成立迄今,公司已在金融科技、先進製造、智能汽車、零售網點、城市大腦等多個領域深耕,標杆客戶包括交通銀行、浦發銀行、上汽集團、東風汽車、海爾集團、屈臣氏、金拱門(麥當勞)等。目前,公司已完成了 D 輪超億元融資,被譽爲科技領域準獨角獸企業。公司在北京、南京、武漢、深圳、成都設立多家分公司及合資公司,總員工人數超過 350 人,是上海市高新技術企業、上海市 “科技小巨人” 企業和上海市 “專精特新” 企業,併入選了科創板培育企業名單。

網址:www.daocloud.io

郵件:info@daocloud.io

電話:400 002 6898

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