网卡offload优化方式简析(TSO/UFO/GSO/LRO/GRO/RSS/VIRTIO/VXLAN)

offload(分流/分片/分段)特性,主要是指将原本在协议栈中进行的IP分片、TCP分段、重组、checksum校验等操作,转移到网卡硬件中进行,降低系统CPU的消耗,提高处理性能。

发送模式

TSO (TCP分段: tcp-segmentation-offload) 

从名字来看很直观,就是把tcp分段的过程转移到网卡中进行。当网卡支持TSO机制时,网络协议栈能够将大块buffer推送至网卡,然后网卡执行分片工作,这样减轻了CPU 的负荷,但TSO需要硬件来实现分片功能。其可以直接把不超过滑动窗口大小的payload下传给协议栈,即使数据长度大于MSS,也不会在TCP层进行分段,同样也不会进行IP分片,而是直接传送给网卡驱动,由网卡驱动进行tcp分段操作,并执行checksum计算和包头、帧头的生成工作。

UFO(UDP分片:udp-fragmentation-offload)

是一种专门针对udp协议的特性,主要机制就是将IP分片的过程转移到网卡中进行,用户层可以发送任意大小的udp数据包(udp数据包总长度最大不超过64k),而不需要协议栈进行任何分片操作。目前貌似没找到有支持UFO机制的网卡,主要是应用在虚拟化设备上。

GSO(generic-segmentation-offload) 

相对于TSO和UFO,GSO机制是针对所有协议设计的,更为通用。同时,与TSO、UFO不同的是,GSO主要依靠软件的方式实现,对于网卡硬件没有过多的要求。其基本思想就是把数据分片的操作尽可能的向底层推迟直到数据发送给网卡驱动之前,并先检查网卡是否支持TSO或UFO机制,如果支持就直接把数据发送给网卡,否则的话再进行分片后发送给网卡,以此来保证最少次数的协议栈处理,提高数据传输和处理的效率。

其基本思想就是尽可能的推迟数据分片直至发送到网卡驱动之前,如果硬件不支持TSO分段则由dev_hard_start_xmit中的dev_gso_segment先软件分段的segs赋值给skb->next(skb->next = segs), 如果网卡硬件支持分段则直接将GSO大帧(skb-next)传给网卡驱动(dev_hard_start_xmit中的ndo_start_xmit)(所以它传给virtio驱动的是GSO大帧),然后继续进行IP分片后再发往网卡。GSO自动检测网卡支持特性, 硬件不支持也可以使用GSO它更通用(TSO一定需要硬件支持)。当打开GSO时,GSO会在xmit那块做GSO分片时调用TCP/UDP的回调函数自动添加TCP/UDP头(不使用GSO的只有第一个分片有TCP/UDP头,后面接着的分片是没有的,这也是为什么在虚机里打开TSO/GSO时对于隧道会多出一块数据的原因),然后再调IP层回调函数为每个分片添加IP头。

TSO与GSO的重要区别

1, TSO只有第一个分片有TCP头和IP头,接着的分段只有IP头。意味着第一个分段丢失,所有分段得重传。
2, GSO在分段时会调用TCP或UDP的回调函数(udp4_ufo_fragment)为每个分段都加上IP头,由于分段是通过mss设置的(mss由发送端设置),所以长度仍然可能超过mtu值,所以在IP层还得再分片(代码位于dev_hard_start_xmit)。
 

接收模式

在网卡驱动层面上将接受到的多个TCP数据包聚合成一个大的数据包,然后上传给协议栈处理。这样可以减少协议栈处理的开销,提高系统接收TCP数据的能力和效率。

LRO(Large Receive Offload)

TSO是发,LRO是收。将多个TCP分段聚合成一个skb结构,以减小上层协议栈的skb的开销。skb的数据保存在skb->data中,分段的数据保存在skb_shared_info->frag_list中。

GRO(Generic Receive Offloading)

