Merbridge 支持 Ambient Mesh,无惧 CNI 兼容性!

此篇博客介绍 Merbridge 支持 Ambient Mesh 的新特性。

深入 Ambient Mesh - 流量路径 一文中,我们分析了 Ambient Mesh 如何实现在不引入 Sidecar 的情况下,将特定 Pod 的出入口流量转发到 ztunnel 进行处理。我们可以发现,其通过 iptables + TPROXY + 路由表的方式进行实现,路径比较长,理解起来比较复杂。而且,由于其使用 mark 做路由标记,这可能导致在某些同样依赖 CNI 的网络下无法正常工作,或者在一些 Bridge 模式的网络 CNI 下无法工作,会极大的限制 Ambient Mesh 使用场景。

Merbridge 主要场景就是使用 eBPF 代替 iptables 为服务网格应用加速。Ambient Mesh 作为 Istio 服务网格的全新模式,Merbridge 自然也要支持这种新模式。 iptables 技术强大,为很多软件实现了各种功能,但在实际应用中也存在一些缺陷。首先,iptables 使用线性匹配方式,当多个应用使用相同能力时可能会产生冲突,进而导致某些功能不可用。其次,虽然其足够灵活,但是仍旧无法实现像 eBPF 一样自由编程的能力。 所以,使用 eBPF 技术代替 iptables 的能力,帮助 Ambient Mesh 实现流量拦截,这应该是一项令人兴奋的技术。

确定目标

通过 深入 Ambient Mesh - 流量路径 一文,我们可以知道的是,我们最终的目标有两个:

  • 将位于 Ambient Mesh 模式下的 Pod 向外发出的流量,拦截到 ztunnel 的 15001 端口。
  • 当主机程序向 Ambient Mesh 模式下的 Pod 发送流量时,应该将流量重定向到 ztunnel 的 15006 端口。

至于其中的 istioin 等网卡,完全是为了配合 Ambient Mesh 原有模式设计的,所以无需关注。

分析难点

在运行模式上,Ambient Mesh 模式和 Sidecar 模式存在很大区别。根据 Istio 官方的定义,将一个 Pod 加入 Ambient Mesh 并不需要重启 Pod,在 Pod 中也不存在 Sidecar 相关进程,这就意味着两个问题:

  1. 之前 Merbridge 使用 CNI 的方案,让 eBPF 程序能够感知到当前 Pod 的 IP 地址从而做出策略判断的方式将失效,因为 Pod 加入或移除 Ambient Mesh 模式并不会重启,也不会调用 CNI 插件,需要重新考量。
  2. 以前的流量拦截,只需要在 eBPF 的 connect 钩子中,将目标地址改为 127.0.0.1:15001,但是在 Ambient Mesh 模式下,需要将目标 IP 换成 ztunnel 的 IP 地址。

同时,由于 Ambient Mesh 模式下的 Pod 中,并不存在 Sidecar 相关进程,所以之前 Merbridge 用查看当前 Pod 中是否监听了 15006 等端口的方式也不再适用,需要重新设计方案判断进程的运行环境。

所以,基于上述分析,基本上需要重新设计整个拦截方案,以便让 Merbridge 支持 Ambient Mesh 模式。

总结下来,我们需要做这些事情:

  • 重新设计判断 Pod 是否加入 Ambient Mesh 模式的方案
  • 不依赖 CNI,实现 eBPF 感知当前 Pod IP 的方案
  • 让 eBPF 程序知道当前节点的 ztunnel 的 IP 地址方案

解决方案

在 0.7.2 版本中,我们引入了使用 cgroup id 加速 connect 程序的性能。通常每一个 Pod 中的容器都有一个对应的 cgroup id,我们可以在 bpf 程序中,通过 bpf_get_current_cgroup_id 函数获取到。通过将 IP 信息写入专用的 cgroup_info_map ,可以优化 connect 程序的运行速度。

之前 CNI 在网络命名空间中监听一个特殊的端口,用于存储 Pod 的信息,而 Ambient Mesh 模式与此不同。在 Ambient Mesh 模式中可以借助 cgroup id,如果能将 cgroup id 与 Pod IP 产生关联,那就可以在 eBPF 中获取到当前 Pod IP 信息。

由于不能依赖 CNI,所以需要改变获取 Pod 状态变更信息的方案。为此,我们通过观测进程的创建和销毁信息来探测本地 Pod 的创建和销毁等操作,创建了一个新的工具,用来观测主机上进程的变更信息:process-watcher 项目

通过从进程 ID 读取所在的 cgroup id 和 ip 信息并将其写入 cgroup_info_map

tcg := cgroupInfo{
		ID:            cgroupInode,
		IsInMesh:      in,
		CgroupIp:      *(*[4]uint32)(_ip),
		Flags:         flag,
		DetectedFlags: cgrinfo.DetectedFlags | AMBIENT_MESH_MODE_FLAG | ZTUNNEL_FLAG,
	}
	return ebpfs.GetCgroupInfoMap().Update(&cgroupInode, &tcg, ebpf.UpdateAny)

