利用eBPF实现socket level重定向
大家好,我是二哥。
最近二哥利用业余时间在复习 eBPF ,为啥说是复习呢?因为我曾经短暂使用过 eBPF 。一晃几年过去了,我在研究 K8s 网络模型和 service mesh 的过程中,反复看到它的出现。它真是一个勤劳的小蜜蜂,哪里都能看到它的身影。而我在几年后重新拾起 eBPF ,对它有了更深的感悟,对它的小巧精悍也有了更多的喜爱。
于是我干脆在公司的年度 Tech Forum 上,和同事一起报了一个 topic "eBPF primer and its use cases in K8s"(eBPF 基础和 K8s 使用案例介绍),希望可以和更多的人分享所思所悟。
eBPF 在 K8s 中的一个使用案例便是:利用 eBPF 实现 socket level 重定向,本篇大致介绍为何需要做 socket level 重定向以及怎么做,最后我们再来看下 Cilium 是如何利用它来将两个 socket 以短路的方式连接到了一起从而大幅提升进程间通信性能的。
为何需要做 socket level 重定向
我们先来看图1所示的简单例子。进程 A 和进程 B 通过本机的 Loopback 网络设备通信。我们知道,虽然通信双方是走的 Loopback 这个虚拟的设备,从而省去了与真实网络设备相关的排队(Queue Descipline)等待时间,也省去了网络包离开本机后的网络延迟,但网络包在 TCP/IP 协议栈上该走的路可一步都少不了,万一路由表和 iptables 设置得比较复杂,那依旧需要在路由和 net filter 上面花去很多时间。
此外,网络包经过协议栈时,起码要经过 30 个左右的函数调用才能进入设备驱动层,这里的每一个函数都为网络延迟助了一把力。你可以按照下面的方法来体验一下这么多函数调用产生的 call stack 是多么的壮观和吓人。
[root@xx tracing]# cd /sys/kernel/debug/tracing
[root@xx tracing]# echo dev_hard_start_xmit > set_graph_function
[root@xx tracing]# echo nop > current_tracer
[root@xx tracing]# echo function_graph > current_tracer
[root@xx tracing]# cat trace | more
图 1:进程间通过 lo 设备通信
上图中,二哥还在 socket 这一层画出了 TX queue 和 RX queue 。在内核里,它们的准确名字应该是 sk_write_queue 和 sk_receive_queue 。它们和 socket 以及 skb 之间的关系见图 2 。之所以把图 2 画出来是因为:
- 图 1里面的 TX 和 RX queue 是更抽象的示意图,我想展现更具体的数据结构给大家,这样会有一种具象的、落地的感觉。
- 图 1虽然是示意图,但每一个细节都是在内核里有据可循的,二哥想和大家说它们可不是我臆想出来的。嗯,你也看出来了,二哥是一个以严谨著称的 RD。
本文中我会多次说到一个词:网络包。它在图 2 中用黄色的框 sk_buff 表示,它亦被简称为 skb 。
图 2:socket TX queue 和 RX queue
我们在图1 的基础上更进一步,把进程 A 和进程 B 放到一个 network namespace 里面来。你在哪里见过这种方式?是的,sidecar 就是这么玩的。
结合刚才对图 1 的分析,你应该能猜到图 3 这种方式和图 1 没有本质上的差别,它一样会碰到网络低效的问题,因为一个 network namespace 一样会包含它自己的路由表、iptables 、network device 等关键要素。
图 3:同一个 network ns 里进程间通过 lo 设备通信
我们把图 3 所涉及到的网络延迟问题再放到 sidecar 的实现场景里面来,如图 4 所示。这就是我们常讲的 sidecar 注入所需要付出的刚性成本。既然是刚性成本,就意味着我们是无法避开这部分消耗的,除非你不用它。
图 4:sidecar 网络成本
socket 重定向的想法
通过我们刚才的分析,我们知道所谓的延迟,很大一部分原因是我们的网络包不得已需要经过 TCP/IP 协议栈处理。那如果网络包不经过协议栈的话,不就完美避开这些导致延迟的坑吗?
这个大胆的想法首先由 Cilium 提出来。我将其想法简要地画在了图 5 上。按照图 5 右边的方式的话,从 envoy 发出来的网络包会从它的 socket 层 TX queue 直接被放入了进程 B 的socket 层RX queue 里。对于进程 B 而言,它当然不会在意这个网络包是怎么过来的。这里所说的网络包即为图 2 中所画的 skb 。
这个想法便是 socket level redirect ,我们可以看到它完美地避开了纷繁复杂的协议栈和网络设备层。
图 5:进程间通过 lo 设备通信
socket 重定向的实现
图 5 的想法很美好,不过该如何实现呢?这个时候就该 eBPF 登场了。
eBPF 可以用来监听所有的内核 socket 事件如被动建立连接事件(BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB)和主动建立连接事件(BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB),并将这些新建的 socket 记录到 BPF_MAP_TYPE_SOCKMAP 这种类型的 map 里。图 6 中 _skops 代表了这样的 eBPF 程序,而 sock_hash 则为这里提及的 map ,这种使用场景下,我们把它称为 sockmap。
eBPF 程序还可以用来拦截所有 sendmsg 系统调用,根据系统调用的参数去 map 里查找 peer socket,之后调用 BPF 函数 bpf_msg_redirect_hash() 来绕过 TCP/IP 协议栈,直接将数据发送到对端的 socket RX queue。图 6 中 _sk_msg 代表了这样的 eBPF 程序。
图 6:实现细节图
实现代码位于 https://github.com/LanceHBZhang/socket-acceleration-with-ebpf 。这是二哥从别人的 repo fork 过来的,已经经过测试了,可以使用。
Cilium 使用案例
最后,我们来看看 Cilium 是如何利用 eBPF socket level 重定向来实现两个神奇的功能的:
- sidecarless Service Mesh,也即将经典的通过 sidecar 方式所实现的 Service Mesh 改成 work node 上所有的 Pod 共享一个代理的模式。
- 网络性能大幅提升
图 7 展示了 Cilium 实现 sideless service mesh 的奥秘:eBPF socket level 重定向。
图 7:sideless service mesh
而基于 socket level 重定向这个想法所实现的 sidecarless service mesh,我们看到与传统方式相比,在每秒可处理的请求数上得到了巨幅的提升。
图 8:性能报告
以上就是本文的全部内容。码字不易,画图更难。喜欢本文的话请帮忙转发或点击“在看”。您的举手之劳是对二哥莫大的鼓励。谢谢!
文章转载自公众号:二哥聊云原生