GSO是发,GRO是收。LRO使用发送方和目的地IP地址,IP封包ID,L4协议三者来区分段,对于从同一个SNAT域的两个机器发向同一目的IP的两个封包可能会存在相同的IP封包ID(因为不是全局分配的ID),这样会有所谓的卷绕的bug。GRO采用发送方和目的地IP地址,源/目的端口,L4协议三者来区分作为改进。所以对于后续的驱动都应该使用GRO的接口,而不是LRO。另外,GRO也支持多协议。

1, 物理网卡不支持GRO时, 使用LRO在驱动处合并了多个skb一次性通过网络栈,对CPU负荷的减轻是显然的。

2, 物理网卡不支持LRO时,使用GRO在从驱动接收数据那一刻合并了多个skb一次性通过网络栈,对CPU负荷的减轻是显然的。

  RSS(Receive Side Scaling) 

具备多个RSS队列的网卡,可以将不同的网络流分成不同的队列,再将这些队列分配到多个CPU核心上进行处理,从而将负荷分散,充分利用多核处理器的能力,提交数据接收的能力和效率。

网卡offload模式的设置

在linux系统下,可以通过ethtool查看各模式的状态并进行设置:
**查看状态 **

ethtool –k 设备名

**设置开关状态 **

ethtool –K 设备名 模式名(缩写)on/off

[root@localhost ~]# ethtool -k eno16777736
Features for eno16777736:
rx-checksumming: off
tx-checksumming: on
 tx-checksum-ipv4: off [fixed]
 tx-checksum-ip-generic: on
 tx-checksum-ipv6: off [fixed]
 tx-checksum-fcoe-crc: off [fixed]
 tx-checksum-sctp: off [fixed]
scatter-gather: on
 tx-scatter-gather: on
 tx-scatter-gather-fraglist: off [fixed]
tcp-segmentation-offload: on
 tx-tcp-segmentation: on
 tx-tcp-ecn-segmentation: off [fixed]
 tx-tcp6-segmentation: off [fixed]
udp-fragmentation-offload: off [fixed]
generic-segmentation-offload: on
generic-receive-offload: on
large-receive-offload: off [fixed]
rx-vlan-offload: on
tx-vlan-offload: on [fixed]
ntuple-filters: off [fixed]
receive-hashing: off [fixed]
highdma: off [fixed]
rx-vlan-filter: on [fixed]
vlan-challenged: off [fixed]
tx-lockless: off [fixed]
netns-local: off [fixed]
tx-gso-robust: off [fixed]
tx-fcoe-segmentation: off [fixed]
tx-gre-segmentation: off [fixed]
tx-ipip-segmentation: off [fixed]
tx-sit-segmentation: off [fixed]
tx-udp_tnl-segmentation: off [fixed]
tx-mpls-segmentation: off [fixed]
fcoe-mtu: off [fixed]
tx-nocache-copy: off
loopback: off [fixed]
rx-fcs: off
rx-all: off
tx-vlan-stag-hw-insert: off [fixed]
rx-vlan-stag-hw-parse: off [fixed]
rx-vlan-stag-filter: off [fixed]
busy-poll: off [fixed]

fixed 项是网卡不支持的功能项目。

tx-udp_tnl-segmentation: 本想通过网卡的这项技术支持硬件加解封装VXLAN报文头来提高vxlan性能,可是集群环境中的网卡不支持,就放弃了。

Offloading 带来的潜在问题

分段offloading可能会带来潜在的问题,比如网络传输的延迟latency,因为packets的大小的增加,大大增加了driver queue的容量(capacity)。比如说,系统一方面在使用大的packet size传输大量的数据,同时在运行许多的交换式应用(interactive application)。因为交互式应用会定时发送许多小的packet,这时候可能会应为这些小的packets被淹没在大的packets之中,需要等待较长的时间才能被处理,这可能会带来不可接受的延迟。在网络上也能看到一些建议,在使用这些offloading技术时如果发现莫名的网络问题,建议先将这些技术关闭后再看看情况有没有改变。
# ethtool -K interface gso off
# ethtool -K interface tso off
 

