shixudong@163.com
近期偶而关注了一下Linux的网卡卸载技术,无奈网上相关资料大多已经过时,如同鸡肋。几经筛选,终于发现tailscale有三篇文章结合实际应用对TSO/GRO介绍得比较深入,最主要的还是紧密结合linux新内核特性,更有实用价值。本人也结合自己的理解做一些解读,对TSO/GRO的基本概念就不再展开,以供随时备查。
一、背景
TSO/LRO/UFO需要硬件支持,TSO针对TCP发送,UFO针对UDP发送(基本没有物理网卡支持,自内核v4.14起废弃),LRO针对数据接收(从未进入主流,事实上已废弃,被内核实现的GRO所取代)。GSO/GRO由内核实现,GRO是早期硬件LRO的软件升级版,虽然GRO是由内核实现,但仍需修改网卡驱动以调用内核GRO接口,网卡才能真正支持GRO功能,目前绝大部分在用网卡驱动都已完成支持GRO的升级。
GSO/GRO号称更加通用,主要是两个特征,一是因为纯软件实现,不需要专用硬件支持,并且GRO实现较硬件来说,因身处内核,掌握信息更多,接收卸载更为准确,这也直接导致了纯硬件接收卸载(LRO)的没落;二是不仅支持TCP,未来还能支持UDP,然而GSO/GRO对UDP的支持迟迟没有实现。随着QUIC逐步被数据中心采用,内核从v4.18才开始支持UDP GSO,对UDP GRO的支持更是到v5.0才提供。尽管如此,GSO/GRO对UDP的支持力度还是不够,一是不像TSO,对网络应用是透明的,UDP GSO/GRO对网络应用不透明,需要应用程序重新编码并设置相应的socket选项以启用GSO/GRO。其次目前流行的各种虚拟网络通常建立在UDP隧道虚拟设备之上,而最初的UDP GSO/GRO实现对虚拟设备的支持还不够完善。
二、TCP卸载抓包效果
网上资料虽然带有丰富的TCP卸载效果图,但通过tcpdump抓包实际观察各种卸载技术并进行对比分析更有意义,有助于进一步加深印象,此处不描述具体的抓包和分析过程,而是结合卸载技术的实现逻辑分析不同因素对抓包效果的影响。
1、抓包位置影响
根据tcpdump在内核的抓包位置,由于GRO在网卡驱动层面就被调用,GRO收包发生在抓包位置之前,可以观察到GRO开关效果。鉴于GRO开关在网络驱动调用的GRO接口里判断,如某个逻辑网卡驱动(如bridge)没有必要调用GRO,自然缺乏GRO判断机制,该网卡对应的GRO开关就无实际意义。
在早期内核,GSO拆包发生在抓包位置之后,抓包工具能观察到GSO开关效果,较新内核将GSO拆包调整到了抓包位置之前,就再也不能通过抓包观察GSO的开关效果了。
TSO开关在内核实际执行GSO拆包动作前判断,无论GSO开关与否,如TSO关或不支持TSO,tcpdump只能抓到分段后的小包(也就是说,无法通过抓包观察GSO开关效果);如TSO为on,总能抓到未分段的大包(也就是说,如TSO为on,GSO总是开启)。对于逻辑网卡,TSO与硬件特性无关,表示其透传大包能力,只要ethtool能控制开关,也能被抓包观察。
2、多层网络影响
多层网络环境下(如中间有bridge),每层都是一个网络驱动程序,有自己的抓包位置和ethtool控制开关,每层抓包位置的逻辑不变,网络卸载则按如下逻辑发挥作用:在发送路径包含多个网络驱动时,如离内核更近的环节TSO为off,对大包进行了拆分操作,那么后续环节的TSO开关将失去实际意义,再也不能将其合并为大包。与之相对应,在接收路径包含多个网络驱动时,如离外部网络更近的环节GRO为on,对小包进行了合并操作,那么后续环节的GRO开关将失去实际意义,再也不能将其恢复小包。
3、同机转发路径影响
主机上层应用程序根据应用层逻辑,发出去的TCP包可为大包,也可为小包,对于TCP大包,由内核根据TSO开关控制发送到网卡驱动的数据包拆分与否。对于转发包,入口处是否合并取决于入口网卡GRO开关,转发环节上不做拆分与合并动作(二层桥转发也允许GSO大包通过),出口处是否拆分取决于出口网卡TSO开关(此处对转发出口处的分析不够全面,详见《再谈UDP GSO和GRO》)。如三层转发出口网卡在桥上,bridge和物理网卡的TSO都能影响出口包是否拆分。
三、UDP GSO/GRO
如前所述,自v4.18以来,内核开始支持UDP GSO,鉴于启用UDP GSO和传统GSO的方式不一致,需要在应用程序设置相应的socket选项显式启用,所以UDP GSO不用受ethtool的GSO开关控制。v4.18还新引入了与UDP GSO相配套的硬件特性tx-udp-segmentation,类似TSO,可通过硬件卸载UDP大包发送,该特性刚发布时,仅个别物理网卡支持。对于逻辑网卡来说,该特性表示其UDP大包透传能力,从 v4.18到v5.10,部分逻辑网卡如bridge能充分利用该特性,透明转发UDP大包。自v5.11起,内核优化了逻辑网卡支持该特性的方式,独立虚拟网卡如wireguard等也开始支持该特性。然而美中不足的是,v5.11的修改,导致先前已支持该特性的部分逻辑网卡如bridge反而不再支持该特性(除非下挂的物理网卡也支持该特性)。UDP GSO虽然不受GSO开关控制,但同样使用了传统GSO的底层框架和机制,所以仅在网卡支持并开启tx-udp-segmentation特性时,才能使用抓包工具直观验证UDP GSO效果。
自v5.0起,内核开始支持UDP GRO,不过也需要应用程序显式启用UDP GRO,同时因UDP GRO收包动作和传统GRO一致,由网卡驱动调用GRO接口实现,所以同样受到网卡GRO开关的控制,这点和UDP GSO稍有区别。自v5.6起,支持UDP fraglist GRO/GSO(入口网卡rx-gro-list为on,此处GSO主要配套用于转发环节),自v5.12起,支持普通(non-fraglisted)UDP GRO大包的接收和转发(入口网卡rx-udp-gro-forwarding为on,同时rx-gro-list必须为off)。以上两种情形,UDP GRO大包接收都对应用程序透明,无需显式启用UDP GRO功能,大大拓展了UDP GRO的使用场景。
四、wireguard-go的UDP GSO/GRO
本文开头提到的Tailscale关于wireguard-go优化的三篇文章,第一篇实现了wireguard-go对上层TCP应用的TSO/GRO支持,第二篇基于v4.18/v5.0的UDP GSO/GRO功能,实现了对底层UDP GSO/GRO支持,用于卸载wireguard协议本身。第三篇依赖v6.2内核tun驱动对UDP GSO/GRO的全面支持,自行实现了wg网卡对上层UDP GRO应用的透明支持(上层应用无需显式启用UDP GRO)。Wireguard-go在实现上述TCP/UDP GRO支持时,除了对底层UDP GRO的支持需要依托底层网卡的GRO机制外,对上层应用的GRO支持(包括TCP/UDP),无需依赖内核TCP/UDP GRO接口调用,所以该wg网卡与GRO相关的ethtool控制开关(如gro、rx-gro-list、rx-udp-gro-forwarding等)并无实际作用。
至于第三篇介绍的wireguard-go对上层应用的UDP GSO支持,亮点就是借助v6.2内核tun驱动,实现了wg网卡的tx-udp-segmentation特性(默认打开),允许wg网卡透明发送UDP GSO大包(内核wireguard自v5.11就支持了)。然而对于上层应用来说,依然需要显式启用UDP GSO功能才能享受wg网卡透明发送UDP大包的能力。此外第二篇介绍的wireguard-go在实现底层UDP GSO支持时,主要目标还是为了给上层TSO/UDP GSO锦上添花,当上层不使用TSO或不支持UDP GSO功能时,底层即使支持UDP GSO也无法将上层多个TCP/UDP包合并为底层一个UDP大包。这个不足实际上是由GSO/GRO的内在机制造成的,GSO只负责硬件不支持时的数据包拆分,不负责发送路径上的数据包合并,而GRO则正好相反,只负责接收路径上的数据包合并。
五、rx-gro-list与rx-udp-gro-forwarding的区别
UDP GSO由于无法实现对UDP应用的透明支持,大规模推广存在一定困难,然而内核对UDP GRO的透明支持却越加完善,如前所述,rx-gro-list和rx-udp-gro-forwarding都支持接收和转发UDP GRO大包。用户空间wireguard-go更是如此,并且接收和转发UDP GRO大包不用受相关gro开关的控制。在提供UDP GRO大包转发服务时,与出口网卡的tx-udp-segmentation特性相结合,可大大提高UDP大包的转发效率(如出口网卡为物理网卡,可实现硬件卸载;如出口网卡为逻辑网卡,可实现透明转发)。
虽然rx-gro-list和rx-udp-gro-forwarding都能支持UDP GRO大包转发,但他们的处理方式有所区别,前者采用fraglist方式,后者采用non-fraglisted方式。针对处理方式的不同,转发UDP GRO大包时对出口网卡的特性要求也是不同的,rx-udp-gro-forwarding只要求出口网卡支持tx-udp-segmentation即可,而rx-gro-list要求出口网卡除了支持该特性外,还得同时支持tx-gso-list和tx-scatter-gather-fraglist。目前基本上没有硬件网卡能支持tx-scatter-gather-fraglist,对于逻辑网卡来说,估计是历史遗留问题,目前尚不能同时支持这三种特性,v5.11起的wireguard,目前还不支持tx-scatter-gather-fraglist,v6.2起的tun(wireguard-go),目前还不支持tx-gso-list。因此如出口网卡具备tx-udp-segmentation特性时,为确保能硬件卸载(物理网卡)或透明转发(逻辑网卡)UDP GRO大包,入口网卡应启用rx-udp-gro-forwarding而非rx-gro-list开关(不能同时启用,否则rx-gro-list优先起作用)。
Tailscale的第三篇文章也专门指出,将wireguard-go用于转发出口网卡时,为了充分利用其tx-udp-segmentation特性,入口网卡应只启用rx-udp-gro-forwarding:“We recommend leaving rx-gro-list disabled. With rx-gro-list taking precedence over rx-udp-gro-forwarding, the effects of UDP GRO will be limited, reducing UDP throughput. rx-gro-list is a compelling, performance-enhancing feature, but the Linux kernel does not currently support a method to carry its benefits through the TUN driver. Instead, packets are segmented before being transmitted out a TUN device.”说了一大通貌似深奥的理由,然而其实质乃是v6.2内核的TUN驱动在实现UDP GSO支持时忘了同时支持NETIF_F_GSO_FRAGLIST属性而已(对应tx-gso-list),虽然TUN已支持tx-udp-segmentation和tx-scatter-gather-fraglist,但三者缺一不可,导致TUN无法透明转发fraglist方式的UDP GRO大包。 同理将内核wireguard网卡用于转发出口网卡时,虽然wg网卡已支持tx-udp-segmentation和tx-gso-list,但并没有同时支持tx-scatter-gather-fraglist,因而入口网卡同样也应只启用rx-udp-gro-forwarding,才能真正发挥wg网卡透明转发UDP GRO大包的能力。 本人在wsl环境下重新编译wsl内核,优化了逻辑网卡的NETIF_F_GSO_SOFTWARE定义和一般网卡的NETIF_F_SOFT_FEATURES定义,为wireguard增加了tx-scatter-gather-fraglist特性,为tap/tun增加了tx-gso-list特性后,无论入口网卡选择rx-gro-list还是rx-udp-gro-forwarding,内核和用户空间的wg网卡都能透明转发UDP GRO大包而无需交由GSO重新拆分为小包。
补充和小结一下,UDP GSO/GRO使用的底层框架和传统GSO/GRO完全一致,故TCP卸载抓包验证效果同样适用于UDP(实际上可以通过抓包工具直观感受UDP GSO的效果,详见《再谈UDP GSO和GRO》)。内核引入的UDP GSO/GRO新特性,如网卡支持的话,tx-udp-segmentation将默认开启,其他特性默认关闭,需要手动开启。wireguard-go针对上层应用实现的TCP/UDP GRO,默认启用,相应开关无实际作用,至于TCP/UDP GSO的实现,完全基于内核,tx-udp-segmentation也是默认开启,可以手动关闭。
最后顺便提一下,对于业务实时性要求高、但吞吐量要求不高的场合,开启UDP GRO将引入不必要的延时,建议同时关闭UDP GRO的本机接收和转发功能。