Merbridge - 使用 eBPF 加速您的服务网格

用 eBPF 替换 iptables 规则,允许将数据直接从入站套接字传输到出站套接字,缩短 Sidecar 和服务之间的数据路径。

2022 年 3 月 7 日 | 作者:刘可贝 - 道客云,韩晓鹏 - 道客云,李辉 - 道客云

Istio 在流量管理、安全、可观测性和策略方面的能力的秘密都在 Envoy 代理中。Istio 使用 Envoy 作为“Sidecar”来拦截服务流量,并使用 iptables 配置内核的 netfilter 包过滤功能。

使用 iptables 进行拦截有一些缺点。由于 netfilter 是一个高度通用的包过滤工具,在到达目标套接字之前会应用多个路由规则和数据过滤过程。例如,从网络层到传输层,netfilter 将根据预定义的规则进行多次处理,例如 pre_routingpost_routing 等。当数据包变为 TCP 数据包或 UDP 数据包,并被转发到用户空间时,将执行一些额外的步骤,例如数据包验证、协议策略处理和目标套接字搜索。当 Sidecar 被配置为拦截流量时,由于重复的步骤执行多次,原始数据路径可能会变得很长。

在过去两年中,eBPF 已经成为一项流行的技术,许多基于 eBPF 的项目已经发布到社区。像 CiliumPixie 这样的工具展示了 eBPF 在可观测性和网络数据包处理方面的巨大应用。凭借 eBPF 的 sockopsredir 功能,可以通过直接将数据包从入站套接字传输到出站套接字来高效地处理数据包。在 Istio 服务网格中,可以使用 eBPF 来替换 iptables 规则,并通过缩短数据路径来加速数据平面。

我们创建了一个名为 Merbridge 的开源项目,并通过对您的 Istio 管理的集群应用以下命令,可以使用 eBPF 实现这种网络加速。

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

使用 Merbridge,数据包数据路径可以直接从一个套接字缩短到另一个目标套接字,以下是它的工作原理。

使用 eBPF sockops 进行性能优化

网络连接本质上是套接字通信。eBPF 提供了一个函数 bpf_msg_redirect_hash,用于将应用程序在入站套接字中发送的数据包直接转发到出站套接字。通过进入上述函数,开发人员可以执行任何逻辑来决定数据包的目标。根据此特性,数据包的数据路径可以在内核中明显优化。

sock_map 是记录数据包转发信息的关键部分。当数据包到达时,将从 sock_map 中选择一个现有套接字来转发数据包。因此,我们需要保存所有数据包的套接字信息,以使传输过程正常运行。当有新的套接字操作时(例如,创建新的套接字),将执行 sock_ops 函数。获取套接字元数据并将其存储在 sock_map 中,以便在处理数据包时使用。sock_map 中的通用键类型是源地址、目标地址和端口的“四元组”。使用键和存储在映射中的规则,当新的数据包到达时,将找到目标套接字。

Merbridge 方法

让我们一步一步地介绍 Merbridge 的详细设计和实现原理,并使用一个真实场景。

基于 iptables 的 Istio Sidecar 流量拦截

Istio Sidecar Traffic Interception Based on iptables
基于 iptables 的 Istio Sidecar 流量拦截

当外部流量到达您的应用程序的端口时,它将被 iptables 中的 PREROUTING 规则拦截,转发到 Sidecar 容器的 15006 端口,并交给 Envoy 处理。这在上图中以红色路径中的步骤 1-4 显示。

Envoy 使用 Istio 控制平面发布的策略处理流量。如果允许,流量将被发送到应用程序容器的实际容器端口。

当应用程序尝试访问其他服务时,它将被 iptables 中的 OUTPUT 规则拦截,然后被转发到 Sidecar 容器的 15001 端口,Envoy 在该端口监听。这与入站流量处理类似,是红色路径中的步骤 9-12。

到达应用程序端口的流量需要转发到 Sidecar,然后从 Sidecar 端口发送到容器端口,这是开销。此外,iptables 的通用性决定了它的性能并不总是理想的,因为它不可避免地会因应用不同的过滤规则而给整个数据路径带来延迟。虽然 iptables 是进行数据包过滤的常见方法,但在 Envoy 代理的情况下,较长的数据路径放大了内核中数据包过滤过程的瓶颈。

如果我们使用 sockops 将 Sidecar 的套接字直接连接到应用程序的套接字,流量将不需要经过 iptables 规则,从而可以提高性能。

处理出站流量

如上所述,我们希望使用 eBPF 的 sockops 来绕过 iptables 以加速网络请求。同时,我们也不想修改 Istio 的任何部分,使 Merbridge 完全适应社区版本。因此,我们需要在 eBPF 中模拟 iptables 的作用。

iptables 中的流量重定向利用了它的 DNAT 功能。当试图使用 eBPF 模拟 iptables 的功能时,我们需要做两件事

  1. 修改目标地址,在连接启动时,以便流量可以发送到新的接口。
  2. 使 Envoy 能够识别原始目标地址,以便能够识别流量。

对于第一部分,我们可以使用 eBPF 的 connect 程序来处理它,方法是修改 user_ipuser_port

对于第二部分,我们需要了解属于内核中 netfilter 模块的 ORIGINAL_DST 的概念。