然后在 eBPF 中获取到当前 cgroup 所关联的信息:

__u64 cgroup_id = bpf_get_current_cgroup_id();
void *info = bpf_map_lookup_elem(&cgroup_info_map, &cgroup_id);

通过这里,我们可以知道,当前的容器是否启用了 Ambient Mesh 模式、是否在网格中等信息。

其次,对于 ztunnel 的 IP 地址,Istio 通过增加网卡绑定固定 IP 的方式实现,这种方案可能存在冲突的风险,也可能在某些情况下(比如 SNAT)会造成原地址信息丢失的情况。所以 Merbridge 放弃了此方案,直接在控制面获取 ztunnel 的 IP 地址,写入 map,让 eBPF 程序读取(这样速度更快)。

static inline __u32 *get_ztunnel_ip()
{
    __u32 ztunnel_ip_key = ZTUNNEL_KEY;
    return (__u32 *)bpf_map_lookup_elem(&settings, &ztunnel_ip_key);
}

然后,可以利用 connect 程序,重写目标地址:

ctx->user_ip4 = ztunnel_ip[3];
ctx->user_port = bpf_htons(OUT_REDIRECT_PORT);

通过与 cgroup id 的关联,可以实现在 eBPF 中获取当前进程所在的 Pod IP 地址,从而进行策略操作,将 Ambient Mesh 模式下 Pod 发出的流量转发到 ztunnel 进行处理,从而实现 Merbridge 在 Ambient Mesh 模式下的兼容。

这将是对所有 CNI 都适配的一种能力,可以避免原有 Ambient Mesh 模式不能很好的在大多数 CNI 模式下无法工作的问题。

使用与反馈

由于目前 Ambient 仍处于早期阶段,且 Merbridge 对于 Ambient 模式的支持也相对初级,还有一些问题没有得到很好的解决,所以 Ambient 模式还没有合并入主分支。如果想要体验 Merbridge 代替 iptables 为 Ambient Mesh 实现流量拦截的能力,可以按照如下的方式操作(首先需要安装好 Ambient Mesh 模式的网格):

  1. 禁用 Istio CNI(安装时设置 --set components.cni.enabled=false ,或删除 Istio CNI 的 DaemonSet kubectl -n istio-system delete ds istio-cni 的方式)。
  2. 删除 ztunnel 的 init 容器(因为它会初始化 iptables 规则、网卡等,而 Merbridge 不需要这个操作)。
  3. 使用 kubectl apply -f https://github.com/merbridge/merbridge/raw/ambient/deploy/all-in-one.yaml 安装支持 Merbridge 。

等待 Merbridge 就绪之后,即可以使用 Ambient Mesh 的所有能力。

*注意:

  1. 当前不支持在 Kind 下使用 Ambient 模式(将在后续支持);
  2. 主机内核版本需要大于等于 5.7;
  3. 需要开启 cgroup v2;
  4. 此模式也兼容 Sidecar 模式;
  5. Ambient 模式下安装默认会开启 debug 模式,会对性能造成一定影响。

更多实现细节可以查看源码

如果遇到任何问题,可在 Slack 中与我们反馈,或加入我们的技术交流微信群。

Kuma 2.0 集成 Merbridge 降低 12% 的网络延迟

最近 Kuma 正式发布了 2.0.0 版本,宣布了几项重大功能,其中第一条就是 Kuma 使用 eBPF 来加速应用访问。

Kuma 2.0 发布预览

根据 Kuma 官方的描述,Kuma 正是使用了 Merbridge,来实现 eBPF 的能力。

Kuma 2.0 eBPF 与 iptables 性能对比

We are utilizing the Merbridge OSS project within our eBPF capabilities and are very excited that we have been able to contribute back to that library and become co-maintainers. We look forward to working more with the Merbridge team as we continue to explore different areas to include eBPF functionality in Kuma.

我们非常高兴 Merbridge 作为一个开源项目,能够为 Kuma 提供如此令人兴奋的能力。这意味着,基本不需要任何开销,即可降低网格应用通讯延迟!

Kuma 从 6 月开始,就在着手于集成 Merbridge 项目,尝试使用社区现有的能力来为 Kuma 提供加速能力。

得益于 Merbridge 比较清晰的架构设计,Kuma 在很短的时间内就完成了对 Merbridge 的兼容。非常感谢 Kuma 社区能够为 Merbridge 贡献如此重要的兼容能力,这有助于双方社区共同成长!

截止目前,Merbridge 已经支持了 Istio、Linkerd2 和 Kuma 等主流的服务网格,也计划了很多新的特性,比如 IPv4/IPv6 双栈支持、Ambient Mesh 支持、更低的内核版本要求等。希望 Merbridge 能够被越来越广泛的应用,并且能够真实地帮助到大家。

深入 Ambient Mesh - 流量路径

此篇博客介绍 Ambient Mesh 中数据面的流量路径。

Ambient Mesh 发布已经有一段时间,也有不少文章讲述了其用法和架构。本文将深入梳理数据面流量在 Ambient 模式下的路径,帮助大家全面地理解 Ambient 数据面的实现方案。

