Flomesh 服务网格采用 eBPF 实现更高效的流量拦截和通信

在不久前发布的 Flomesh 服务网格 1.3.3 我们引入了 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 信息等内容。

Flomesh 服务网格采用 eBPF 实现更高效的流量拦截和通信

实现原理

接下来介绍下引入 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 中取出原地目的地址并返回。

Flomesh 服务网格采用 eBPF 实现更高效的流量拦截和通信

入站流量

对于入站流量的拦截,主要是将原本访问应用端口的流量,转发到 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 通信,该方案也同样适用。

Flomesh 服务网格采用 eBPF 实现更高效的流量拦截和通信

快速体验

• 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 !