其他相关技术也在下面做个简单介绍:

VirtIO

在guest中有virtio前端驱动, 如针对网络的virtio-net,和其他驱动如virtio-pci等共同经过virt-queue的notify的trap中断到主机的hypervisor中执行host中面向guest前端驱动的api接口与后端驱动(virtio-backend driver)。guest内核的rx0与tx0两个队列与host的rx与tx两个队列通过socket共享内存交换数据。

下面是如何打开virtqueue的调试功能

echo -n "func virtqueue_add +flmpt" | sudo tee /sys/kernel/debug/dynamic_debug/control 
sudo cat /sys/kernel/debug/dynamic_debug/control | grep virtio 
这样能看到 virtqueue_add() 函数的信息:
drivers/virtio/virtio_ring.c:294 [virtio_ring]virtqueue_add =pmflt "Added buffer head %i to %p\012" 
drivers/virtio/virtio_ring.c:236 [virtio_ring]virtqueue_add =pmflt "Can't add buf len %i - avail = %i\012"

vxlan

 vxlan的实现

从guest出来的tcp数据到达host的vxlan driver时会调用vxlan_xmit, 它的主要逻辑是获取 vxlan dev,然后为 sk_buff 中的每一个skb调用vxlan_xmit_skb方法, vlan_xmit_skb除了计算 tos,ttl,df,src_port,dst_port,md,flags等以外,也调用skb_set_inner_protocol(skb, htons(ETH_P_TEB))设置GSO参数。最后调用udp_tunnel_xmit_skb将skb传给udp tunnel协议栈继续处理。

