Linux flow offload提高路由转发效率

凡是正确的东西,该来的最终还是会来的。 (当然了,经理可能也有同感。)

来看看几年前我写的文章:

  • 利用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/

其原理图如下:
Linux flow offload提高路由转发效率_第1张图片
你觉得flow offload仅仅就是在Netfilter上增加了个HOOK吗?如果这样,我早就能做到了,而且在上海市的很多机房跑了好多年了。

flow offload真正厉害的地方是它迎合了云计算虚拟化智能网卡offload网络处理逻辑的风潮,实现了Linux标准的Netfilter API的硬件offload接口!它的意义在于下图显示的:
Linux flow offload提高路由转发效率_第2张图片
再来总结一下数据包的路径:

  • 网卡收包进ingress。
  • ingress匹配flow table:
    • 命中:
      1. 获取flow entry项里的路由项。
      2. 解析路由项里的出口网卡dev_out,MAC地址等。
      3. 递减TTL。
      4. 直接dev_out转发。
    • 未命中:
      1. 转交给慢速路径,标准Linux内核处理。
      2. ip_forward在FORWARD hook点将flow entry注入flow table(软件或硬件)。

迄至Linux 5.3版本的内核,硬件offload依然处于pending状态,关于硬件offload的patch,可以参考下面链接的patch:
https://www.mail-archive.com/[email protected]/msg198158.html
https://www.mail-archive.com/[email protected]/msg198155.html
https://www.mail-archive.com/[email protected]/msg198157.html
https://www.mail-archive.com/[email protected]/msg198156.html
https://www.mail-archive.com/[email protected]/msg198159.html

本质上来讲,硬件offload其实就是调用了一个网卡驱动程序的接口,实现操作网卡流表的目的。


在演示怎么玩之前,必须澄清一个问题,即flow offload和nf_conntrack的关系。

首先,flow offload依赖两个内核模块:

  1. nf_flow_table
  2. 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被打折扣呢?也许吧。

但是,这里要从三方面看这件事:

  1. 承认性能损失,毕竟nf_conntrack也带来了很多功能性收益。
  2. 修改flow offload,接触对nf_conntrack的依赖,同时也丧失了与NAT等功能的联动机制。
  3. 向前看,flow offload的主场在硬件offload,Netfilter的软flow offload就是个Demo,谁来在乎nf_conntrack呢。

是时候演示怎么玩了。

搭建下面的拓扑:
Linux flow offload提高路由转发效率_第3张图片
我们希望的效果是在中间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包被转发,这说明了两个事实:

  1. 能抓到包说明我们用的flow offload是基于Netfilter的软实现。
  2. 没有被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 [email protected]

代码来自其github:
https://github.com/openwrt/openwrt/commit/820f03099894bd48638fb5be326b5c551f0f2b98

似乎我也就不需要再做什么了…

插曲:
有人问我写那么多模块和路由转发优化的文章,为什么却不给社区提交patch呢?很简单,因为我对Linux内核社区这个熟人名利场没有兴趣,整这些东西可以提高自己的知名度,也会极大的削弱兴趣以及降低手艺人(而不是工程师)的效率,规范和协议是为了多人协作的意义存在的,一个人干手艺活儿当然是怎么方便怎么来了,我完全不需要关注一行多少字符,也可以随便命名函数和变量,当然引入很多魔术字也很方便。不过,经理可能不这么想。

此外:

  1. 这些模块是给公司写的。
  2. 这些思路都比较简单,没什么技术含量,整理成套话会比代码好些。

说回flow offload,我觉得整体来看,这里有三方面的内容:

  1. 对于转发的包,offload其路由的查找过程,即在FORWARD链cache它的路由项到flowtable。
  2. 对于本地处理的包,offload其socket的查找过程,即在传输层cache它的socket地址到flowtable。
  3. 上述1和2,如果网卡支持(可编程SmartNIC),那么将cache置于硬件,即HW offload。

说来说去,还是我几年前想做的那些事,只是一直没有条件玩智能网卡,因为太贵吧。当然,经理并不一定这么认为。


浙江温州皮鞋湿,下雨进水不会胖。

你可能感兴趣的:(linux路由,flow,table,nftables,flow,offload)