在阅读本文之前,请先阅读 Ambient Mesh 介绍 了解 Ambient Mesh 的基本架构。

为了方便阅读和同步实践,本文使用的环境按照 Ambient 使用 的方式进行部署。

从发起请求的一刻开始

为了探究流量路径,首先我们分析同在 Ambient 模式下的两个服务互相访问的情况(仅 L4 模式,不同节点)。

在 default 命名空间启用 Ambient 模式后,所有的服务都将具备网格治理的能力。

我们的分析从这条命令开始: kubectl exec deploy/sleep -- curl -s http://productpage:9080/ | head -n1

在 Sidecar 模式下,Istio 通过 iptables 进行流量拦截,当在 sleep 的 Pod 中执行 curl 时,流量会被 iptables 转发到 Sidecar 的 15001 端口进行处理。 但是在 Ambient 模式下,在 Pod 中不存在 Sidecar,且开启 Ambient 模式也不需要重启 Pod,那它的请求如何确保被 ztunnel 处理呢?

出口流量拦截

要了解出口流量拦截的方案,我们首先可以看一下控制面组件:

kebe@pc $ kubectl -n istio-system get po
NAME                                   READY   STATUS    RESTARTS   AGE
istio-cni-node-5rh5z                   1/1     Running   0          20h
istio-cni-node-qsvsz                   1/1     Running   0          20h
istio-cni-node-wdffp                   1/1     Running   0          20h
istio-ingressgateway-5cfcb57bd-kx9hx   1/1     Running   0          20h
istiod-6b84499b75-ncmn7                1/1     Running   0          20h
ztunnel-nptf6                          1/1     Running   0          20h
ztunnel-vxv4b                          1/1     Running   0          20h
ztunnel-xkz4s                          1/1     Running   0          20h

在 Ambient 模式下 istio-cni 变成了默认组件。 而在 Sidecar 模式下,istio-cni 主要是为了避免使用 istio-init 容器处理 iptables 规则而造成权限泄露等情况推出的 CNI 插件。 但是在 Ambient 模式下,理论上不需要 Sidecar,为什么还需要 istio-cni 呢?

我们可以看一下日志:

kebe@pc $ kubectl -n istio-system logs istio-cni-node-qsvsz
...
2022-10-12T07:34:33.224957Z	info	ambient	Adding route for reviews-v1-6494d87c7b-zrpks/default: [table 100 10.244.1.4/32 via 192.168.126.2 dev istioin src 10.244.1.1]
2022-10-12T07:34:33.226054Z	info	ambient	Adding pod 'reviews-v2-79857b95b-m4q2g/default' (0ff78312-3a13-4a02-b39d-644bfb91e861) to ipset
2022-10-12T07:34:33.228305Z	info	ambient	Adding route for reviews-v2-79857b95b-m4q2g/default: [table 100 10.244.1.5/32 via 192.168.126.2 dev istioin src 10.244.1.1]
2022-10-12T07:34:33.229967Z	info	ambient	Adding pod 'reviews-v3-75f494fccb-92nq5/default' (e41edf7c-a347-45cb-a144-97492faa77bf) to ipset
2022-10-12T07:34:33.232236Z	info	ambient	Adding route for reviews-v3-75f494fccb-92nq5/default: [table 100 10.244.1.6/32 via 192.168.126.2 dev istioin src 10.244.1.1]

我们可以看到,对于在 Ambient 模式下的 Pod,istio-cni 做了两件事情:

  1. 添加 Pod 到 ipset
  2. 添加了一个路由规则到 table 100(后面介绍用途)

我们可以在其所在的节点上查看一下 ipset 里面的内容(注意,这里使用 kind 集群,需要用 docker exec 先进入所在主机):

kebe@pc $ docker exec -it ambient-worker2 bash
root@ambient-worker2:/# ipset list
Name: ztunnel-pods-ips
Type: hash:ip
Revision: 0
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 520
References: 1
Number of entries: 5
Members:
10.244.1.5
10.244.1.7
10.244.1.8
10.244.1.4
10.244.1.6

我们发现这个 Pod 所在的节点上有一个 ipset,其中保存了很多 IP。这些 IP 是 PodIP:

kebe@pc $ kubectl get po -o wide
NAME                              READY   STATUS    RESTARTS   AGE   IP           NODE              NOMINATED NODE   READINESS GATES
details-v1-76778d6644-wn4d2       1/1     Running   0          20h   10.244.1.9   ambient-worker2   <none>           <none>
notsleep-6d6c8669b5-pngxg         1/1     Running   0          20h   10.244.2.5   ambient-worker    <none>           <none>
productpage-v1-7c548b785b-w9zl6   1/1     Running   0          20h   10.244.1.7   ambient-worker2   <none>           <none>
ratings-v1-85c74b6cb4-57m52       1/1     Running   0          20h   10.244.1.8   ambient-worker2   <none>           <none>
reviews-v1-6494d87c7b-zrpks       1/1     Running   0          20h   10.244.1.4   ambient-worker2   <none>           <none>
reviews-v2-79857b95b-m4q2g        1/1     Running   0          20h   10.244.1.5   ambient-worker2   <none>           <none>
reviews-v3-75f494fccb-92nq5       1/1     Running   0          20h   10.244.1.6   ambient-worker2   <none>           <none>
sleep-7b85956664-z6qh7            1/1     Running   0          20h   10.244.2.4   ambient-worker    <none>           <none>