这样也就进了IP层的ip_finish_output_gso,如果硬件支持,则由硬件调用linux内核中的UDP GSO函数(当有GSO时,由 UDP协议栈提供UDP分片逻辑而不是IP分片逻辑,这使得每个分片都有完整的UDP包头,然后继续IP层的GSO分片。所以GSO本身是对UFO的优化);如果硬件不支持,则在进入device driver queue之前由linux内核调用UDP GSO分片函数,然后再一直往下到网卡。
static int ip_finish_output_gso(struct net *net, struct sock *sk,
struct sk_buff *skb, unsigned int mtu){
...
   #调用回调函数, 对于UDP则是调用UDP的gso_segment回调函数进行UDP GSO分段
   segs = skb_gso_segment(skb, features & ~NETIF_F_GSO_MASK); 
   ...
do {
struct sk_buff *nskb = segs->next;
int err;
segs->next = NULL;
                #有必要则IP分片,因为UDP GSO是按照MSS进行,MSS还是有可能超过IP分段所使用的host物理网卡MTU
err = ip_fragment(net, sk, segs, mtu, ip_finish_output2);
if (err && ret == 0)
ret = err;
segs = nskb;
} while (segs);

这里是udp_offload.c中定义的回调函数:
static const struct net_offload udpv4_offload = {
.callbacks = {
.gso_segment = udp4_ufo_fragment,
                ...
},
};

可见,在整个过程中,有客户机上TCP协议层设置的skb_shinfo(skb)->gso_size始终保持不变为MSS,因此,在网卡中最终所做的针对UDP GSO数据报的GSO分片所依据的分片的长度还是根据skb_shinfo(skb)->gso_size的值即TCP MSS,所以vxlan协议有一个问题,即host的IP分片是根据geuest中TCP连接的MSS来进行的。

1, netif_needs_gso先判断网卡是否支持GSO(guest里的virtio nic肯定支持),if(unlikely(foo))认为foo通常为0, 所以如果网卡支持TSO时dev_gso_segment将返回0(skb->next==NULL),也就是说,当guest网卡的GSO特性打开时,guest会直接将没有分段的GSO大帧传递到virtio-net driver中。
int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev,
struct netdev_queue *txq)
{
...
if (netif_needs_gso(skb, features)) {
if (unlikely(dev_gso_segment(skb, features)))
goto out_kfree_skb;
if (skb->next)
goto gso;
}
gso:
do {
struct sk_buff *nskb = skb->next;
skb->next = nskb->next;
...
rc = ops->ndo_start_xmit(nskb, dev);
...
} while (skb->next);

static int dev_gso_segment(struct sk_buff *skb, netdev_features_t features)
{
struct sk_buff *segs;
segs = skb_gso_segment(skb, features);

/* Verifying header integrity only. */
if (!segs)
return 0;
skb->next = segs;
DEV_GSO_CB(skb)->destructor = skb->destructor;
skb->destructor = dev_gso_skb_destructor;
return 0;
}

2, virtio_net.c中的如下代码定义了virtio-net driver的xmit函数为start_xmit,它会直接将这个GSO大帧通过vring(<=64K)传给host上的virtio backend driver.
static const struct net_device_ops virtnet_netdev = {
        ...
.ndo_start_xmit      = start_xmit,

3, 然后host上的tap和bridge都会原封不动动转发这个GSO大帧(payload, inner-tcp, inner-ip, inner-ethernet)。

4, host上的vxlan driver在原GSO大帧上添加vxlan帧头,从(payload, inner-tcp, inner-ip, inner-ethernet)变成(payload, inner-tcp, inner-ip, inner-ethernet, vxlan)。

5, 如果host不支持GSO,那么在host的IP层直接对外层的UDP分片,注意:只有第1个分片有UDP头,接下来的分片没有UDP头。
   分片1: payload, inner-tcp, inner-ip, inner-ethernet, vxlan,outer-udp, outer-ip
   分片2: payload, inner-tcp, inner-ip, inner-ethernet, vxlan,outer-ip

6, 如果host支持GSO,在host的dev_hard_start_xmit处(代码回到第1步),如果物理网卡支持TSO直接将大帧(payload, inner-tcp, inner-ip, inner-ethernet, vxlan)将给物理网卡的TSO去硬件分片。如果物理网卡不支持TSO将调用skb_gso_segment软件执行GSO。GSO由于会调用UDP的回调函数,但vxlan没有GSO回调函数,所以这里的GSO分片应该每一个分片都有UDP头部,但是只有第一个分片有vxlan头部。
   分片1: payload, inner-tcp, inner-ip, inner-ethernet, vxlan,outer-udp, outer-ip
   分片2: payload, inner-tcp, inner-ip, inner-ethernet,outer-udp, outer-ip

7, 但是host的物理网卡是根据mss(由sender确定,也就是guest设定,而不是由mtu设定)发给远端的。host打开GSO增加outer-udp时可能会造成包大于mss值从而继续在添加outer-ip时做ip分片。所以redhat的最佳实践要求关闭host机上的GSO特性。


从host到guest的包流向

1, 如果host上的GRO打开的话,host上的物理网卡要先将分片重组合并成一个大的GRO包。

2, 包在过路由器时,conntrack需要将分段重组后使用防火墙规则检查,为防止攻击,路由器会有定时器设置一段时间没有做完分段重组就会丢弃清理相应的内存资源,下面参数可以设置分段使用的内存量和
hua@node1:~$ cat /proc/sys/net/ipv4/ipfrag_high_thresh  #一旦达到最高内存分配值,其它分段将被丢弃,直到达到最低内存分配值。 
4194304
hua@node1:~$ cat /proc/sys/net/ipv4/ipfrag_low_thresh
3145728
hua@node1:~$ cat /proc/sys/net/ipv4/ipfrag_time
30

具体到openstack的网络节点,数据流向是:
eth0(pysical nic for public network) -> br-ex (maybe a linux bridge) -> qg-XXX -> qr-XXX -> br-int (ovs bridge) -> ovs-patch-ports -> br-tun (for stt tunnel) -> eth1(pysical nic for management network)
如eth0, eth1的mtu是9000的话,若host的GRO打开的话,eth0收到数据会会分段重组成一个大GRO帧:

1, 如果br-ex是linux bridge,且net.bridge.bridge-nf-call-arptables = 1,在br-ex上就会分段重组然后使用qg-xxx的mtu=1500再进行分片。

2, 即使br-ex不是linux bridge,eth0在重组后做完防火墙后再使用9000再重新分片, 再传到后面1500的虚拟设备又会重组。
所以如果mtu设置不当,还不如将host上的TSO/GSO/GRO关闭(Redhat这是这样推荐的),这样也不会造成一个大帧反复重组和分片的问题了。

另外就是路由器又和NAT搅和在一起的问题,由于在路由器上可能会重组再分片,路由器为了识别一个包属于哪一个分片,会使用<发送方和接收方IP、IP分段ID,L4协议>五元组来区分,由于SNAT在IP分段ID相同的话会出现来自NAT路由器背后不同机器上的分段的五元组相同的情况。所以后来GRO在TSO的基础上将这个五元组改为:<发送方和接收方IP、TOS/协议字段,L4协议>.

3, 在linux bridge上,如果net.bridge.bridge-nf-call-arptables = 1 , netfilter conntrack为了做二层的防欺骗检查也需要先分片重组, 然后重新分片(此时分片采用出口NIC的MTU进行分片, 而从geust往host出时的分片采用的是来自guest的mss值)。故不想linux bridge有防火墙功能的话应该设置:
net.bridge.bridge-nf-call-arptables = 0
net.bridge.bridge-nf-call-ip6tables = 0
net.bridge.bridge-nf-call-iptables = 0

4, 另外,包在过路由器时,

5, 然后virtio继续将大帧传入guest.

VMware STT隧道

如果现在不是Linux + OVS bridge (ovs bridge支持STT,Linux Bridge不支持STT) + virtio, 而是VMware STT呢?有几个问题要搞清楚:

1, STT是加了伪装TCP头利用网卡的TSO特性硬件分片的,所以它走的是TCP,不是UDP(因为很多网卡不支持UDP的硬件分段)。

2, NSX Bridge是如何实现的,是否有类似于Linux中的net.bridge.bridge-nf-call-iptables, (tap -> NSX bridge -> ??? -> dev_queue_xmit )
    这个网址(http://blog.scottlowe.org/learning-nvp-nsx/)有一堆关于nsx的介绍,nsx也是用了ovs的,bridge-nf-call-iptables只是linux bridge的特性,ovs bridge不存在这个特性。但nsx与neutron集成时可能仍然使用tap->qbr->qvb->qvr->br-int->ovs-patch-port->phy-br-eth0,在这里面qbr与phy-br-eth0均有可能是Linux Bridge.

3, NSX实现了virtio吗?
    上面已回答,nsx仍然使用ovs。virtio是qemu的特性,与nsx无关。

解决办法

链连[10]上说:
1, 若用隧道guest中设置mtu

2, echo 0 > /proc/sys/net/bridge/bridge-nf-call-iptable (可选,如果有Linux Bridge的话)

3, 关闭host上的GSO, TSO, GRO (如果为使用conntract特性没有关闭bridge-nf-call-iptable的话)

4, 重点关注host机上物理网卡与Linux bridge上的mtu,其他的虚拟网卡的mtu可以默认设置很大如65000 (可选)

5, 也可以试试关闭tx与rx两个offload特性,sudo ethtool --offload eth0 rx off tx off sg off tso off, 注意:在tx与rx打开时,chksum是在硬件网卡处做的,tcpdump的输出“cksum 0x0a2b (incorrect -> xxx”应该是正常的。
 

--------------------- 

参考链接:https://blog.csdn.net/codeforce/article/details/65437359?utm_source=blogxgwz3

参考链接:https://blog.csdn.net/quqi99/article/details/51066800

猜你喜欢

转载自blog.csdn.net/jiangganwu/article/details/83417381
TSO
ufo