KubeCon 2021|使用 eBPF 代替 iptables 優化服務網格數據面性能

語言: CN / TW / HK

作者簡介

  劉旭

騰訊雲高級工程師,專注容器雲原生領域,有多年大規模 Kubernetes 集羣管理及微服務治理經驗,現負責騰訊雲服務網格 TCM 數據面產品架構設計和研發工作。

引言

目前以 Istio[1] 為代表的服務網格普遍使用 Sidecar 架構,並使用 iptables 將流量劫持到 Sidecar 代理,優點是對應用程序無侵入,但是 Sidecar 代理會增加請求時延和資源佔用。

性能一直是用户十分關心的一個點,也是用户評估是否使用服務網格產品的關鍵因素,騰訊雲 TCM 團隊一直致力於優化服務網格性能,上週我們在 KubeCon 分享了 使用 eBPF 代替 iptables 優化服務網格數據面性能的方案

iptables 實現流量劫持

首先看一下當前社區使用的基於 iptables 的流量劫持方案,下圖是一個 Pod 的創建過程,sidecar injector 會向 Pod 中注入兩個容器,istio-init 和 istio-proxy。

  • istio-init 是一個 init container,負責創建流量劫持相關的 iptables 規則,在創建完成後會退出;

  • istio-proxy 中運行着 envoy,負責代理 Pod 的網絡流量,iptables 會將請求劫持到 istio-proxy 處理。

下圖展示了 iptables 完成流量劫持的整個過程,這裏簡單説明下,感興趣的同學可以查看[2]

  • Inboundiptables 將入流量重定向到 15006 端口,也就是 envoy 的 VirtualInboundListener,envoy 會根據請求的原始目的地址轉發到應用程序的指定端口;

  • Outboundiptables 將出流量重定向到 15001 端口,也就是 envoy 的 VirtualOutboundListener,envoy 會根據請求的原始目的地址以及 Host URL 等信息路由到指定後端。

eBPF 實現流量劫持

eBPF(extended Berkeley Packet Filter) 是一種可以在 Linux 內核中運行用户編寫的程序,而不需要修改內核代碼或加載內核模塊的技術,目前被廣泛用於網絡、安全、監控等領域。在 Kubernetes 社區最早也是最有影響的基於 eBPF 項目是 Cilium[4],Cilium 使用 eBPF 代替 iptables 優化 Service 性能。

Inbound

首先來看一下對入流量的劫持,對入流量的劫持主要使用 eBPF 程序 hook bind 系統調用完成。

eBPF 程序會劫持 bind 系統調用並修改地址,例如應用程序 bind 0.0.0.0:80 會被修改為 127.0.0.1:80,應用程序還有可能 bind ipv6 的地址,所以這裏有兩個 eBPF 程序分別處理 ipv4 和 ipv6 的 bind。

和 iptables 不同,iptables 可以針對每個 netns 單獨設置規則,eBPF 程序 attach 到指定 hook 點後,會對整個系統都生效,例如 attach 到 bind 系統調用後,所有 Pod 內以及節點上進程調用 bind 都會觸發 eBPF 程序,我們需要區分哪些調用是來自需要由 eBPF 完成流量劫持的 Pod。

在 K8s 中,除了 hostnetwork 的情況,每個 Pod 都有獨立的 netns,而每個 netns 都有唯一的 cookie,因此我們將需要使用 eBPF 完成流量劫持的 Pod 對應的 netns cookie 保存在 cookie_map 中,eBPF 程序通過判斷當前 socket 的 netns cookie 是否在 cookie_map 中來決定是否修改 bind 地址。

修改應用程序的 bind 地址後,還需要下發 pod_ip:80 listener 配置到 envoy,pod_ip:80 listener 會將請求轉發到 127.0.0.1:80 也就是應用程序監聽的地址,這樣就實現了對入流量的劫持。但是這裏有一個問題,由於 istio 使用 istio-proxy 用户啟動 envoy,默認情況下非 root 用户不能 bind 1024 以下的特權端口,我們通過 istio-init 修改內核參數 sysctl net.ipv4.ip_unprivileged_port_start=0 解決了這個問題。

對比 iptables 和 eBPF 對入流量的劫持,iptables 方案每個包都需要 conntrack 處理,而 eBPF 方案只有在應用程序調用 bind 時執行一次,之後不會再執行,減少了性能開銷。

Outbound

再來看一下對出流量的劫持,對出流量的劫持比較複雜, 根據協議分為 TCP 和 UDP 兩種情況。

TCP 流量劫持

對 TCP 的出流量劫持過程:

  • _coonect4 通過劫持 connect 系統調用將目的地址修改為127.0.0.1:15001,也就是 envoy 的 VirtualOutboundListerer,同時將連接的原始目的地址保存在 sk_storage_map;

  • 在 TCP 連接建立完成後, sockops 會讀取 sk_storage_map 中的數據,並以四元組(源IP、目的IP、源端口、目的端口)為 key 將原始目的地址保存在 origin_dst_map;

  • _getsockopt 通過劫持 getsockopt 系統調用,讀取 origin_dst_map 中的數據將原始目的地址返回給 envoy。

