shixudong@163.com
一、UDP/GSO再认识
近期本人收到一位网友私信:“tx-udp-segmentation = on时,发送UDP报文, 发包端抓包发现协议栈仍然分片,这是为什么?”在回答这个问题之前,先简要梳理一下UDP/GSO和TCP/GSO的主要区别:
1、对于TCP,只要开启TSO,GSO总是开启(不受GSO开关控制),只有TSO关闭时,才能通过GSO开关控制GSO开启与否。然而,自内核v4.17起,GSO总是开启,无论TSO开启与否,再也不受GSO开关影响。
2、对于UDP,没有对应的GSO开关,并且对网络应用不透明,需要应用程序设置相应的socket选项显式启用。UDP有类似TSO的硬件卸载开关tx-udp-segmentation,但同样需要应用程序设置相应的socket选项才能发挥tx-udp-segmentation的作用。
所以针对开头的问题,实际上就是传统的UDP应用程序根本无法利用UDP/GSO特性(包括硬件卸载tx-udp-segmentation)。事实上,由于当时没找到简易的验证工具,在《也谈UDP GSO和GRO》一文中,关于抓包工具验证UDP/GSO效果的描述不太全面,在该文中提到,由于较新内核调整了抓包位置,导致无法验证TCP/GSO。但对于UDP/GSO来说,虽然其同样使用了TSO/GSO的底层框架和机制,然而由于UDP/GSO在传输层对网络应用不透明的特性,UDP/GSO的直观效果是可以通过抓包工具验证的。
受到《TPROXY与Wireguard》文末小插曲的启发,可以利用管道串接dd和socat构造UDP大包,再通过socat的setsockopt参数启用UDP/GSO。网卡不支持tx-udp-segmentation时,使用dd if=/dev/zero bs=1024 count=8 |socat -b65536 - UDP-send:192.168.4.68:8888,setsockopt=17:103:1472,可以抓到UDP分段包(除最后一包外每包长度1514)。网卡支持tx-udp-segmentation时,使用前述命令可以抓到长度为8234(8*1024+28+14)的UDP大包。如不带setsockopt参数,则无论网卡是否支持tx-udp-segmentation,使用dd if=/dev/zero bs=1024 count=8 |socat -b65536 - UDP-send:192.168.4.68:8888,只能抓到IP分片包(Fragmented IP)。
UDP/GSO硬件卸载特性tx-udp-segmentation自内核v4.18引入,支持该特性的逻辑网卡在《也谈UDP GSO和GRO》一文有提到,包括内核wireguard、bridge/bond/team等。自内核v6.2起, virtio_net、tap/tun也开始支持tx-udp-segmentation,与前面那些逻辑网卡不同的是,虚拟机virtio_net不仅需要主机tap的配合,还需要QEMU8.0的加持,所以guest和host内核都需要升级到v6.2。此外低版本QEMU升级到QEMU8.0后,还需要额外修改xml文件使得guest的机器类型和QEMU8.0一致。上述逻辑/虚拟网卡中,virtio_net需要手工开启tx-udp-segmentation,其他网卡则默认开启。
二、UDP/GRO再认识
由于当时对网卡的两个特性tx-scatter-gather和tx-scatter-gather-fraglist认识不够深入,网上资料也不全面,导致《也谈UDP GSO和GRO》一文中,对GRO大包转发出口处的抓包分析有瑕疵,对UDP/GRO有关的两个特性rx-gro-list与rx-udp-gro-forwarding认识也不够深入。经过对Linux有关GRO卸载技术源代码的再次学习,对GRO相关底层实现有了进一步的理解,特记录于此。
tx-scatter-gather和tx-scatter-gather-fraglist可以分别对应到内核sk_buff的frags数组和frag_list 链表。在本机发送环节,TCP/GSO和UDP/GSO只使用frags方式发送数据,且网卡必须支持tx-scatter-gather特性(事实上目前基本上所有网卡都已支持tx-scatter-gather);未启用UDP/GSO时,本机发送的UDP大包和ping大包由__ip_make_skb函数转换成frag_list链表,并在IP层被ip_do_fragment拆分成IP分片包(根据以上分析,实际上发送环节的frag_list链表与tx-scatter-gather-fraglist不会发生任何关系。常规情况下,即使发送环节产生了frag_list 链表,也会被ip_do_fragment拆分)。在网卡接收环节,基本上所有硬件网卡也都是使用frags方式接收数据,个别硬件网卡使用frag_list方式接收数据,然后两者都交给内核GRO做进一步的合并处理。
在内核TCP/GRO处理环节,早期内核GRO不支持frags和frag_list组合使用,由于大部分网卡只支持使用frags方式收包,且frags数组最多只能包含MAX_SKB_FRAGS(内核硬编码,取值17)个成员,而且收包不像发包,每个frags只能容纳一个帧(1448字节),导致GRO最多只能合并17*1448+1*1514=26130字节的大包。内核v3.13起GRO已支持frags和frag_list组合使用,最多可合并17*1448(frags)+27*1448(frag_list)+1*1514=65226字节的大包,合并效率较早期内核GRO大大提高。然而该效率提高却并不完全适用于转发环节,虽然转发环节在IP层不对前述大包做拆分与合并动作,但由于硬件网卡基本上都不支持tx-scatter-gather-fraglist,导致带有frag_list属性的大包(包含其frags部分)在继续转发出去前只能由GSO重新拆分为小包。对于长度大于26130字节的大包,显然浪费了不必要的GRO合并和GSO拆分开销。通过抓包也能发现,接收网卡能收到最大65226字节的大包,而转发网卡最大只能发送纯frags方式的26130字节的大包。针对这一不足,自内核v6.4起,引入了CONFIG_MAX_SKB_FRAGS(取值17和45之间,默认还是17,修改需要重新编译内核),MAX_SKB_FRAGS取值45后,内核GRO合并时无需使用frag_list也能合并44*1448(frags)+1*1514=65226字节的大包,转发时再也无需经由GSO重新拆分小包,显著提高了转发效率。
对于UDP来说,目前有三种方法可以接收UDP/GRO包:
1、应用程序显式启用相应的socket选项接收UDP/GRO(内核v5.0);
2、开启rx-gro-list接收和转发UDP/GRO(内核v5.6);
3、开启rx-udp-gro-forwarding接收和转发UDP/GRO(内核v5.12)。
方法3接收和转发UDP/GRO时,采用了TCP/GRO同样的底层框架(合并函数都是skb_gro_receive),存在TCP/GRO同样的问题,即UDP/GRO合并后有部分大包同时采用了frags和frag_list组合。如转发网卡不支持tx-udp-segmentation,包括纯frags方式在内的所有大包都将由GSO重新拆分为小包,显著降低了转发效率。如转发网卡支持tx-udp-segmentation,则只有frag_list属性的大包(包含其frags部分)才需要由GSO重新拆分为小包,相较前者,还能适度平衡转发效率。
方法2接收和转发UDP/GRO时,GRO底层引入了新的合并函数skb_gro_receive_list,UDP/GRO合并大包采用纯frag_list方式,即使转发网卡支持tx-udp-segmentation,但依然不会支持tx-scatter-gather-fraglist,所有大包都将由GSO重新拆分为小包。
显然在转发网卡支持tx-udp-segmentation时,方法3总体转发效率要优于方法2。在转发网卡不支持tx-udp-segmentation时,由于方法2后续GSO拆分frag_list大包较方法3同时拆分frags/frag_list组合方式大包效率有改善,此时方法2转发效率要优于方法3。
基于同样的原因,对于TCP来说,如果转发网卡不支持TSO(如PPPOE、wifi),鉴于先前TCP/GRO不支持纯frag_list方式合并大包,导致后续TCP/GSO拆分frags/frag_list组合方式GRO大包时效率较低。针对这一不足,自内核v6.10起,新增了纯frag_list方式的TCP fraglist GRO support,底层共用UDP/GRO引入的合并函数skb_gro_receive_list。在转发网卡不支持TSO特性时,通过设置入口网卡rx-gro-list=on改用TCP fraglist GRO(仅适用于转发,本机接收仍使用传统方式),可有效改善转发效率。
根据源码和官方资料,如果同时开启方法2和方法3,方法2优先,在转发网卡支持tx-udp-segmentation时,考虑到方法2转发效率不如方法3,正确的操作姿势应该是只开启方法3。
方法1只能用于本机接收,GRO底层框架和方法3一致(合并函数为skb_gro_receive),方法2和3既能用于转发,也能用于本机接收。上述三种方法用于本机接收时,考虑到纯flag_list方式涉及到更多的skb申请/释放操作,相对来说,方法1和方法3的效率更佳。然而方法3有一个小瑕疵,本机接收无法用于内核类udp隧道的底层UDP,比如内核wireguard使用的底层UDP。
三、rx-gro-list和rx-udp-gro-forwarding再认识
在《也谈UDP GSO和GRO》一文中提到,将wireguard网卡用于转发出口网卡时,为了充分利用其tx-udp-segmentation特性,入口网卡应只启用rx-udp-gro-forwarding。本文则针对wireguard网卡用作入口网卡进行展开分析。
将内核wireguard网卡用作入口网卡接收UDP/GRO大包时,建议同时开启wg网卡以及底层网卡的UDP/GRO功能,分别对应上下两层UDP。由于rx-udp-gro-forwarding实现时的小瑕疵,本机接收无法用于wg网卡的底层UDP,因此底层网卡只能使用rx-gro-list。wg网卡建议使用rx-udp-gro-forwarding,无论后续转发还是自己接收,效率都要高于使用rx-gro-list(详见前面分析过程)。
至于将最新版的用户空间wireguard-go用作入口网卡,如内核版本已为v6.2,wg网卡以及底层网卡的UDP/GRO功能均已自动启用,如内核版本在v5.0和v6.2之间,则底层网卡的UDP/GRO功能自动启用,上层wg网卡则无法支持UDP/GRO功能(相关控制开关本来就没有实际作用)。此外,由于wireguard-go的底层UDP/GRO功能采用前述方法1实现,rx-gro-list和rx-udp-gro-forwarding开关对其压根没有意义,但会受gro开关控制,在切换gro开关后,还需要重新启动wg网卡。
四、关于tx-gso-list和UDP GRO转发
v5.6引入的UDP fraglist GRO/GSO同时新增了两个ethtool开关,即rx-gro-list和tx-gso-list,rx-gro-list已经在《也谈UDP GSO和GRO》和本文做了详细的分析。至于tx-gso-list,在前一篇文章中提到,如入口网卡采用rx-udp-gro-forwarding,出口网卡只要支持tx-udp-segmentation即可卸载或透明转发UDP大包(实际上出口网卡还需要同时支持tx-scatter-gather,只不过因为基本上所有网卡都已具备该特性,默认就不提这一茬了);如入口网卡采用rx-gro-list,出口网卡需要同时支持tx-udp-segmentation、tx-gso-list和tx-scatter-gather-fraglist三种特性,才能卸载或透明转发UDP大包。
前面分析过,本机发送环节,TCP/GSO和UDP/GSO只使用frags方式发送数据,所以用不到网卡的tx-scatter-gather-fraglist特性,事实上目前似乎也没有物理网卡支持该特性。然而在转发环节,取决于入口网卡GRO合并大包所采用方式,转发数据可能包含frags方式或frag_list方式、以及两者的组合。如转发数据为纯frags方式(对应rx-udp-gro-forwarding=on且MAX_SKB_FRAGS=45),出口网卡只需支持tx-udp-segmentation即可;如转发数据为两者组合方式(对应rx-udp-gro-forwarding=on),出口网卡需增加第二个特性tx-scatter-gather-fraglist;如转发数据为纯frag_list方式(对应rx-gro-list=on),出口网卡还需再增加第三个特性tx-gso-list。 UDP/GRO引入tx-gso-list特性的初衷是为了在转发情形下可以独立控制是否由GSO对纯frag_list方式的GRO大包进行软件分段,然而由于纯frag_list方式GRO大包同样受转发网卡tx-scatter-gather-fraglist特性影响,导致tx-gso-list特性不仅显得有点多余,并且增加了纯frag_list方式下UDP/GRO转发的复杂性。 自v5.6引入tx-gso-list特性以来,直到v5.10,只有设置了NETIF_F_GSO_MASK属性的逻辑网卡才支持tx-gso-list和tx-udp-segmentation特性(如bridge/bond/team)。v5.11通过更新NETIF_F_GSO_SOFTWARE定义,使得大部分逻辑网卡也开始支持tx-gso-list和tx-udp-segmentation特性,如wireguard。然而对于bridge/bond/team等设备来说,是否具备NETIF_F_GSO_SOFTWARE特性,完全依赖于下挂网卡。由于:1、支持tx-udp-segmentation特性的硬件网卡依然很少,2、到目前为止,已支持tx-udp-segmentation特性的硬件网卡都不支持tx-gso-list特性。NETIF_F_GSO_SOFTWARE特性的重定义,导致事实上bridge/bond/team不再支持tx-gso-list,并且大部分情形下也不再支持tx-udp-segmentation。v6.2起,虽然virtio_net、tap/tun开始支持tx-udp-segmentation,然而却仍不支持tx-gso-list。
显然,由于网卡(包含逻辑网卡)普遍缺乏对tx-gso-list特性的支持,至少在逻辑网卡层面,纯frag_list方式下UDP GRO大包的转发性能明显低于其他两种方式。
至于v6.10引入的TCP fraglist GRO,因为直接借用了UDP/GRO的rx-gro-list和tx-gso-list特性,也会存在上述tx-gso-list的短板问题。不过由于TCP fraglist GRO的引入,本意就是为了在个别网卡不支持TSO时改善转发效率,实际环境中转发网卡已经不支持TSO,即使支持tx-gso-list也毫无意义,TCP/GRO大包总是需要经由GSO重新拆分。如前所述,纯frag_list方式接收TCP/GRO,只是为了后续转发时改善GSO的拆包效率而已。
v6.10新增的TCP fraglist GRO功能,可以通过抓包观察效果(转发网卡需开启TSO),入口网卡不启用rx-gro-list(off)时,转发网卡上能抓到TCP/GRO大包(物理网卡因为不支持tx-scatter-gather-fraglist,最大包长26130字节;逻辑网卡如支持scatter-gather-fraglist,最大包长可为65226字节)。入口网卡启用rx-gro-list=on时,如前所述,由于物理网卡都不支持tx-scatter-gather-fraglist特性,逻辑网卡不会同时支持tx-scatter-gather-fraglist和tx-gso-list特性,转发网卡只能抓到GSO分段后的小包。
在进行以上抓包测试时,入口网卡请不要选择主机vnetX或虚拟机virtio_net。virtio_net具备rx_gro_hw特性,无需启用内核GRO就能收取GSO大包,在virtio_net上即使启用rx-gro-list=on,也因为透传的GSO大包无需合并,不会转换成纯frag_list方式。主机上的vnetX,其底层为tun驱动,vnetX也能直接收取虚拟机发出的GSO大包,并且其压根没有GRO功能一说。这两种情形下,TCP fraglist GRO功能针对这些GSO大包无法发挥作用,透传的纯frags方式GSO大包将不受转发网卡tx-scatter-gather-fraglist和tx-gso-list特性影响,转发网卡上仍能抓到TCP/GSO大包,从而无法通过抓包观察TCP fraglist GRO效果。如非要选择virtio_net作为入口网卡进行抓包测试,可关闭其rx_gro_hw特性,其实质是关闭主机上对应vnetX网卡的TSO功能。