本文转载自微信公众号「云巅论剑」,作者寒蝉。转载本文请联系云巅论剑公众号。
什么是sockmap
sockmap是BPF程序的一种map类型,顾名思义,这种map类型中存储的是对struct sock的引用。熟悉网络的同学知道struct sock是套接字在网络层的表示,因此利用sockmap我们可以在网络层上对TCP套接字进行一些自定义处理和操纵。
背景
在现实环境中,绝大多数的网络请求并不会像如下左图一样直接从客户端发送到服务端,而是由于负载均衡、安全性等原因像如下右图一样,客户端发出的请求要经过代理服务器转发才能到达服务端。
很明显,代理转发行为拉长了网络传输路径,相对于直连增加了2次内核态和用户态之间的切换、2次内核态和用户态之间的数据拷贝、2次网络协议栈的处理以及1次用户态处理,因此代理转发相比直连大大的降低了网络性能(提高了时延,降低了带宽)。
sockmap就是在这个背景下产生的,其目的就是提供一种加速本机内部TCP socket之间数据转发的机制。
机制1 socket数据转发卸载
简介
如图2.1,通常用户态程序(如代理程序)需要通过系统调用sys_read从一个socket中读取数据到用户态,这个过程涉及到2次上下文切换(用户态->内核态->用户态)和1次内核态到用户态的数据拷贝;用户态程序对从socket中读取的数据进行一系列处理后,再通过系统调用sys_write将处理后的数据写入到另一个socket中去,这个过程涉及到2次上下文切换(用户态->内核态->用户态)和1次用户态到内核态的数据拷贝。
sockmap最初的版本提供了一种在本机TCP socket之间直接进行skb(struct sk_buff,其表示一个包含包头的数据包,后续均简称为skb)转发的机制。如图2.2,这种机制允许将原先图2.1中的用户态处理部分的逻辑卸载到内核BPF程序中进行处理,处理后的数据包skb直接在内核态中转发到另一个socket中去进行数据包的发送。整个过程无需的用户态内核态之间上下文切换,也无需任何用户态内核态之间的数据拷贝,大大缩短了数据转发路径。
关联的BPF程序
针对本机制,sockmap提供了两种BPF程序类型:
1. BPF_SK_SKB_STREAM_VERDICT
该类型的BPF程序根据用户的逻辑对数据进行处理和仲裁,其返回值(有如下三种)决定了源sock中数据的具体转发行为:
1.1 SK_PASS
该行为表示数据将按图2.1的方式被用户态程序接收处理并再发送到目的sock中。
1.2 SK_REDIRECT
该行为表示数据将按图2.2的方式在内核态中直接被BPF程序处理并发送到目的sock中。
1.3 SK_DROP
该行为表示数据将被直接丢弃。
2. BPF_SK_SKB_STREAM_PARSER
该类型的BPF程序不可单独使用,必须与BPF_SK_SKB_STREAM_VERDICT程序搭配使用,其用于确定一条完整消息的边界。通常数据流协议,在协议头中会指定playload有几个字节,然后通过底层tcp读取完协议头header和完整的payload后,才形成一条完整的消息记录。当BPF_SK_SKB_STREAM_VERDICT程序不能自行确定一条完整的消息长度时,就需要该BPF程序来确定是否读取到一条完整消息的尾部。
大多数情况下,BPF_SK_SKB_STREAM_VERDICT程序要么是可以自行确定消息的边界要么只是从skb中获取一些元数据例如IP地址。这时,该类型的BPF程序一般就直接返回SKB长度即可,如下。在内核5.10版本之后,这种情形的BPF_SK_SKB_STREAM_PARSER程序可以省略,仅使用BPF_SK_SKB_STREAM_VERDICT即可。
- SEC("stream_parser")
- int parser(struct __sk_buff *skb)
- {
- return skb->len;
- }
实现原理
那么现在我们要实现机制一的效果,即从图2.1到图2.2,核心是两点:
1. 用户态对数据包的处理(图2.1)-> 内核态对数据包的处理(图2.2)
2. 用户态收发网络数据包(图2.1)-> 内核态收发网络包(图2.2)
用户态数据包处理转换到内核态数据包处理
将用户态数据包的处理卸载到内核态进行处理的方法很简单,就是将用户态的处理逻辑用BPF程序在内核态重写一遍即可。
用户态收发包转换到内核态收发包
回想一下用户态应用是如何从网络收发数据的。
通常用户态应用阻塞在系统调用recv/recvfrom/read上或是通过select/poll/epoll等方式监听socket,当socket接收缓存有数据到达时,内核会通过回调函数sk_data_ready(sk_data_ready是结构体struct sk的一个函数指针成员)唤醒用户态应用进程从socket中收包并进行用户态的处理。
相对应的,通常用户态调用send/sendto/write等系统调用向socket的发送缓存写入数据时,内核会通过回调函数sk_write_space(sk_write_space也是结构体struct sk的一个函数指针程序)唤醒内核态进程并把数据从用户态进程转交到内核协议栈。
sockmap利用了Linux内核的一个名为Stream Parser的框架,该框架提供了在内核中做基于tcp之上的数据流协议解析的能力,其核心原理就是替换了sk_data_ready和sk_write_space的实现。
当tcp收到数据后,通过回调函数sk_data_ready进行事件通知,将收到的数据传递到Stream Parser中而不是唤醒用户态程序进行收包处理,接着Stream Parser把数据包交由前述的BPF程序进行处理。如前面所说,类型为BPF_SK_SKB_STREAM_VERDICT的程序会决定SKB的转发行为,经BPF程序处理后返回值为SK_REDIRECT的SKB会在内核态直接被转发到目的sock中发送缓存中,最后调用sk_write_space通知目的sock有数据写入。
机制2数据发送BYPASS协议栈
简介
通常从用户态空间中创建的TCP socket中发送数据可以使用send/sendto/sendmsg/write等系统调用,而这些系统调用最终都将由tcp_sendmsg和tcp_sendpage来处理,tcp_sendmsg和tcp_sendpage将用户空间的数据复制到内核SKB中,并将其按照TCP数据段发送出去。
如图3.1,对于位于同一台主机的发送端和接收端来说,发送端用户程序向发送侧socket发送的数据经tcp_sendmsg和tcp_sendpage发送后经过网络协议栈到达虚拟网卡Loopback后再经过一次网络协议栈到达接收端的接受侧socket,最后接收端的用户程序从接收侧socket中收取数据到用户态空间。
很明显,在这个过程中,数据经过了两次网络协议栈的处理。
因此,sockmap中又提供了一种加速机制,当发送端和接收端的socket都位于同一主机时,在发送数据时可以绕过网络协议栈,直接将数据发送接收端的socket中,如图3.2。
注:该机制下在Loopback网卡上将无法抓到网络报文。
关联的BPF程序
针对本机制,sockmap提供了一种BPF程序类型:
1. BPF_SK_MSG_VERDICT
该类型的BPF程序根据用户的逻辑对用户态发送的数据进行仲裁,其返回值(如下)决定了数据的转发行为。
1.1 SK_PASS
该行为表示数据将重新有如图3.1的方式发送到网络协议栈中经Loopback转发到接收端socket中。
1.2 SK_REDIRECT
该行为表示数据将按照图3.2的方式直接发送到接收端socket中,不经过网络协议栈和Loopback。
1.3 SK_DROP
该行为表示数据将被丢弃。
实现原理
为了实现本机制,sockmap直接替换了tcp_sendmsg和tcp_sendpage的实现为tcp_bpf_sendmsg和tcp_bpf_sendpage,如下。
在tcp_bpf_sendmsg和tcp_bpf_sendpage中会将数据的控制权交由前面提到的类型为BPF_SK_MSG_VERDICT的BPF程序仲裁决定该数据是否要经过该机制的加速。对于BPF程序返回值为SK_REDIRECT的数据将被直接放入接收侧socket的接收缓存中,无需经过协议栈和Loopback。
总结
sockmap针对位于同一主机下socket的TCP数据转发路径的不同部分提供了两种机制。
机制一由于BPF自身的限制(如只支持有限的循环)无法重写较为复杂的用户态逻辑,因此在实际应用上较为少见。
机制二目前已经可以在一些应用上看到了,比如社区的Cilium,以及我们团队在service mesh的一些场景做的网络优化。但是由于其只是BYPASS内核网络协议栈,网络转发加速效果并没有机制一那么明显。
参考资料
https://github.com/torvalds/linux
https://lwn.net/Articles/731133/
https://lwn.net/Articles/768371/
https://www.spinics.net/lists/netdev/msg691409.html