所以,这个 ipset 保存了当前节点上所有处于 Ambient 模式下的 PodIP 列表。

那这个 ipset 在哪可以用到呢?

我们看一下 iptables 规则,可以发现:

root@ambient-worker2:/# iptables-save
*mangle
...
-A POSTROUTING -j ztunnel-POSTROUTING
...
-A ztunnel-PREROUTING -p tcp -m set --match-set ztunnel-pods-ips src -j MARK --set-xmark 0x100/0x100

通过这个我们知道,当节点上处于 Ambient 模式的 Pod(ztunnel-pods-ips ipset 中)发起请求时,其连接会被打上 0x100/0x100 的标记。

一般在这种情况下,会与路由相关,我们看一下路由规则:

root@ambient-worker2:/# ip rule
0:	from all lookup local
100:	from all fwmark 0x200/0x200 goto 32766
101:	from all fwmark 0x100/0x100 lookup 101
102:	from all fwmark 0x40/0x40 lookup 102
103:	from all lookup 100
32766:	from all lookup main
32767:	from all lookup default

可以看到,被标记了 0x100/0x100 的流量会走 table 101 的路由表,我们可以查看路由表:

root@ambient-worker2:/# ip r show table 101
default via 192.168.127.2 dev istioout
10.244.1.2 dev veth5db63c11 scope link

可以明显看到,默认网关被换成了 192.168.127.2,且走了 istioout 网卡。

这里就有问题了,192.168.127.2 这个 IP 并不属于 NodeIP、PodIP、ClusterIP 中的任意一种,istioout 网卡默认应该也不存在,那这个 IP 是谁创建的呢? 因为流量最终需要发往 ztunnel,我们可以查看 ztunnel 的配置,看看能否找到答案。

kebe@pc $ kubectl -n istio-system get po ztunnel-vxv4b -o yaml
apiVersion: v1
kind: Pod
metadata:
  ...
  name: ztunnel-vxv4b
  namespace: istio-system
	...
spec:
  ...
  initContainers:
  - command:
			...
      OUTBOUND_TUN=istioout
			...
      OUTBOUND_TUN_IP=192.168.127.1
      ZTUNNEL_OUTBOUND_TUN_IP=192.168.127.2

      ip link add name p$INBOUND_TUN type geneve id 1000 remote $HOST_IP
      ip addr add $ZTUNNEL_INBOUND_TUN_IP/$TUN_PREFIX dev p$INBOUND_TUN

      ip link add name p$OUTBOUND_TUN type geneve id 1001 remote $HOST_IP
      ip addr add $ZTUNNEL_OUTBOUND_TUN_IP/$TUN_PREFIX dev p$OUTBOUND_TUN

      ip link set p$INBOUND_TUN up
      ip link set p$OUTBOUND_TUN up
      ...

如上,ztunnel 会负责创建 istioout 网卡,我们现在去节点上查看对应网卡。

root@ambient-worker2:/# ip a
11: istioout: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
    link/ether 0a:ea:4e:e0:8d:26 brd ff:ff:ff:ff:ff:ff
    inet 192.168.127.1/30 brd 192.168.127.3 scope global istioout
       valid_lft forever preferred_lft forever

192.168.127.2 这个网关 IP 在哪呢?它被分配在了 ztunnel 里面。

kebe@pc $ kubectl -n istio-system exec -it ztunnel-nptf6 -- ip a
Defaulted container "istio-proxy" out of: istio-proxy, istio-init (init)
2: eth0@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 46:8a:46:72:1d:3b brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.244.2.3/24 brd 10.244.2.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::448a:46ff:fe72:1d3b/64 scope link
       valid_lft forever preferred_lft forever
4: pistioout: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default qlen 1000
    link/ether c2:d0:18:20:3b:97 brd ff:ff:ff:ff:ff:ff
    inet 192.168.127.2/30 scope global pistioout
       valid_lft forever preferred_lft forever
    inet6 fe80::c0d0:18ff:fe20:3b97/64 scope link
       valid_lft forever preferred_lft forever

现在可以看到,流量会到 ztunnel 里面,但此时并没有对流量做任何其它操作,只是简单地路由到了 ztunnel。如何才能让 ztunnel 里面的 Envoy 对流量进行处理呢?

我们继续看一看 ztunnel 的配置,其中写了很多 iptables 规则。我们可以进入 ztunnel 看一下具体的规则:

kebe@pc $ kubectl -n istio-system exec -it ztunnel-nptf6 -- iptables-save
Defaulted container "istio-proxy" out of: istio-proxy, istio-init (init)
...
*mangle
-A PREROUTING -i pistioout -p tcp -j TPROXY --on-port 15001 --on-ip 127.0.0.1 --tproxy-mark 0x400/0xfff
...
COMMIT

