在不久前釋出的 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 程式将端口修改為
,實作類似 DNAT 的功能。15003
網絡通信加速
網絡資料包在 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