当应用程序(包括 Envoy)接收连接时,它将调用 get_sockopt 函数来获取 ORIGINAL_DST。如果经过 iptables DNAT 过程,iptables 将设置此参数,并使用“原始 IP + 端口”值设置到当前套接字。因此,应用程序可以根据连接获取原始目标地址。

我们必须通过 eBPF 的 get_sockopts 函数来修改此调用过程。(这里不使用 bpf_setsockopt,因为此参数目前不支持 SO_ORIGINAL_DST 的 optname)。

参考下图,当应用程序发起请求时,它将经历以下步骤

  1. 当应用程序发起连接时,connect 程序将修改目标地址为 127.x.y.z:15001,并使用 cookie_original_dst 保存原始目标地址。
  2. sockops 程序中,当前套接字信息和四元组将保存到 sock_pair_map 中。同时,相同的四元组及其对应的原始目标地址将被写入 pair_original_dest。(这里不使用 Cookie,因为它无法在 get_sockopt 程序中获取)。
  3. Envoy 接收到连接后,将调用 get_sockopt 函数读取当前连接的目标地址。get_sockopt 将根据四元组信息从 pair_original_dst 中提取并返回原始目标地址。因此,连接完全建立。
  4. 在数据传输步骤中,redir 程序将根据四元组信息从 sock_pair_map 中读取套接字信息,然后通过 bpf_msg_redirect_hash 直接转发它,以加快请求速度。
Processing Outbound Traffic
处理出站流量

为什么我们将目标地址设置为 127.x.y.z 而不是 127.0.0.1?当存在不同的 Pod 时,可能会出现冲突的四元组,这优雅地避免了冲突。(Pod 的 IP 不同,它们在任何时候都不会处于冲突状态。)

入站流量处理

入站流量的处理与出站流量基本相同,唯一的区别是:修改目标端口为 15006。

需要注意的是,由于 eBPF 无法像 iptables 那样在指定命名空间中生效,因此更改将是全局性的,这意味着如果我们使用一个最初未由 Istio 管理的 Pod,或使用一个外部 IP 地址,将会遇到严重的问题,例如连接根本无法建立。

因此,我们设计了一个小型控制平面(作为 DaemonSet 部署),它监控所有 Pod(类似于 kubelet 监控节点上的 Pod),将已注入 Sidecar 的 Pod IP 地址写入 local_pod_ips 映射中。

在处理入站流量时,如果目标地址不在映射中,我们将不对流量执行任何操作。

否则,步骤与出站流量相同。

Processing Inbound Traffic
处理入站流量

同节点加速

理论上,在同一节点上的 Envoy sidecar 之间的加速可以通过入站流量处理直接实现。然而,Envoy 在这种情况下访问当前 pod 的应用程序时会抛出错误。

在 Istio 中,Envoy 使用当前 pod 的 IP 地址和端口号访问应用程序。在上述场景中,我们发现 pod IP 也存在于 local_pod_ips 映射中,并且流量将被重新定向到端口 15006 上的 pod IP,因为它与入站流量的来源地址相同。重新定向到同一个入站地址会导致无限循环。

问题来了:有没有办法使用 eBPF 获取当前命名空间中的 IP 地址?答案是肯定的!

我们设计了一种反馈机制:当 Envoy 尝试建立连接时,我们会将其重定向到端口 15006。但是,在 sockops 步骤中,我们会判断源 IP 和目标 IP 是否相同。如果相同,则意味着发送了错误的请求,我们将在 sockops 过程中丢弃此连接。同时,当前的 ProcessIDIP 信息将被写入 process_ip 映射中,以便 eBPF 支持进程和 IP 之间的对应关系。

当下一个请求发送时,就不需要再次执行相同的流程。我们将在 process_ip 映射中直接检查目标地址是否与当前 IP 地址相同。

Same-node acceleration
同节点加速

连接关系

在使用 Merbridge 应用 eBPF 之前,pod 之间的数据路径如下

iptables's data path
iptables 的数据路径

在应用 Merbridge 之后,出站流量将跳过许多过滤步骤,以提高性能

eBPF's data path
eBPF 的数据路径

如果两个 pod 在同一台机器上,连接甚至可以更快

eBPF's data path on the same machine
同一台机器上的 eBPF 的数据路径

性能结果

让我们看看使用 eBPF 代替 iptables 对整体延迟的影响(越低越好)

Latency vs Client Connections Graph
延迟与客户端连接数图表

我们还可以看到使用 eBPF 后整体 QPS(越高越好)。测试结果使用 wrk 生成。

QPS vs Client Connections Graph
QPS 与客户端连接数图表

总结

本文介绍了 Merbridge 的核心思想。通过使用 eBPF 替换 iptables,可以在网格场景中加速数据传输过程。同时,Istio 完全不会改变。这意味着如果您不想再使用 eBPF,只需删除 DaemonSet,数据路径就会恢复到传统的基于 iptables 的路由,没有任何问题。

Merbridge 是一个完全独立的开源项目。它还处于早期阶段,我们期待有更多的用户和开发者参与其中。如果您能尝试使用这项新技术来加速您的网格,并提供一些反馈,我们将不胜感激!

另请参阅

分享此帖子