现在可以看到,当流量进入 ztunnel 时,会使用 TPROXY 将流量转入 15001 端口进行处理,此处的 15001 即为 Envoy 实际监听用于处理 Pod 出口流量的端口。 关于 TPROXY,大家可以自行学习相关信息,本文不再赘述。

所以,总结下来,当 Pod 处于 Ambient 模式下,其出口流量路径大致为:

  1. 从 Pod 里面的进程发起流量。
  2. 流量流经所在节点网络,经节点的 iptables 进行标记。
  3. 节点上的路由表会将流量转发到当前节点的 ztunnel Pod。
  4. 流量到达 ztunnel 时,会经过 iptables 进行 TPROXY 透明代理,将流量交给当前 Pod 中的 Envoy 的 15001 端口进行处理。

到此我们可以看出,在 Ambient 模式下,对于 Pod 出口流量的处理相对复杂。路径也比较长,不像 Sidecar 模式,直接在 Pod 内部完成流量转发。

入口流量拦截

有了上面的经验,我们不难发现,Ambient 模式下,对于流量的拦截主要通过 MARK 路由 + TPROXY 的方式,入口流量应该也差不多。

我们采用最简单的方式分析一下。当节点上的进程,或者其他主机上的程序相应访问当前节点上的 Pod 时,流量会经过主机的路由表。 我们查看一下当响应访问 productpage-v1-7c548b785b-w9zl6(10.244.1.7) 时的路由信息:

root@ambient-worker2:/# ip r get 10.244.1.7
10.244.1.7 via 192.168.126.2 dev istioin table 100 src 10.244.1.1 uid 0
    cache

我们可以看到,当访问 10.244.1.7 时,流量会被路由到 192.168.126.2,而这条规则正是由上面 istio-cni 添加的。

同样地 192.168.126.2 这个 IP 属于 ztunnel:

kebe@pc $ kubectl -n istio-system exec -it ztunnel-nptf6 -- ip a
Defaulted container "istio-proxy" out of: istio-proxy, istio-init (init)
2: eth0@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 46:8a:46:72:1d:3b brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.244.2.3/24 brd 10.244.2.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::448a:46ff:fe72:1d3b/64 scope link
       valid_lft forever preferred_lft forever
3: pistioin: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default qlen 1000
    link/ether 7e:b2:e6:f9:a4:92 brd ff:ff:ff:ff:ff:ff
    inet 192.168.126.2/30 scope global pistioin
       valid_lft forever preferred_lft forever
    inet6 fe80::7cb2:e6ff:fef9:a492/64 scope link
       valid_lft forever preferred_lft forever

按照相同的分析方法,我们看一下 iptables 规则:

kebe@pc $ kubectl -n istio-system exec -it ztunnel-nptf6 -- iptables-save
...
-A PREROUTING -i pistioin -p tcp -m tcp --dport 15008 -j TPROXY --on-port 15008 --on-ip 127.0.0.1 --tproxy-mark 0x400/0xfff
-A PREROUTING -i pistioin -p tcp -j TPROXY --on-port 15006 --on-ip 127.0.0.1 --tproxy-mark 0x400/0xfff
...

如果直接在节点上访问 PodIP + Pod 端口,流量会被转发到 ztunnel 的 15006 端口,而这就是 Istio 处理入口流量的端口。

至于目标端口为 15008 端口的流量,这是 ztunnel 用来做四层流量隧道的端口。本文暂不细述。

对于 Envoy 自身的流量处理

我们知道,在 Sidecar 模式下,Envoy 和业务容器运行在相同的网络命名空间中。对于业务容器的流量,我们需要全部拦截,以保证对流量的完全掌控,但是在 Ambient 模式下是否需要呢?

答案是否定的,因为 Envoy 已经被独立到其它 Pod 中,Envoy 发出的流量是不需要特殊处理的。换言之,对于 ztunnel,我们只需要处理入口流量即可,所以 ztunnel 中的规则看起来相对简单。

未完待续…

上面我们主要分析了在 Ambient 模式下对于 Pod 流量拦截的方案,还没有涉及到七层流量的处理以及 ztunnel 实现的具体原理,后续将分析流量在 ztunnel 和 waypoint proxy 中详细的处理路径。

Merbridge CNI 模式

此篇博客将向您介绍 Merbridge CNI 的工作原理。

Merbridge CNI 模式的出现,旨在能够更好地适配服务网格的功能。之前没有 CNI 模式时,Merbridge 能够做得事情比较有限。其中最大的问题是不能适配注入 Istio 的 Sidecar Annotation,这就导致 Merbridge 无法排除某些端口或 IP 段的流量等。 同时,由于之前 Merbridge 只处理 Pod 内部的连接请求,这就导致,如果是外部发送到 Pod 的流量,Merbridge 将无法处理。

为此,我们精心设计了 Merbridge CNI,旨在解决这些问题。

为什么需要 CNI 模式?

