一、网络分段卸载技术
先介绍 MSS:
MSS 是 TCP 数据段每次能够传输的最大数据分段的长度。为了达到最佳的传输效能,TCP 协议在建立连接的时候通常要协商双方的 MSS 值,这个值 TCP 协议在实现的时候往往用 MTU 值代替( MSS = MTU - IP 数据包包头大小20Bytes - TCP 数据段的包头大小20Bytes),所以在默认以太网 MTU 为 1500 bytes 时,MSS为 1460。
为什么需要segmentation offloading ??
Linux 内核传输层和网络层都要做大量的计算工作,具体见上图,这些计算都在服务器的主 CPU 中进行。这里有一些网络协议栈计算所需要的 CPU 资源的一些参考数据。大体上,发送或者接收 1 bit/s 的数据需要 1 赫兹的 CPU 处理能力,也就是说,5 Git/s (625 MB/s) 的网络流量大概需要 5 GHz 的 CPU 处理能力,相当于此时需要 2 个 2.5 Ghz 的多核处理器。因为以太网是单向的,发送和接收 10 Gbit/s (吞吐量就是 20 10 Gbit/s)时,大概需要 8 个 2.5 GHz 的 CPU 内核。
这些计算大概可以分为两类:
- (1)数据计算,比如校验和计算和验证、分包和组包等,这个和所处理的 packets 的数量有关
- (2)数据传输和上下文切换带来的 overhead,这个和传输和切换的次数有关。
为了解决问题,考虑到越来越多的物理网卡具有较强的处理能力,就出现了两个思路:
- (1)如果网卡能够支持某些 Linux 内核协议栈所承担的计算任务,那么就可以将这些计算从协议栈 offload (卸载)到物理网卡。
- (2)如果网卡能支持这些计算,那么尽可能地将这些计算在 Linux 内核网络栈中延后(传输过程)和提前(接收过程)来减少 overhead。
以 TCP 分组或者 IP 分片为例,延迟该过程,可以减少在网络栈中传输和处理的 packets 的数目,从而减少数据传输和上下文切换所需要的主 CPU 计算能力。
目前内核新特性中,对分段卸载技术的应用:
TSO/LRO/UFO需要硬件支持,TSO针对TCP发送,UFO针对UDP发送(早已废弃),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实现对虚拟设备的支持还不够完善。
二、Segmentation offloading 技术
2.1 TSO (TCP Segmentation Offloading)
2.1.1 TCP Segmentation (TCP 分段)
MSS(Maxium Segment Size): MSS 是 TCP 数据段每次能够传输的最大数据分段的长度。为了达到最佳的传输效能,TCP 协议在建立连接的时候通常要协商双方的 MSS 值,这个值 TCP 协议在实现的时候往往用 MTU 值代替( MSS = MTU - IP 数据包包头大小20Bytes - TCP 数据段的包头大小20Bytes),所以在默认以太网 MTU 为 1500 bytes 时,MSS为 1460。
TCP 分段:当网络应用发给 TCP 的 message 的长度超过 MSS 时,TCP 会对它按照 MSS 的大小将其分为多个小的 packet,并且在每个 packet 上添加 TCP Header 成为一个 TCP 段(segement)。
2.1.2 TSO
TSO 是一种利用网卡分割大数据包,减小 CPU 负荷的一种技术,也被叫做 LSO (Large segment offload) ,如果数据包的类型只能是 TCP,则被称之为 TSO,如果硬件支持 TSO 功能的话,也需要同时支持硬件的 TCP 校验计算和分散 - 聚集 (Scatter Gather) 功能。可以看到 TSO 的实现,需要一些基本条件,而这些其实是由软件和硬件结合起来完成的,对于硬件,具体说来,硬件能够对大的数据包进行分片,分片之后,还要能够对每个分片附着相关的头部。
TSO 就是将由 TCP 协议栈所做的 TCP 分段交给具有这种能力的物理网卡去做,因此它需要如下支持:
- 物理网卡支持。
- Linux 网卡驱动支持。可以使用 ethtool -K ethX tso on 命令打开网卡和驱动对 TSO 的支持,如果返回错误则表示不支持。
- 还需要 Net:TCP checksum offloading and Net:Scatter Gather 支持。
使用 TSO 以后,应用发出的大的数据块在不超过 64k 的情况下,将会直接经过Linux 网络栈发到网卡的驱动的 driver queue,然后在网卡中根据 skb 中的预设分组数据(主要是 MSS)对它执行 TCP 分段。
问题:目前在代码tcp_sendmsg 函数中,没有看到 有64K的限制。
TSO 只能支持 TCP协议 ,而GSO 可以支持tcpv4,tcpv6,udp 等协议,GSO 是对TSO 的扩展,软件上的逻辑。
补充:通过ethtool -k 网卡设备名,来查看是否打开TSO
nvidia@tegra-ubuntu:/opt/usr$ ethtool -k mgbe3_0
上面标注的红色线可以看出:tx-checksumming-ipv4:on,tx-checksumming-ipv6:on 和tx-scatter-gather: on 和tx-tcp-segmentation:on 是打开的,所以tcp 校验 和分散-聚集 是硬件支持的。
2.2 UFO - UDP Fragmentation Offload (虽然已废弃,也介绍一下)
UDP 数据报,由于它不会自己进行分段,因此当长度超过了 MTU 时,会在网络层进行 IP 分片。同样,ICMP(在网络层中)同样会出现IP分片情况。
2.2.1 IP fragmentation (IP 分片)
MTU 和 IP 分片:
- MTU:上文已经说过了,MTU 是链路层中的网络对数据帧的一个限制,依然以以太网为例,默认 MTU 为1500字节。
- IP 分片:一个 IP 数据报在以太网中传输,如果它的长度大于该 MTU 值,就要进行分片传输,使得每片数据报的长度小于MTU。分片传输的 IP 数据报不一定按序到达,但 IP 首部中的信息能让这些数据报片按序组装。IP数据报的分片与重组是在网络层进完成的。
IP 分片和 TCP 分段的区别:
- IP 数据报分片后,只有第一片带有UDP首部或ICMP首部,其余的分片只有IP头部,到了端点后根据IP头部中的信息再网络层进行重组。而 TCP 报文段的每个分段中都有TCP 首部,到了端点后根据 TCP 首部的信息在传输层进行重组。IP数据报分片后,只有到达目的地后才进行重组,而不是向其他网络协议,在下一站就要进行重组。
- 对 IP 分片的 TCP segment (段)来说,即使只丢失一片数据, TCP 层也要重新传整个数据报。这是因为IP层本身没有超时重传机制------由更高层(比如TCP)来负责超时和重传。当来自TCP报文段的某一段(在IP数据报的某一片中)丢失后,TCP在超时后会重发整个TCP报文段,该报文段对应于一份IP数据报(可能有多个IP分片),没有办法只重传数据报中的一个数据分片。这就是为什么对 TCP 来说要尽量避免 IP 分片的原因。
IP 分片和 TCP 分段的关系:
- 在非虚拟化环境中,MSS 肯定是要比 MTU 小的( MSS = MTU - IP 数据包包头大小20Bytes - TCP 数据段的包头大小20Bytes),因此,每个 TCP 分组不再需要 IP 分片就可以直接交给网卡去传输。
- 在虚拟户环境中,如果配置不当,虚机网络应用的 TCP 连接的 MSS 比宿主机物理网卡的 MTU 大的情况下,宿主机上还是会执行 IP 分片的。
2.2.2 UFO
UDP 协议层本身不对大的数据报进行分片,而是交给 IP 层去做。因此,UFO 就是将 IP 分片 offload 到网卡(NIC)中进行。其原理同 TSO。
"IPv4/IPv6: UFO (UDP Fragmentation Offload) Scatter-gather approach: UFO is a feature wherein the Linux kernel network stack will offload the IP fragmentation functionality of large UDP datagram to hardware. This will reduce the overhead of stack in fragmenting the large UDP datagram to MTU sized packets"
“ IPv4 / IPv6:UFO(UDP碎片分载)分散收集方法:UFO是一种功能,Linux内核网络堆栈会将大型UDP数据报的IP分片功能分担给硬件。这将减少在分割大型UDP数据报时的堆栈开销。 UDP数据报到MTU大小的数据包”
2.3 GSO -Generic Segemetation Offload
TSO 是使得网络协议栈能够将大块 buffer 推送至网卡,然后网卡执行分片工作,这样减轻了 CPU 的负荷,但 TSO 需要硬件来实现分片功能;而性能上的提高,主要是因为延缓分片而减轻了 CPU 的负载,因此,可以考虑将 TSO 技术一般化,因为其本质实际是延缓分片,这种技术,在 Linux 中被叫做 GSO(Generic Segmentation Offload)。它比 TSO 更通用,原因在于它不需要硬件的支持分片就可使用,对于支持 TSO 功能的硬件,则先经过 GSO 功能,然后使用网卡的硬件分片能力执行分片;而对于不支持 TSO 功能的网卡,将分片的执行,放在了将数据推送到网卡的前一刻,也就是在调用驱动的 xmit 函数前。
2.3.1 对于 UDP,在物理网卡不支持 UFO 时,使用和不使用 GSO 的情形
注意这两者中间的重要区别:
- 当没有 GSO 时,UDP 包会在 IP 层做 IP 分片,这会带来比较严重的问题,包括:依赖于 PMTU,这个技术在很多的实际网络中有时候无法工作;在高速网络中,IPv4 packet ID 有时候会重复而导致数据损坏(Breaks down on high-bandwidth links because the IPv4 16-bit packet ID value can wrap around, causing data corruption);它将 UDP 头算在 payload 内,因此只有第一个分片有 UDP 头,因此一个分片丢失会导致整个IP包的损失。
- 当有 GSO 时,由 Linux UDP 协议栈提供 UDP 分片逻辑而不是 IP 分片逻辑,这使得每个分片都有完整的 UDP 包头,然后继续 IP 层的 GSO 分片。所以 GSO 本身是对 UFO 的优化。
2.3.2 GSO for UDP 代码分析
路径: kernel/net/ipv4/udp_offload.c
- UDP GSO 回调函数:
static const struct net_offload udpv4_offload = {
.callbacks = {
.gso_segment = udp4_ufo_fragment,
.gro_receive = udp4_gro_receive,
.gro_complete = udp4_gro_complete,
},
};
int __init udpv4_offload_init(void)
{
return inet_add_offload(&udpv4_offload, IPPROTO_UDP);
}
函数 udp4_ufo_fragment 最终调用 skb_segment 函数进行分片:
/**
* skb_segment - Perform protocol segmentation on skb.
* @head_skb: buffer to segment
* @features: features for the output path (see dev->features)
*
* This function performs segmentation on the given skb. It returns
* a pointer to the first in a list of new skbs for the segments.
* In case of error it returns ERR_PTR(err).
*/
struct sk_buff *skb_segment(struct sk_buff *head_skb,
netdev_features_t features)
skb_segment 函数源码太多,没有贴出来。
- 在函数 static int ip_finish_output_gso(struct net *net, struct sock *sk, struct sk_buff *skb, unsigned int mtu) 中能看到,首先按照 MSS 做 GSO,然后在调用 ip_fragment 做 IP 分片。可见,在通常情况下(虚机 TCP MSS 要比物理网卡 MTU 小),只做 UDP GSO 分段,IP 分片是不需要做的;只有在特殊情况下 (虚机 TCP MSS 超过了宿主机物理网卡 MTU),IP 分片才会做。这个和试验中看到的效果是相同的。
根据 linux之网络子系统-网络协议栈 发包收包详解-CSDN博客 udp 发包流程图,在补充一下什么时候调用 udpv4_offload 回调和ip_finish_output_gso ?
截取局部的流程图:
对于UDP 发包流程中,在执行到__ip_finish_output 时,调用流程如下:
__ip_finish_output->
ip_finish_output_gso->
skb_gso_segment->
__skb_gso_segment->
skb_mac_gso_segment ->#net/core/dev.c 路径
inet_gso_segment->
udpv4_gso_segment
接着看一下 skb_mac_gso_segment 函数:(链路层的分段函数)
/**
* skb_mac_gso_segment - mac layer segmentation handler.
* @skb: buffer to segment
* @features: features for the output path (see dev->features)
*/
struct sk_buff *skb_mac_gso_segment(struct sk_buff *skb,
netdev_features_t features)
{
struct sk_buff *segs = ERR_PTR(-EPROTONOSUPPORT);
struct packet_offload *ptype;
int vlan_depth = skb->mac_len;
__be16 type = skb_network_protocol(skb, &vlan_depth);
if (unlikely(!type))
return ERR_PTR(-EINVAL);
__skb_pull(skb, vlan_depth);
rcu_read_lock();
list_for_each_entry_rcu(ptype, &offload_base, list) {
if (ptype->type == type && ptype->callbacks.gso_segment) {
//执行udpv4_offload 函数的回调
segs = ptype->callbacks.gso_segment(skb, features);
break;
}
}
rcu_read_unlock();
__skb_push(skb, skb->data - skb_mac_header(skb));
return segs;
}
EXPORT_SYMBOL(skb_mac_gso_segment);
上面可以看到,执行了 packet_offload 对应类型的 gso_segment 回调函数。在源码中,定义如下:
/*
* IP protocol layer initialiser
*/
static struct packet_offload ip_packet_offload __read_mostly = {
.type = cpu_to_be16(ETH_P_IP),
.callbacks = {
.gso_segment = inet_gso_segment,
.gro_receive = inet_gro_receive,
.gro_complete = inet_gro_complete,
},
};
...
static int __init ipv4_offload_init(void)
{
/*
* Add offloads
*/
if (udpv4_offload_init() < 0)
pr_crit("%s: Cannot add UDP protocol offload\n", __func__);
if (tcpv4_offload_init() < 0)
pr_crit("%s: Cannot add TCP protocol offload\n", __func__);
if (ipip_offload_init() < 0)
pr_crit("%s: Cannot add IPIP protocol offload\n", __func__);
dev_add_offload(&ip_packet_offload);
return 0;
}
.....
可以看出 ip_packet_offload 定义了 packet_offload 类型的回调函数 inet_gso_segment.并且udpv4_offload_init 和tcpv4_offload_init 定义了UDP 和TCP 的gso 回调函数,这两个回调函数,是在inet_gso_segment 函数中进行判断执行回调。下面看一下 inet_gso_segment 函数:
struct sk_buff *inet_gso_segment(struct sk_buff *skb,
netdev_features_t features)
{
bool udpfrag = false, fixedid = false, gso_partial, encap;
struct sk_buff *segs = ERR_PTR(-EINVAL);
const struct net_offload *ops;
unsigned int offset = 0;
struct iphdr *iph;
int proto, tot_len;
int nhoff;
int ihl;
int id;
skb_reset_network_header(skb);
nhoff = skb_network_header(skb) - skb_mac_header(skb);
if (unlikely(!pskb_may_pull(skb, sizeof(*iph))))
goto out;
iph = ip_hdr(skb);
ihl = iph->ihl * 4;
if (ihl < sizeof(*iph))
goto out;
id = ntohs(iph->id);
proto = iph->protocol;
/* Warning: after this point, iph might be no longer valid */
if (unlikely(!pskb_may_pull(skb, ihl)))
goto out;
__skb_pull(skb, ihl);
encap = SKB_GSO_CB(skb)->encap_level > 0;
if (encap)
features &= skb->dev->hw_enc_features;
SKB_GSO_CB(skb)->encap_level += ihl;
skb_reset_transport_header(skb);
segs = ERR_PTR(-EPROTONOSUPPORT);
if (!skb->encapsulation || encap) {
udpfrag = !!(skb_shinfo(skb)->gso_type & SKB_GSO_UDP);
fixedid = !!(skb_shinfo(skb)->gso_type & SKB_GSO_TCP_FIXEDID);
/* fixed ID is invalid if DF bit is not set */
if (fixedid && !(ip_hdr(skb)->frag_off & htons(IP_DF)))
goto out;
}
ops = rcu_dereference(inet_offloads[proto]);
if (likely(ops && ops->callbacks.gso_segment)) {
// 执行udp 或者tcp 的gso 回调函数
segs = ops->callbacks.gso_segment(skb, features);
if (!segs)
skb->network_header = skb_mac_header(skb) + nhoff - skb->head;
}
if (IS_ERR_OR_NULL(segs))
goto out;
gso_partial = !!(skb_shinfo(segs)->gso_type & SKB_GSO_PARTIAL);
skb = segs;
do {
iph = (struct iphdr *)(skb_mac_header(skb) + nhoff);
if (udpfrag) {
iph->frag_off = htons(offset >> 3);
if (skb->next)
iph->frag_off |= htons(IP_MF);
offset += skb->len - nhoff - ihl;
tot_len = skb->len - nhoff;
} else if (skb_is_gso(skb)) {
if (!fixedid) {
iph->id = htons(id);
id += skb_shinfo(skb)->gso_segs;
}
if (gso_partial)
tot_len = skb_shinfo(skb)->gso_size +
SKB_GSO_CB(skb)->data_offset +
skb->head - (unsigned char *)iph;
else
tot_len = skb->len - nhoff;
} else {
if (!fixedid)
iph->id = htons(id++);
tot_len = skb->len - nhoff;
}
iph->tot_len = htons(tot_len);
ip_send_check(iph);
if (encap)
skb_reset_inner_headers(skb);
skb->network_header = (u8 *)iph - skb->head;
skb_reset_mac_len(skb);
} while ((skb = skb->next));
out:
return segs;
}
EXPORT_SYMBOL(inet_gso_segment);
2.3.3 对 TCP,在网卡不支持 TSO 时,使用和不使用 GSO 的情形
两者都是 TCP 分片,只是位置不同。
只要打开GSO 支持,都是在 ip_finish_output_gso 进行分片。对于TCP 来说,如果不支持GSO,就是在TCP 层就开始分片,而对于UDP,在IP层进行分片。
2.3.4 GSO for TCP 代码逻辑分析
在源码中定义如下:
static const struct net_offload tcpv4_offload = {
.callbacks = {
.gso_segment = tcp4_gso_segment,
.gro_receive = tcp4_gro_receive,
.gro_complete = tcp4_gro_complete,
},
};
int __init tcpv4_offload_init(void)
{
return inet_add_offload(&tcpv4_offload, IPPROTO_TCP);
}
函数栈 和UDP 一致:
__ip_finish_output->
ip_finish_output_gso->
skb_gso_segment->
__skb_gso_segment->
skb_mac_gso_segment ->#net/core/dev.c 路径
inet_gso_segment->
tcp4_gso_segment
skb_mac_gso_segment 这个函数会根据不同协议,来调用不用的offload 函数。都是在IP 层进行分片了,可能就符合了 IP分层的理念。
2.4 LRO (Large Receive Offload) (已废弃)
inux 在 2.6.24 中加入了支持 IPv4 TCP 协议的 LRO (Large Receive Offload) ,它通过将多个 TCP 数据聚合在一个 skb 结构,在稍后的某个时刻作为一个大数据包交付给上层的网络协议栈,以减少上层协议栈处理 skb 的开销,提高系统接收 TCP 数据包的能力。当然,这一切都需要网卡驱动程序支持。理解 LRO 的工作原理,需要理解 sk_buff 结构体对于负载的存储方式,在内核中,sk_buff 可以有三种方式保存真实的负载:
- 数据被保存在 skb->data 指向的由 kmalloc 申请的内存缓冲区中,这个数据区通常被称为线性数据区,数据区长度由函数 skb_headlen 给出
- 数据被保存在紧随 skb 线性数据区尾部的共享结构体 skb_shared_info 中的成员 frags 所表示的内存页面中,skb_frag_t 的数目由 nr_frags 给出,skb_frags_t 中有数据在内存页面中的偏移量和数据区的大小
- 数据被保存于 skb_shared_info 中的成员 frag_list 所表示的 skb 分片队列中
合并了多个 skb 的超级 skb,能够一次性通过网络协议栈,而不是多次,这对 CPU 负荷的减轻是显然的。
2.5 GRO (Generic Receive Offloading)
前面的 LRO 的核心在于:在接收路径上,将多个数据包聚合成一个大的数据包,然后传递给网络协议栈处理,但 LRO 的实现中存在一些瑕疵:
- 数据包合并可能会破坏一些状态
- 数据包合并条件过于宽泛,导致某些情况下本来需要区分的数据包也被合并了,这对于路由器是不可接收的
- 在虚拟化条件下,需要使用桥接功能,但 LRO 使得桥接功能无法使用
- 实现中,只支持 IPv4 的 TCP 协议
而解决这些问题的办法就是新提出的 GRO。首先,GRO 的合并条件更加的严格和灵活,并且在设计时,就考虑支持所有的传输协议,因此,后续的驱动,都应该使用 GRO 的接口,而不是 LRO,内核可能在所有先有驱动迁移到 GRO 接口之后将 LRO 从内核中移除。GRO 和 LRO 的最大区别在于,GRO 保留了每个接收到的数据包的熵信息,这对于像路由器这样的应用至关重要,并且实现了对各种协议的支持。以 IPv4 的 TCP 为例,匹配的条件有:
- 源 / 目的地址匹配
- TOS/ 协议字段匹配
- 源 / 目的端口匹配
另外一篇文章是讲GRO:linux之网络子系统-GRO机制分析-CSDN博客
2.6 TCP/UDP Segementation Offload 小结
2.6.1 小结
2.6.2 性能对比
从这图也可以看出:
- 对 TCP 来说,在 CPU 资源充足的情况下,TSO/GSO 能带来的效果不大,但是在CPU资源不足的情况下,其带来的改观还是很大的。
- 对 UDP 来说,其改进效果一般,改进效果不超过 20%。所以在 VxLAN 环境中,其实是可以把 GSO 关闭,从而避免它带来的一些潜在问题。
2.6.3 Offloading 带来的潜在问题
分段offloading 可能会带来潜在的问题,比如网络传输的延迟 latency,因为 packets 的大小的增加,大大增加了 driver queue 的容量(capacity)。比如说,系统一方面在使用大的 packet size 传输大量的数据,同时在运行许多的交换式应用(interactive application)。因为交互式应用会定时发送许多小的packet,这时候可能会应为这些小的 packets 被淹没在大的 packets 之中,需要等待较长的时间才能被处理,这可能会带来不可接受的延迟。
在网络上也能看到一些建议,在使用这些 offloading 技术时如果发现莫名的网络问题,建议先将这些技术关闭后再看看情况有没有改变
2.7 对网卡分段技术的总结
查看自己板子上的网卡驱动打开选项:
通过上面可以发现:gso 功能