UDP 流量劫持

istio 在 1.8 版本支持了智能 DNS 代理[5],開啟後 iptables 會將 DNS 請求劫持到 Sidecar 處理,我們也需要用 eBPF 實現相同邏輯,對於 TCP DNS 的劫持和上面類似, 對 UDP DNS 的劫持見下圖:

對 UDP 的出流量劫持過程:

  • _connect4 _sendmsg4 都是負責修改 UDP 的目的地址為 127.0.0.1:15053 並保存原始的目的地址到 sk_storage_map ,因為 Linux 提供兩種發送 UDP 數據的方式
    • 先調用 connect 再調用 send,這種情況由 _connect4 處理
    • 直接調用 sendto,這種情況由 _sendmsg4 處理
  • recvmsg4 通過讀取 sk_storage_map 將回包的源地址改為原始的目的地址,這是因為有些應用程序,例如 nslookup 會校驗回包的源地址。

對於 TCP 和 connected UDP,iptables 方案每個包都需要 conntrack 處理,而eBPF 方案的開銷是一次性的,只需要在 socket 建立時執行一次,降低了性能開銷。

Sockmap

使用 sockmap 優化服務網格性能的方案最早由 cilium 提出,我們的方案也參考了 cilium,這裏 借用 cilium 的兩張圖來説明下優化效果。

優化前 Sidecar 代理與應用程序間的網絡通信都需要經過 TCP/IP 協議棧處理。

優化後 Sidecar 代理與應用程序間的網絡通信繞過了 TCP/IP 協議棧,如果兩個 Pod 在同一節點上,兩個 Pod 間的網絡通信也可以被優化。這裏簡單説明下 sockmap 的優化原理,感興趣的同學可以查看[6][7]。

  • sock_hash 是一個存儲 socket 信息的 eBPF map,key 是四元組(源IP、目的IP、源端口、目的端口);
  • _sockops 負責監聽 socket 事件,並將 socket 信息保存在 sock_hash;
  • _sk_msg 會攔截 sendmsg 系統調用,然後到 sock_hash 中查找對端 socket,如果找到會調用 bpf_msg_redirect_hash 直接將數據發送給對端 socket。

問題

但是用四元組做為 key 可能會存在衝突的問題,例如在同一節點上的兩個 Pod 中,envoy 使用同一源端口 50000 請求應用程序的 80 端口。

為了解決這個問題,我們在 key 中添加了 netns cookie,同時對於非 localhost 的請求將 cookie 設置為 0,這樣既保證了 key 不會衝突,又可以加速同一節點上兩個 Pod 間的網絡通信。

但是之前版本的內核不支持在 sockops sk_msg 這兩種 eBPF 程序中獲取 netns cookie 信息,因此我們提交了兩個 patch [8 ][9]到內核社區,目前已合入 5.15 版本。

架構

整個方案的架構如圖所示,istio-ebpf 以 DaemonSet 的形式運行在節點上,負責 load/attach eBPF 程序和創建 eBPF map。istio-init 容器仍然保留,但是不再創建 iptables 規則,而是更新 eBPF map,istio-init 會將 Pod 的 netns cookie 保存在 cookie_map 中。同時我們也修改了 istiod,istiod 會根據 Pod 的流量劫持模式(iptables/eBPF)下發不同的 xDS 配置。

性能對比

測試環境:Ubuntu 21.04 5.15.7

  • 同等條件下,使用 eBPF 可減少 20% 的 System CPU 佔用;

  • 同等條件下,使用 eBPF 可提高 20% QPS;

  • 同等條件下,使用 eBPF 可降低請求時延。

總結

服務網格的 Sidecar 架構不可避免的會增加請求時延和資源佔用,我們通過使用 eBPF 代替 iptables 實現流量劫持,同時使用 sockmap 加速 Sidecar 代理和應用程序間的網絡通信,在 一定程度上降低了請求時延和資源開銷 ,由於內核版本等限制這一方案預計會在明年初上線,TCM 團隊將持續探索新的性能優化方向。

參考資料

[1] https://istio.io

[2] https://jimmysong.io/blog/sidecar-injection-iptables-and-traffic-routing

[3] https://ebpf.io

[4] https://cilium.io

[5] https://istio.io/latest/blog/2020/dns-proxy

[6] https://arthurchiao.art/blog/socket-acceleration-with-ebpf-zh

[7] https://github.com/cilium/cilium/tree/v1.11.0/bpf/sockops

[8] https://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git/commit/?id=6cf1770d

[9] https://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git/commit/?id=fab60e29f