其一,之前的 Merbridge 只有一个很小的控制面,其监听 Pod 资源,将当前节点的 IP 信息写入 local_pod_ips 的 map,以供 connect 使用。 但是,connect 程序由于工作在主机内核层,其无法知道当前正在处理的是哪个 Pod 的流量,就没法处理如 excludeOutboundPorts 等配置。 为了能够适配注入 excludeOutboundPorts 的 Sidecar Annotation,我们需要让 eBPF 程序能够得知当前正在处理哪个 Pod 的请求。

为此,我们设计了一套方法,与 CNI 配合,能够获取当前 Pod 的 IP,以适配针对 Pod 的特殊配置。

其二,在之前的 Merbridge 版本中,只有 connect 会处理主机发起的请求,这在同一台主机上的 Pod 互相通讯时,是没有问题的。但是在不同主机之间通讯时就会出现问题,因为按照之前的逻辑,在跨节点通讯时流量不会被修改,这会导致在接收端还是离不开 iptables。

这次,我们依靠 XDP 程序,解决入口流量处理的问题。因为 XDP 程序需要挂载网卡,所以也需要借助 CNI。

CNI 如何解决问题?

这里我们将探讨 CNI 的工作原理,以及如何使用 CNI 来解决问题。

如何通过 CNI 让 eBPF 程序获取当前正在处理的 Pod IP?

我们通过 CNI,在 Pod 创建的时候,将 Pod 的 IP 信息写入一个 Map(mark_pod_ips_map),其 Key 为一个随机的值,Value 为 Pod 的 IP。然后,在当前 Pod 的 NetNS 里面监听一个特殊的端口 39807,将 Key 使用 setsockopt 写入这个端口 socket 的 mark。

在 eBPF 中,我们通过 bpf_sk_lookup_tcp 取得端口 39807 的 Mark 信息,然后从 mark_pod_ips_map 中即可取得当前 NetNS(也是当期 Pod)的 IP。

有了当前 Pod IP 之后,我们可以根据这个 Pod 的配置,确认流量处理路径(比如 excludeOutboundPorts)。

同时,我们还使用 Pod 优化了之前解决四元组冲突的方案,改为使用 bpf_bind 绑定源 IP,目的 IP 直接使用 127.0.0.1,为了后续支持 IPv6 做准备。

如何处理入口流量?

为了能够处理入口流量,我们引入了 XDP 程序,XDP 程序作用在网卡上,能够对原始数据包做修改。 我们借助 XDP 程序,在流量到达 Pod 的时候,修改目的端口为 15006 以完成流量转发。

同时考虑到可能存在主机直接访问 Pod 的情况,也为了减小影响范围,我们选择将 XDP 程序附加到 Pod 的网卡上。这就需要借助 CNI 的能力,在创建 Pod 时进行附加操作。

如何体验 CNI 模式?

CNI 模式默认被关闭,需要手动开启。

可以使用以下命令一键开启:

curl -sSL https://raw.githubusercontent.com/merbridge/merbridge/main/deploy/all-in-one.yaml | sed 's/--cni-mode=false/--cni-mode=true/g' | kubectl apply -f -

注意事项

CNI 模式处于测试阶段

CNI 模式刚被设计和开发出来,可能存在不少问题,我们欢迎大家在测试阶段进行反馈,或者提出更好的建议,以帮助我们改进 Merbridge!

如果需要使用注入 Istio perf benchmark 等工具进行测试性能,请开启 CNI 模式,否则会导致性能测试结果不准确。

需要注意主机是否可开启 hardware-checksum 能力

为了保证 CNI 模式的正常运行,我们默认关闭了 hardware-checksum 能力,这可能会影响到网络性能。建议大家在开启 CNI 模式前,先确认主机是否可开启 hardware-checksum 能力。如果可以开启,建议设置 --hardware-checksum=true 以获得最佳的性能表现。

测试方法:ethtool -k <网卡> | grep tx-checksum-ipv4 为 on 表示开启。

Merbridge 和 Cilium

Merbridge 与 Cilium

Cilium 是一款基于 eBPF 为云原生应用提供诸多网络能力的优秀开源软件,有很多很棒的设计。例如,Cilium 设计了一套基于 sockmap 的 redir 能力,帮助加速网络通讯,这给了我们很大的启发,也是 Merbridge 提供网络加速的基础,这真是一个非常棒的设计。

Merbridge 借助于 Cilium 打下的良好基础,加上我们在服务网格领域做地一些针对性的适配,让大家可以更加方便地将 eBPF 技术应用于服务网格。

我们的开发团队从 Cilium 提供的资料中学习了很多关于 eBPF 的理论知识、实践方法和测试方法等,也与 Cilium 技术团队多有交流,也就是因为这些经历,才能有 Merbridge 项目的诞生。

再次衷心感谢 Cilium 项目和社区,以及 Cilium 的这些优秀设计。

与 Solo.io 一起举办的直播活动

2022 年 3 月 29 日,Solo.io 和 Merbridge 共同举办了一场直播活动。

在这次直播中,我们一起探讨了很多与 Merbridge 相关的问题,其中包含了一个线上 Demo,可以帮你快速了解 Merbridge 的功能和使用方法。

