Flomesh 服務網格採用 eBPF 實現更高效的流量攔截和通信
在不久前發佈的 Flomesh 服務網格 1.3.3[1] 我們引入了 eBPF 功能,用以替代流量攔截方面的實現 iptables。由於 eBPF 對較新內核的依賴,iptables 的實現仍繼續提供。同時,得益於 eBPF 網絡方面的能力,我們也實現了同節點網絡通信的加速。
背景
在服務網格中,iptables 和 eBPF 是兩種比較常見的流量攔截方式。
iptables 是一種基於 Linux 內核的流量攔截工具,它可以通過過濾規則來對流量進行控制。它的優點包括:
-
• 通用性:iptables 工具已經在 Linux 操作系統中被廣泛使用,因此大多數的 Linux 用戶都熟悉它的使用方法;
-
• 穩定性:iptables 早已經成爲了 Linux 內核的一部分,因此具有較高的穩定性;
-
• 靈活性:iptables 工具可以根據需要靈活地配置規則,以控制網絡流量。
然而,iptables 也存在一些缺點:
-
• 難以調試:由於 iptables 工具本身較爲複雜,因此在進行調試時比較困難;
-
• 性能問題:iptables 會在內核空間中進行處理,這可能會對網絡性能產生影響;
-
• 處理複雜流量可能存在問題:當涉及到一些複雜的流量處理時,iptables 可能不太適合,因爲其規則處理不夠靈活。
ebpf 是一種高級的流量攔截工具,它可以通過自定義的程序在 Linux 內核中進行流量攔截和分析。ebpf 的優點包括:
-
• 靈活性:ebpf 可以使用自定義的程序來攔截和分析流量,因此具有更高的靈活性;
-
• 可擴展性:ebpf 可以動態加載和卸載程序,因此具有更高的可擴展性;
-
• 高效性:ebpf 可以在內核空間中進行處理,因此具有更高的性能。
然而,ebpf 也存在一些缺點:
-
• 較高的學習曲線:ebpf 相對於 iptables 來說比較新,因此需要一些學習成本;
-
• 複雜性:ebpf 自定義程序的開發可能比較複雜;
-
• 安全性:由於 ebpf 可以直接操作內核,因此需要謹慎使用以確保安全。
綜合來看,iptables 更適合簡單的流量過濾和管理,而 ebpf 更適合需要更高靈活性和性能的複雜流量攔截和分析場景。
架構
在 1.3.3 中爲了提供 eBPF 特性,Flomesh 服務網格提供了 CNI 實現 osm-cni 和運行於各個節點的 osm-interceptor,其中 osm-cni 可與主流的 CNI 插件兼容。
當 kubelet 在節點上創建 pod,會通過容器運行時 CRI 實現調用 CNI 的接口創建 Pod 的網絡命名空間,osm-cni 會在 pod 網絡命名空間創建完成後調用 osm-interceptor 的接口加載 BPF 程序並將其附加到 hook 點上。除此以外 osm-interceptor 還會在 eBPF Maps 中維護 pod 信息等內容。
實現原理
接下來介紹下引入 eBPF 後帶來的兩個特性的實現原理,注意這裏會忽略很多的處理細節。
流量攔截
出站流量
下面的圖中展示的是出站(outbound)流量的攔截。將 BPF 程序附加到 socket 操作 connect
上,在程序中判斷當前 pod 是否被網格納管,也就是是否注入 sidecar,然後將目標地址修改爲 127.0.0.1
、目標端口修改爲 sidecar 的 outbound 端口 15003
。只是修改還不夠,還要將原始目的地址和端口保存在 map 中,使用 socket 的 cookie 作爲 key。
當與 sidecar 的連接建立成功後,通過附加到掛載點 sock_ops
上的程序將原始目的地保存在另一個 map 中,使用 本地地址 + 端口和遠端地址 + 端口 作爲 key。後面 sidecar 訪問目標應用時,通過 socket 的 getsockopt
操作獲得原始目的地址。沒錯,getsockopt
上也附加了 BPF 程序,程序會從 map 中取出原地目的地址並返回。
入站流量
對於入站流量的攔截,主要是將原本訪問應用端口的流量,轉發到 sidecar 的 inbound 端口 15003
。這裏有兩種情況:
-
• 第一種,請求方和服務方位於同一個節點,請求方 sidecar 的
connect
操作被攔截後會將目標端口改爲15003
。 -
• 第二種,二者位於不同的節點,請求方 sidecar 使用原始端口建立連接,但握手的數據包到了服務方網絡命名空間時,被附加到 tc(traffic control)ingress 上的 BPF 程序將端口修改爲
15003
,實現類似 DNAT 的功能。
網絡通信加速
網絡數據包在 Kubernetes 的網絡中不可避免的要經過多次內核網絡協議棧的處理,eBPF 對網絡通信的加速則是通過跳過不必要的內核網絡協議棧,由互爲對端的兩個 socket 直接交換數據。
在上面流量攔截的圖中有消息的發送和接收軌跡。當附加在 sock_ops
上的程序發現連接建立成功,會將 socket 保存 map 中,同樣使用 本地地址 + 端口和遠端地址 + 端口 作爲 key。而互爲對端的兩個 socket,本地和遠端信息正好 相反,因此一個 socket 發送消息時可以直接從 map 中尋址對端的 socket。
對於同一個節點上的兩個 pod 通信,該方案也同樣適用。
快速體驗
-
• Ubuntu 20.04
-
• Kernel 5.15.0-1034
-
• 2c4g 虛擬機 * 3:master、node1、node2
安裝 CNI 插件
在所有的節點上執行下面的命令,下載 CNI 插件。
sudo mkdir -p /opt/cni/bin
curl -sSL https://github.com/containernetworking/plugins/releases/download/v1.1.1/cni-plugins-linux-amd64-v1.1.1.tgz | sudo tar -zxf - -C /opt/cni/bin
Master 節點
獲取 master 節點的 IP 地址。
export MASTER_IP=10.0.2.6
Kubernetes 集羣使用 k3s 發行版,但在安裝集羣的時候,需要禁用 k3s 集成的 flannel,使用獨立安裝的 flannel 進行驗證。這是因爲 k3s 的 CNI bin 目錄在 /var/lib/rancher/k3s/data/xxx/bin
下,而非 /opt/cni/bin
。
curl -sfL https://get.k3s.io | sh -s - --disable traefik --disable servicelb --flannel-backend=none --advertise-address $MASTER_IP --write-kubeconfig-mode 644 --write-kubeconfig ~/.kube/config
安裝 Flannel。這裏注意,Flannel 默認的 Pod CIDR 是 10.244.0.0/16
,我們將其修改爲 k3s 默認的 10.42.0.0/16
。
curl -s https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml | sed 's|10.244.0.0/16|10.42.0.0/16|g' | kubectl apply -f -
獲取 apiserver 的訪問 token,用於初始化工作節點。
sudo cat /var/lib/rancher/k3s/server/node-token
工作節點
使用 master 節點的 IP 地址以及前面獲取的 token 初始化節點。
export INSTALL_K3S_VERSION=v1.23.8+k3s2
export NODE_TOKEN=K107c1890ae060d191d347504740566f9c506b95ea908ba4795a7a82ea2c816e5dc::server:2757787ec4f9975ab46b5beadda446b7
curl -sfL https://get.k3s.io | K3S_URL=https://${MASTER_IP}:6443 K3S_TOKEN=${NODE_TOKEN} sh -
下載 osm-edge CLI
system=$(uname -s | tr [:upper:] [:lower:])
arch=$(dpkg --print-architecture)
release=v1.3.3
curl -L https://github.com/flomesh-io/osm-edge/releases/download/${release}/osm-edge-${release}-${system}-${arch}.tar.gz | tar -vxzf -
./${system}-${arch}/osm version
sudo cp ./${system}-${arch}/osm /usr/local/bin/
安裝 osm-edge
export osm_namespace=osm-system
export osm_mesh_name=osm
osm install \
--mesh-name "$osm_mesh_name" \
--osm-namespace "$osm_namespace" \
--set=osm.trafficInterceptionMode=ebpf \
--set=osm.osmInterceptor.debug=true \
--timeout=900s
部署示例應用
#模擬業務服務
kubectl create namespace ebpf
osm namespace add ebpf
kubectl apply -n ebpf -f https://raw.githubusercontent.com/cybwan/osm-edge-start-demo/main/demo/interceptor/curl.yaml
kubectl apply -n ebpf -f https://raw.githubusercontent.com/cybwan/osm-edge-start-demo/main/demo/interceptor/pipy-ok.yaml
#讓 Pod 調度到不同的 node 上
kubectl patch deployments curl -n ebpf -p '{"spec":{"template":{"spec":{"nodeName":"node1"}}}}'
kubectl patch deployments pipy-ok-v1 -n ebpf -p '{"spec":{"template":{"spec":{"nodeName":"node1"}}}}'
kubectl patch deployments pipy-ok-v2 -n ebpf -p '{"spec":{"template":{"spec":{"nodeName":"node2"}}}}'
sleep 5
#等待依賴的 POD 正常啓動
kubectl wait --for=condition=ready pod -n ebpf -l app=curl --timeout=180s
kubectl wait --for=condition=ready pod -n ebpf -l app=pipy-ok -l version=v1 --timeout=180s
kubectl wait --for=condition=ready pod -n ebpf -l app=pipy-ok -l version=v2 --timeout=180s
測試
測試時可通過命令查看工作節點上的內核 tracing 日誌查看 BPF 程序執行的 debug 日誌。爲了避免干擾 sidecar 與控制平面通信產生的干擾,先獲取控制面的 IP 地址。
kubectl get svc -n osm-system osm-controller -o jsonpath='{.spec.clusterIP}'
10.43.241.189
在兩個工作節點執行下面的命令。
sudo cat /sys/kernel/debug/tracing/trace_pipe | grep bpf_trace_printk | grep -v '10.43.241.189'
執行下面的命令發起請求。
curl_client="$(kubectl get pod -n ebpf -l app=curl -o jsonpath='{.items[0].metadata.name}')"
kubectl exec ${curl_client} -n ebpf -c curl -- curl -s pipy-ok:8080
正常會收到類似下面的結果,同時內核 tracing 日誌也會相應地輸出 BPF 程序的 debug 日誌(內容較多,這裏不做展示)。
Hi, I am pipy ok v1 !
Hi, I am pipy ok v2 !
引用鏈接
[1]
Flomesh 服務網格 1.3.3: https://github.com/flomesh-io/osm-edge/releases/tag/v1.3.3
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/-yJa2W7acU56WYuW0njFMA