凡是正确的东西,该来的最终还是会来的。 (当然了,经理可能也有同感。)
来看看几年前我写的文章:
- 利用nf_conntrack机制存储路由,省去每包路由查找: https://blog.csdn.net/dog250/article/details/24101425
- 在Linux的连接跟踪(nf_conntrack)中缓存私有数据省去每次查找: https://blog.csdn.net/dog250/article/details/42814563
- 使用multi zone的nf conntrack来缓存路由和socket构建高性能处理: https://blog.csdn.net/dog250/article/details/43370449
很多年前我写了很多这种模块,各种协议栈短路的设计,并且在上海的很多机房里跑了很久,也用这些东西成功面试了很多家大公司,也用这些东西面试过很多应聘者…
一晃好多年过去了,优化conntrack并置入e1000e驱动的周末恍如昨日,如今,Linux 4.18内核之后,使用标准内核,路由转发终于可以正儿八经地被offload了:
- 系统在网卡ingress建立flow cache,flow元组命中的将会直接被转发,不必再进行路由查找。
关于flow offload的详细信息参见下面的文章:
https://lwn.net/Articles/738214/
其原理图如下:
你觉得flow offload仅仅就是在Netfilter上增加了个HOOK吗?如果这样,我早就能做到了,而且在上海市的很多机房跑了好多年了。
flow offload真正厉害的地方是它迎合了云计算虚拟化智能网卡offload网络处理逻辑的风潮,实现了Linux标准的Netfilter API的硬件offload接口!它的意义在于下图显示的:
再来总结一下数据包的路径:
- 网卡收包进ingress。
- ingress匹配flow table:
- 命中:
- 获取flow entry项里的路由项。
- 解析路由项里的出口网卡dev_out,MAC地址等。
- 递减TTL。
- 直接dev_out转发。
- 未命中:
- 转交给慢速路径,标准Linux内核处理。
- ip_forward在FORWARD hook点将flow entry注入flow table(软件或硬件)。
- 命中:
迄至Linux 5.3版本的内核,硬件offload依然处于pending状态,关于硬件offload的patch,可以参考下面链接的patch:
https://www.mail-archive.com/netdev@vger.kernel.org/msg198158.html
https://www.mail-archive.com/netdev@vger.kernel.org/msg198155.html
https://www.mail-archive.com/netdev@vger.kernel.org/msg198157.html
https://www.mail-archive.com/netdev@vger.kernel.org/msg198156.html
https://www.mail-archive.com/netdev@vger.kernel.org/msg198159.html
本质上来讲,硬件offload其实就是调用了一个网卡驱动程序的接口,实现操作网卡流表的目的。
在演示怎么玩之前,必须澄清一个问题,即flow offload和nf_conntrack的关系。
首先,flow offload依赖两个内核模块:
- nf_flow_table
- nft_flow_offload
当modinfo它们时,一个很令人遗憾的消息出现了:
root@zhaoya-VirtualBox:~/# modinfo nft_flow_offload |grep depends
depends: nf_tables,nf_flow_table,nf_conntrack
它们依赖nf_conntrack这么一个饱受诟病的东西。在我五六年前实现conntrack rtcache的时候,我还特意优化了nf_conntrack,增加了一个hot cache机制,从而提升了性能,当时我还使用的是2.6.32这个古老的版本。
虽然一直到5.3版本,nf_conntrack一直在迭代优化,但是它本质上还是避免不了lock,依然不能被彻底洗白,遗憾的是,flow offload依赖了nf_conntrack。
那么flow offload的性能收益会不会因为nf_conntrack被打折扣呢?也许吧。
但是,这里要从三方面看这件事:
- 承认性能损失,毕竟nf_conntrack也带来了很多功能性收益。
- 修改flow offload,接触对nf_conntrack的依赖,同时也丧失了与NAT等功能的联动机制。
- 向前看,flow offload的主场在硬件offload,Netfilter的软flow offload就是个Demo,谁来在乎nf_conntrack呢。
是时候演示怎么玩了。
搭建下面的拓扑:
我们希望的效果是在中间R机器上配置flow offload,从A访问B的流量将被offload,不再需要经过R机器的路由查找。
登录到中间的R机器,打开ip_forward,我是用nftables来配置flow offload的,编写以下的nft配置:
# bypass.conf
flush ruleset
table ip filter {
flowtable ft {
hook ingress priority 0;
devices = {enp0s9, enp0s10};
}
chain forward {
type filter hook forward priority 0;
ip protocol tcp flow offload @ft
}
}
注意⚠️:本文不讲nft,请自行翻阅其手册,非常好玩。
执行nft命令加载它:
root@zhaoya-VirtualBox:~/monitor# nft -f ./bypass.conf
root@zhaoya-VirtualBox:~/monitor# nft list flowtables
table ip filter {
flowtable f {
hook ingress priority filter
devices = { enp0s9, enp0s10 }
}
}
root@zhaoya-VirtualBox:~/monitor#
同时,为了验证流量确实被offload了,我们只需要证明一个flow除了第一个包之外的后续流量没有经过FORWARD链即可,我们添加一条iptables空规则,然后查看确认它的统计数据:
root@zhaoya-VirtualBox:~/monitor# iptables -L FORWARD -v
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 all -- any any anywhere anywhere
root@zhaoya-VirtualBox:~/monitor#
此时,在机器B上发起到A的ssh连接并随便执行个命令产生数据输出,也就是模拟一个双向通信的TCP流:
查看iptables的统计数据:
root@zhaoya-VirtualBox:~/monitor# iptables -L FORWARD -v
Chain FORWARD (policy ACCEPT 2 packets, 120 bytes)
pkts bytes target prot opt in out source destination
2 120 all -- any any anywhere anywhere
root@zhaoya-VirtualBox:~/monitor#
可见,仅仅有2个包通过了FORWARD链。后续的包均被offload到了网卡的ingress。
那么,为什么是2个包触发了flow offload呢?如果确实是2个包触发flow offload,具体是哪两个包呢?
以下是触发flow offload项被创建添加进flow table的代码:
static void nft_flow_offload_eval(const struct nft_expr *expr,
struct nft_regs *regs,
const struct nft_pktinfo *pkt)
{
...
// 必须关联一个被confirm的conntrack才可被视为一条完整的可offload的流
if (!nf_ct_is_confirmed(ct))
goto out;
...
flow = flow_offload_alloc(ct, &route);
...
ret = flow_offload_add(flowtable, flow);
可见,可offload的flow的条件是其conntrack必须被confirm,即 完整地经过Netfiler路径的flow才可被offload ,这是合理的,毕竟被当前Box给DROP掉的流不是完整的流,offload它没有任何意义。
因此,一个TCP流的双向包均被FORWARD链 “看到” 是该流被offload的必要条件,所以,我们在iptables FORWARD的统计数据中看到了2个包。
现在,我们已经知道,主机B发起的经由主机R到达主机A的TCP流已经被主机R的flow offload机制给卸载了,如果我们在主机R上DROP掉FORWARD数据包,会怎样呢?
让我们试一下:
root@zhaoya-VirtualBox:~/monitor# iptables -F
root@zhaoya-VirtualBox:~/monitor# iptables -A FORWARD -j DROP
root@zhaoya-VirtualBox:~/monitor#
root@zhaoya-VirtualBox:~/monitor# tcpdump -i enp0s9 -n
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on enp0s9, link-type EN10MB (Ethernet), capture size 262144 bytes
18:42:17.775319 IP 192.168.57.7.22 > 192.168.58.7.60987: Flags [P.], ...
18:42:17.775734 IP 192.168.58.7.60987 > 192.168.57.7.22: Flags [.], ...
18:42:18.775997 IP 192.168.57.7.22 > 192.168.58.7.60987: Flags [P.], ...
18:42:18.776370 IP 192.168.58.7.60987 > 192.168.57.7.22: Flags [.], ...
依然可以看到双向的TCP包被转发,这说明了两个事实:
- 能抓到包说明我们用的flow offload是基于Netfilter的软实现。
- 没有被iptables的FORWARD DROP规则丢包说明iptables没有同步给flowtable。
此时,我们flush掉nftables的flow offload规则,再抓包:
root@zhaoya-VirtualBox:~/monitor# nft flush ruleset
root@zhaoya-VirtualBox:~/monitor# tcpdump -i enp0s9 -n
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on enp0s9, link-type EN10MB (Ethernet), capture size 262144 bytes
18:46:23.304633 IP 192.168.57.7.22 > 192.168.58.7.60987: Flags [P.], ...
18:46:25.035613 IP 192.168.57.7.22 > 192.168.58.7.60987: Flags [P.], ...
18:46:28.502334 IP 192.168.57.7.22 > 192.168.58.7.60987: Flags [P.], ...
18:46:35.427412 IP 192.168.57.7.22 > 192.168.58.7.60987: Flags [P.], ...
^C
很明显,双向通信被iptables的DROP规则阻滞了,只剩下TCP重传了,看前面的重传间隔时间戳,符合TCP的RTO退避规则。
完成了一个简单的验证实验之后,我们是不是要祈求一个iptables的target呢?这对于大多数熟悉iptables却不熟悉nftables的我们十分有意义,比如如下设置规则即可实现以上类似的flow offload:
iptables -A FORWARD -p tcp -j FLOWOFFLOAD
嗯,有擦掌欲试的想法了,这对我而言并不难,曾经对xtables-addons各种高级玩。
实现这个target的过程顺便还可以把flow offload对nf_conntrack的那个依赖砍掉,实现一个 更加纯粹(不带那些个花里胡哨的NAT支持) 的flow offload机制,换句话说,我将自己定义flowtable结构体,除了元组信息之外,去除其它花里胡哨的东西。
…
然而,似乎没有必要,因为OpenWRT已经实现了一个xt_FLOWOFFLOAD target了:
netfilter: add a xt_FLOWOFFLOAD target for NAT/routing offload support
This makes it possible to add an iptables rule that offloads routing/NAT
packet processing to a software fast path. This fast path is much
quicker than running packets through the regular tables/chains.
Requires Linux 4.14
Signed-off-by: Felix Fietkau nbd@nbd.name
代码来自其github:
https://github.com/openwrt/openwrt/commit/820f03099894bd48638fb5be326b5c551f0f2b98
似乎我也就不需要再做什么了…
插曲:
有人问我写那么多模块和路由转发优化的文章,为什么却不给社区提交patch呢?很简单,因为我对Linux内核社区这个熟人名利场没有兴趣,整这些东西可以提高自己的知名度,也会极大的削弱兴趣以及降低手艺人(而不是工程师)的效率,规范和协议是为了多人协作的意义存在的,一个人干手艺活儿当然是怎么方便怎么来了,我完全不需要关注一行多少字符,也可以随便命名函数和变量,当然引入很多魔术字也很方便。不过,经理可能不这么想。
此外:
- 这些模块是给公司写的。
- 这些思路都比较简单,没什么技术含量,整理成套话会比代码好些。
说回flow offload,我觉得整体来看,这里有三方面的内容:
- 对于转发的包,offload其路由的查找过程,即在FORWARD链cache它的路由项到flowtable。
- 对于本地处理的包,offload其socket的查找过程,即在传输层cache它的socket地址到flowtable。
- 上述1和2,如果网卡支持(可编程SmartNIC),那么将cache置于硬件,即HW offload。
说来说去,还是我几年前想做的那些事,只是一直没有条件玩智能网卡,因为太贵吧。当然,经理并不一定这么认为。
浙江温州皮鞋湿,下雨进水不会胖。