同时,PPT 可以在这里下载。

如果您有兴趣,可以查看:

一行代码,使用 eBPF 代替 iptables 加速服务网格

一行代码使用 eBPF 代替 iptables 加速 Istio

介绍

以 Istio 为首的服务网格技术正在被越来越多的企业关注,其使用 Sidecar 借助 iptables 技术实现流量拦截,可以处理所有应用的出入口流量,以实现诸如治理、观测、加密等能力。

但是使用 iptables 的方式进行拦截,由于需要对出入口都拦截,会让原本只需要在内核态处理两次的链路变成四次,会损失不少性能,这在一些要求高性能的场景下显然是有影响的。

近两年,由于 eBPF 技术的兴起,不少围绕 eBPF 的项目也应声而出,eBPF 在可观测性和网络包的处理方面也有不少优秀的案例。如 Cilium、px.dev 等项目。

借助 eBPF 的 sockops 和 redir 能力,可以高效的处理数据包,再结合实际场景,那么我们就可以使用 eBPF 去代替 iptables 为 Istio 进行加速。

现在,我们开源了 Merbridge 项目,只需要在您的 Istio 集群执行以下命令,即可直接使用 eBPF 代替 iptables 实现网络加速!

kubectl apply -f https://raw.githubusercontent.com/merbridge/merbridge/main/deploy/all-in-one.yaml

注意:当前仅支持在 5.7 版本及以上的内核下运行,请事先升级您的内核版本。

eBPF 的 sockops 加速

网络连接本质上是 socket 的通讯,eBPF 提供了一个 bpf_msg_redirect_hash 函数,用来将应用发出的包,直接转发到对端的 socket 上面,可以极大的加速包在内核中的处理流程。

这里需要一个 sock_map,需要根据当前的数据包信息,从 sock_map 中挑选一个存在的 socket 连接,转发请求,所以,需要在 sockops 的 hook 处或者其它地方将 socket 信息保存到 sock_map,并提供根据 key 查到 socket 的规则(一般为四元组)。

原理

下面,将按照实际的场景,逐步的介绍 Merbridge 详细的设计和实现原理,这将让你对 Merbridge 或者 eBPF 有一个初步的了解。

Istio 基于 iptables 的原理

Istio 基于 iptables 的流量拦截原理

如上图所示,当外部流量相应访问应用的端口时,会在 iptables 中被 PREROUTING 拦截,最后转发到 Sidecar 容器的 15006 端口,然后交给 Envoy 来进行处理。(图中红色 1 2 3 4 的路径)

Envoy 根据从控制平面下发的规则进行处理,处理完成后,会发送请求给实际的容器端口。

当应用想要访问其它服务时,会在 iptables 中 OUTPUT 拦截,然后转发给 Sidecar 容器的 15001 端口(Envoy 监听)。(图中红色 9 10 11 12 的路径)然后和入口流量处理差不多。

由此可以看到,原本流量可以直接到应用端口,但是中间需要通过 iptables 转发到 Sidecar,然后又让 Sidecar 发送给应用,这无疑增加了开销。并且,iptables 的通用性决定了它的性能没有很理想。会在整条链路上增加不少延迟。

如果我们能使用 sockops 去直接连接 Sidecar 到应用的 Socket,这样可以使流量不经过 iptables,可以提高性能。

出口流量处理

如上所述,我们希望使用 eBPF 的 sockops 来绕过 iptables 以加速网络请求。同时,我们希望创造的是一个能够完全适配社区版 Istio,不做任何改造。所以,我们需要模拟 iptables 所做的操作。

这个时候我们在看回 iptables 本身,其使用 DNAT 功能做流量转发。

想要用 eBPF 模拟 iptables 的能力,那么就需要使用 eBPF 实现类似 iptables DNAT 的能力。

这里主要有两个点:

  1. 修改连接发起时的目的地址,让流量能够发送到新的接口;
  2. 让 Envoy 能识别原始的目的地址,以能够识别流量;

对于其中第一点,我们可以使用 eBPF 的 connect 程序来做,通过修改 user_ipuser_port 实现。

对于其中第二点,需要用到 ORIGINAL_DST 的概念。这个在内核中其实是在 netfilter 模块专属的。

其原理就是,应用程序(包括 Envoy)会在收到连接之后,调用 get_sockopts 函数,获取 ORIGINAL_DST,如果经过了 iptables 的 DNAT,那么 iptables 就会给当前的 socket 设置这个值,并把原有的 IP + 端口写入这个值,应用程序就可以根据连接拿到原有的目的地址。

那么我们就需要通过 eBPF 的 get_sockopt 程序来修改这个调用。(不用 **bpf_setsockopt** 的原因是因为目前这个参数并不支持 SO_ORIGINAL_DST` 的 optname)

参见下图,在应用向外发起请求时,会经过如下阶段:

  1. 在应用向外发起连接时,connect 程序会将目标地址修改为 127.x.y.z:15001,并用 cookie_original_dst 保存原始目的地址。
  2. 在 sockops 程序中,将当前 sock 和四元组保存在 sock_pair_map 中。同时,将四元组信息和对应的原始目的地址写入 pair_original_dst 中(之所以不用 cookie,是因为在 get_sockopt 程序中无法获取当前 cookie)。
  3. Envoy 收到连接之后会调用 getsockopt 获取当前连接的目的地址,get_sockopt 程序会根据四元组信息从 pair_original_dst 取出原始目的地址并返回,由此连接完全建立。
  4. 在发送数据阶段,redir 程序会根据四元组信息,从 sock_pair_map 中读取 sock,然后通过 bpf_msg_redirect_hash 进行直`接转发,加速请求。

出口流量处理

其中,之所以在 connect 的时候,修改目的地址为 127.x.y.z 而不是 127.0.0.1,是因为在不同的 Pod 中,可能产生冲突的四元组,使用此方式即可巧妙的避开。(每个 Pod 间的目的 IP 就已经不同了,不存在冲突的情况)

入口流量处理

入口流量处理基本和出口流量类似,唯一差别:只需要将目的地址的端口改成 15006 即可。

但是,需要注意的是,由于 eBPF 不像 iptables 能在指定命名空间生效,它是全局的,这就造成如果我们将一个本来不是 Istio 所管理的 Pod,或者就是一个外部的 IP 地址,也做了这个操作的话,那就会引起严重问题,会请求直接无法建立连接。

所以这里我们设计了一个小的控制平面(以 DaemonSet 方式部署),其通过 Watch 所有的 Pod,类似于像 kubelet 那样获取当前节点的 Pod 列表,将已经被注入了 Sidecar 的 Pod IP 地址写入 local_pod_ips 这个 map。

当我们在做入口流量处理的时候,如果目的地址不在这个列表之中,我们就不做处理,让它走原来的逻辑,这样就可以比较灵活且简单的处理入口流量。

其他的流程和出口流量流程一样。

入口流量处理

同节点加速

通过入口流量处理,理论上,我们已经可以直接加速同节点的 Envoy 到 Envoy 的加速。但是存在一个问题。就是在这种场景下,Envoy 访问当前 Pod 的应用的时候会出错。

在 Istio 中,Envoy 访问应用的方式是使用当前 PodIP 加服务端口。经过上面入口流量处理章节,其实我们会发现,由于 PodIP 肯定也存在于 local_pod_ips 中,那么这个请求会被转发到 PodIP + 15006 端口,这显然是不行的,会造成无限递归。

那么我们也没办法在 eBPF 中获取当前 ns 的 IP 地址信息,怎么办?

为此,我们设计了一套反馈机制:

即,在 Envoy 尝试建立连接的时候,我们还是会走重定向到 15006 端口,但是,在 sockops 阶段,我们会判断源 IP 和目的地址 IP是否一致,如果一致,代表发送了错误的请求,那么我们会在 sockops 丢弃这个连接,并将当前的 ProcessID 和 IP 地址信息写入 process_ip 这个 map,让 eBPF 支持进程和 IP 的对应关系。

当下次请求发送时,我们直接从 process_ip 表检查目的地址是否和当前 IP 地址

Envoy 会在请求失败的时候重试,且这个错误只会发生一次,后续的连接会非常快。

同节点加速

连接关系

在没有使用 Merbridge(eBPF) 优化之前,Pod 到 Pod 间的访问入下图所示:

iptable 路径

图片参考:Accelerating Envoy and Istio with Cilium and the Linux Kernel

在使用 Merbridge(eBPF)优化之后,出入口流量会使用直接跳过很多内核模块,提高性能:

eBPF 路径

图片参考:Accelerating Envoy and Istio with Cilium and the Linux Kernel

同时,如果两个 Pod 在同一台机器上,那么他们之间的通讯将更加高效:

同节点 eBPF 路径

图片参考:Accelerating Envoy and Istio with Cilium and the Linux Kernel

以上,通过使用 eBPF 在主机上对相应的连接进行处理,可以大幅度的减少内核处理流量的流程,提升服务之间的通讯质量。

加速效果

下面的测试只是一个基本的测试,不是非常严谨。

下图展示了使用 eBPF 代替 iptables 之后,整体延迟的情况(越低越好):

延迟与连接数

下图展示了使用 eBPF 代替 iptables 之后,整体 QPS 的情况(越高越好):

延迟与 QPS

以上数据使用 wrk 测试得出。

Merbridge 项目

以上介绍的都是 Merbridge 项目的核心能力,其通过使用 eBPF 代替 iptables,可以在服务网格场景下,完全无感知的对流量通路进行加速。同时,我们不会对现有的 Istio 做任何修改,原有的逻辑依然畅通,这意味着,如果不再希望使用 eBPF,那么可以直接删除掉 DaemonSet,改为传统的 iptables 方式也不会出任何问题。

Merbridge 是一个完全独立的开源项目,此时还处于早期阶段,我们希望可以有更多的用户或者开发者参与其中,使用先进的技术能力,优化我们的服务网格。

项目地址:https://github.com/merbridge/merbridge

参考文档: