Packet fragmentation and segmentation offload in UDP and VXLAN

| 分类 Linux  | 标签 network  vxlan  ufo 

对于以太网,每个传输的数据帧的大小受限于PMTU,PMTU一般为1500字节(不包括L2 header本身)。当应用层下发的数据超过PMTU(严格来说是PMTU - L4 header - L3 header)时,就会在L4/L3进行分片,保证下发给NIC的数据不会超过PMTU。当然,对于TSO/GSO/UFO,情况又不太一样。我们先看看UDP的分片过程,从而更好的理解TSO/GSO。

UDP fragmentation

老的内核通常在IP层处理IP分段,IP层可以接收0~64KB的数据。因此,当数据IP packet大于PMTU时,就必须把数据分成多个IP分段。 较新的内核中,L4会尝试进行分段:L4不会再把超过PMTU的缓冲区直接传给IP层,而是传递一组和PMTU相匹配的缓冲区。这样,IP层只需要给每个分段增加IP报头。但是这并不意味着IP层就不做分段的工作了,一些情况下,IP层还会进行分段操作,见ip_fragment。

对于UDP,分片工作主要在ip_append_data(ip_apepend_page)中完成。

Memory allocation in skb_buff

ip_append_data会创建一个或者多个skb_buff对象,每个skb_buff表示一个IP packet。并根据下面两个因素决给skb分配的内存大小:

  • <1> MSG_MORE:如果socket设置了MSG_MORE,意味着应用层还有数据要发送,所以,可以分配大一点的缓冲区(PMTU),使得后续的数据可以合并到同一个skb。

  • <2>Scatter/Gather I/O:如果NIC支持SG,分段可以更有效的方式存储至内存页面(不用每次都分配PMTU大小缓冲区)。

                 if ((flags & MSG_MORE) &&
                     !(rt->u.dst.dev->features&NETIF_F_SG))
                     alloclen = mtu;
                 else
                     alloclen = datalen + fragheaderlen;
				 ///最后一个分段,考虑trailer是否存在
                 if (datalen == length)
                         alloclen += rt->u.dst.trailer_len;

如果设置了MSG_MORE且设备不支持SG,则分配PMTU大小的skb,否则,分配只需要能够容纳当前数据大小的skb。

查看NIC是否支持SG IO:

# ethtool -k eth1|grep scatter
scatter-gather: on
        tx-scatter-gather: on
        tx-scatter-gather-fraglist: off [fixed]
  • IP packet that does not need fragmentation, with IPsec

不需要分片的IP packet(IPsec)的skb的内存结构:

注意,对于IPsec,exthdrlen为IPsec header的长度,对于普通IP packet,exthdrlen为0。

  • Fragmentation without Scatter/Gather I/O

如果设备不支持SG(Scatter/Gather I/O),会根据是否设置MSG_MORE,情况会不一样:

(1)No SG and No MSG_MORE

左下角的对象为ip_append_data需要处理的数据,length=x+y。由于长度(include L4 header)大于PMTU(严格来说是length+fraghdrlen > PMTU),会分成2个skb,第1个skb的大小为PMTU(include L3 header),第2个skb存储剩下的数据。

值得注意是第2个skb没有L4 header。

(2)No SG but MSG_MORE

与情况(1)的区别在于,由于设置了MSG_MORE,所以第2个skb仍然分配PMTU大小的空间。当再次调用ip_append_data的时候,会先将数据填充到第2个skb的剩余空间,然后再创建第3个skb(如果第2个skb空间不够)。

  • Fragmentation with Scatter/Gather I/O

如果设备支持SG,skb->data指向的内存只会在SKB第一次填充数据时才会使用(skb->data指向的内存刚好能容下第一次调用ip_append_data的数据),接着调用ip_append_data的数据会写到专门分配的内存页中。当支持SG时,第二次调用ip_append_data时,数据如下存放:

当第二次调用ip_append_data时,数据(S1)会写到由frags指向的page中。S1不需要header:skb_buff实例中的所有数据分片(fragments)都属于同一个IP packet,这也意味着X+S1仍然小于PMTU。

每个skb_buff都有一个struct skb_shared_info的字段(可以通过skb_shinfo(skb)得到)。

//include/linux/skb_buff.h
struct skb_shared_info {
	unsigned char	nr_frags;///frags number
...
	struct sk_buff	*frag_list; ///IP packet所有分段(skb)链表,在从L4->L3时,内核会将一个IP packet的所有skb对象都加到该链表
...
	/* must be last field, see pskb_expand_head() */
	skb_frag_t	frags[MAX_SKB_FRAGS]; ///page array
};


/* To allow 64K frame to be packed as single skb without frag_list we
 * require 64K/PAGE_SIZE pages plus 1 additional page to allow for
 * buffers which do not start on a page boundary.
 *
 * Since GRO uses frags we allocate at least 16 regardless of page
 * size.
 */
#if (65536/PAGE_SIZE + 1) < 16
#define MAX_SKB_FRAGS 16UL
#else
#define MAX_SKB_FRAGS (65536/PAGE_SIZE + 1)
#endif

struct skb_frag_struct {
	struct {
		struct page *p;
	} page;

	__u16 page_offset;
	__u16 size;
};

skb_shared_info->frags指向这些缓冲区,nr_frags记录有多少个缓冲区(一个缓冲区一个page)最多MAX_SKB_FRAGS个,基于一个IP packet最大64KB。

如果设备不支持SG,frags数组是不会使用的,内核会按PMTU给skb->data分配内存:

一个SKB可能包括多个skb_frag_struct,这些frags可能指向一个或者多个page。如下所示,SKB有2个frags,指向同一个page。

值得注意的是,SG与IP packet分段是互相独立的。SG IO只是让程序和硬件可以使用非相邻的内存区域,就像它们是相邻的那样。但是,每个IP分段必须受限于PMTU。也就是说,即使PAGE_SIZE大于PMTU,但是sk_buff的数据(skb->data所指)加上frags所引用的数据不能超过PMTU。一旦超过,就要创建新的skb_buff。

再记一遍,frags指向的分片与IP packet分段是两回事,只要设备支持SG,skb_buff就可能使用frags保存数据,

而IP packet的每个分段都对应一个skb_buff对象。

另外一个值得注意是skb_shared_info->frag_list,它表示IP packet所有分段(skb)链表,在从L4->L3时,内核会将一个IP packet的所有skb对象都加到该链表:

udp_push_pending_frames -> ip_finish_skb -> __ip_make_skb
/*
 *	Combined all pending IP fragments on the socket as one IP datagram
 *	and push them out.
 */
struct sk_buff *__ip_make_skb(struct sock *sk,
			      struct flowi4 *fl4,
			      struct sk_buff_head *queue,
			      struct inet_cork *cork)
{
...
	if ((skb = __skb_dequeue(queue)) == NULL)
		goto out;
	tail_skb = &(skb_shinfo(skb)->frag_list);

	/* move skb->data to ip header from ext header */
	if (skb->data < skb_network_header(skb))
		__skb_pull(skb, skb_network_offset(skb));

	///move from sock->sk_write_queue to skb_shinfo(skb)->frag_list
	while ((tmp_skb = __skb_dequeue(queue)) != NULL) {
		__skb_pull(tmp_skb, skb_network_header_len(skb));
		*tail_skb = tmp_skb;
		tail_skb = &(tmp_skb->next);
		skb->len += tmp_skb->len;
		skb->data_len += tmp_skb->len;
		skb->truesize += tmp_skb->truesize;
		tmp_skb->destructor = NULL;
		tmp_skb->sk = NULL;
	}
...

UDP fragmentation example

发送1472字节的UDP数据,不会发生分片:

发送1473字节的时候,发生分片:

注意,第2个分片对应的frame的总长度为35字节,包括L2 header(14 bytes)、L3 header(20 bytes)、data(1 byte)。

发送过程如下:

More details.

几个注意点:

(1)2个分片会创建2个skb_buff对象

(2)__ip_make_skb将除第1个SKB的其它SKB加到第1个SKB的frag_list

(3)ip_fragment对frag_list中的每个SKB设置IP header

TCP fragmentation

每个TCP数据包(segment)的大小受MSS(TCP_MAXSEG选项)限制。最大报文段长度 ( MSS )表示 TCP 传往另一端的最大块数据的长度。当一个连接建立时(SYN packet), 连接的双方都要通告各自的MSS。

一般说来,如果没有分段发生, MSS还是越大越好。报文段越大允许每个报文段传送的数据就越多,相对IP和TCP首部有更高的网络利用率。当TCP发送一个SYN时,或者是因为一个本地应用进程想发起一个连接,或者是因为另一端的主机收到了一个连接请求,它能将MSS值设置为外出接口上的MTU长度减去固定的IP首部(20 bytes)和TCP首部长度(20 bytes)。对于一个以太网,MSS值可达1460字节。详细参考tcp_sendmsg。

TCP/SCTP会将数据按MTU进行切片,然后3层的工作只需要给传递下来的切片加上 ip头就可以了(也就是说调用这个函数的时候,其实4层已经切好片了)。所以ip_queue_xmit(TCP调用该函数将数据下发至L3)的实现非常简单。

Fragmentation in L3

一般来说,L4已经根据PMTU完成分片,L3只需要给每个分片加下IP header即可。如果skb的数据长度仍然超过PMTU,L3就会进行分片,保证每个分片的大小不超过PMTU:

int ip_output(struct sk_buff *skb)
{
	IP_INC_STATS(IPSTATS_MIB_OUTREQUESTS);
	///大于MTU(且不支持GSO),必须在IP层进行分片
	if (skb->len > dst_pmtu(skb->dst) && !skb_shinfo(skb)->tso_size)
		return ip_fragment(skb, ip_finish_output);
	else
		return ip_finish_output(skb);
}

///skbuff.h
static inline bool skb_is_gso(const struct sk_buff *skb)
{
	return skb_shinfo(skb)->gso_size;
}

有几个地方值得注意:

  • (1) 第1个skb_buff->len等于skb->data、skb_shinfo(skb)->frags和skb_shinfo(skb)->frag_list中的所有数据之和。

  • (2) 对于UDP,只有NIC支持UFO,skb_shinfo(skb)->gso_size才会被设置:

static int __ip_append_data(struct sock *sk,
			    struct flowi4 *fl4,
			    struct sk_buff_head *queue,
			    struct inet_cork *cork,
			    struct page_frag *pfrag,
			    int getfrag(void *from, char *to, int offset,
					int len, int odd, struct sk_buff *skb),
			    void *from, int length, int transhdrlen,
			    unsigned int flags)
{
...
	if (((length > mtu) || (skb && skb_has_frags(skb))) && ///len(L4 header + user data) > mtu and UFO
	    (sk->sk_protocol == IPPROTO_UDP) &&
	    (rt->dst.dev->features & NETIF_F_UFO) && !rt->dst.header_len &&
	    (sk->sk_type == SOCK_DGRAM)) {///UDP offload
		err = ip_ufo_append_data(sk, queue, getfrag, from, length,
					 hh_len, fragheaderlen, transhdrlen,
					 maxfraglen, flags);
		if (err)
			goto error;
		return 0;
	}
...



static inline int ip_ufo_append_data(struct sock *sk,
			struct sk_buff_head *queue,
			int getfrag(void *from, char *to, int offset, int len,
			       int odd, struct sk_buff *skb),
			void *from, int length, int hh_len, int fragheaderlen,
			int transhdrlen, int maxfraglen, unsigned int flags)
{
	struct sk_buff *skb;
	int err;

	/* There is support for UDP fragmentation offload by network
	 * device, so create one single skb packet containing complete
	 * udp datagram
	 */
	if ((skb = skb_peek_tail(queue)) == NULL) {
		skb = sock_alloc_send_skb(sk,
			hh_len + fragheaderlen + transhdrlen + 20,
			(flags & MSG_DONTWAIT), &err);

		if (skb == NULL)
			return err;

		/* reserve space for Hardware header */
		skb_reserve(skb, hh_len);

		/* create space for UDP/IP header */
		skb_put(skb, fragheaderlen + transhdrlen);

		/* initialize network header pointer */
		skb_reset_network_header(skb);

		/* initialize protocol header pointer */
		skb->transport_header = skb->network_header + fragheaderlen;

		skb->ip_summed = CHECKSUM_PARTIAL;
		skb->csum = 0;

		/* specify the length of each IP datagram fragment */
		skb_shinfo(skb)->gso_size = maxfraglen - fragheaderlen; ///L4 header  + user data
		skb_shinfo(skb)->gso_type = SKB_GSO_UDP;
		__skb_queue_tail(queue, skb);
	}

	return skb_append_datato_frags(sk, skb, getfrag, from,
				       (length - transhdrlen));
}

Segmentation offload

现在很多网卡本身支持数据分片,这样,上层L4/L3就可以不用进行分片(最大64KB),而由NIC来完成,从而提高网络性能。见NIC Offloads

TCP Segmentation Offload (TSO)

如果网路适配器支持TSO功能,需要声明网卡的能力支持 TSO,这是通过以NETIF_F_TSO标志设置 net_device structure 的 features字段来表明,例如,在ixgbe网卡的驱动程序中,设置NETIF_F_TSO的代码如下:

static int __devinit ixgbe_probe(struct pci_dev *pdev,
				 const struct pci_device_id __always_unused *ent)
{
...
	netdev->features |= ixgbe_tso_features();
...

static inline unsigned long ixgbe_tso_features(void)
{
	unsigned long features = 0;

#ifdef NETIF_F_TSO
	features |= NETIF_F_TSO;
#endif /* NETIF_F_TSO */
#ifdef NETIF_F_TSO6
	features |= NETIF_F_TSO6;
#endif /* NETIF_F_TSO6 */

	return features;
}

当一个TCP的socket被创建,其中一个职责是设置该连接的能力,在网络层的socket的表示是 struck sock,其中有一个字段 sk_route_caps 标示该连接的能力,在TCP的三次握手完成之后,将基于NIC的features和连接来设置该字段。

/* This will initiate an outgoing connection. */
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
...
	/* OK, now commit destination to socket.  */
	sk->sk_gso_type = SKB_GSO_TCPV4;
	sk_setup_caps(sk, &rt->dst);
...

void sk_setup_caps(struct sock *sk, struct dst_entry *dst)
{
	__sk_dst_set(sk, dst);
	sk->sk_route_caps = dst->dev->features;///hardware feature
	if (sk->sk_route_caps & NETIF_F_GSO)
		sk->sk_route_caps |= NETIF_F_GSO_SOFTWARE;
	sk->sk_route_caps &= ~sk->sk_route_nocaps;
	if (sk_can_gso(sk)) {
		if (dst->header_len) {
			sk->sk_route_caps &= ~NETIF_F_GSO_MASK;
		} else {
			sk->sk_route_caps |= NETIF_F_SG | NETIF_F_HW_CSUM;
			sk->sk_gso_max_size = dst->dev->gso_max_size;
			sk->sk_gso_max_segs = dst->dev->gso_max_segs;
		}
	}
}

现在,一切的准备工作都已经做好了,当实际的数据需要传输时,需要使用我们设置好的 gso_max_size,我们知道,TCP 向 IP 层发送数据会考虑 MSS,使得发送的 IP 包在 MTU 内,不用分片。而 TSO 设置的 gso_max_size 就影响该过程,这主要是在计算 mss_now 字段时使用。如果内核不支持 TSO 功能,mss_now 的最大值为”MTU – HLENS”,而在支持 TSO 的情况下,mss_now 的最大值为”gso_max_size - HLENS”,这样,从网络层到驱动的路径就被打通了。

static unsigned int tcp_xmit_size_goal(struct sock *sk, u32 mss_now,
				       int large_allowed)
{
...
	xmit_size_goal = mss_now;
	if (large_allowed && sk_can_gso(sk)) {
...
		xmit_size_goal = min_t(u32, gso_size,
				       sk->sk_gso_max_size - 1 - hlen);
...
	}
	return max(xmit_size_goal, mss_now);
}

Generic Segmentation Offload (GSO)

TSO是使得网络协议栈能够将超过PMTU的数据推送至网卡,然后网卡执行分片工作,这样减轻了CPU的负荷,但 TSO 需要硬件来实现分片功能;而性能上的提高,主要是因为延缓分片而减轻了 CPU 的负载,因此,可以考虑将 TSO 技术一般化,因为其本质实际是延缓分片,这种技术,在 Linux 中被叫做 GSO(Generic Segmentation Offload),它比TSO 更通用,原因在于它不需要硬件的支持分片就可使用,对于支持 TSO 功能的硬件,则先经过 GSO 功能,然后使用网卡的硬件分片能力执行分片;而对于不支持 TSO 功能的网卡,将分片的执行,放在了将数据推送的网卡的前一刻,也就是在调用驱动的 xmit 函数前。

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)))///GSO
				goto out_kfree_skb;
			if (skb->next)
				goto gso;
		}

函数netif_needs_gso根据skb->gso_type和dev->features判断对当前SKB实例是否进行软件GSO。

///检查硬件的features能否满足gso_type(可能是多个组合,比如对于VXLAN包可能为SKB_GSO_UDP|SKB_GSO_UDP_TUNNEL)
///如果dev->features & gso_type = gso_type,则表明NIC满足SKB的要求.
static inline bool net_gso_ok(netdev_features_t features, int gso_type)
{
	netdev_features_t feature = gso_type << NETIF_F_GSO_SHIFT;

	/* check flags correspondence */
	BUILD_BUG_ON(SKB_GSO_TCPV4   != (NETIF_F_TSO >> NETIF_F_GSO_SHIFT));
	BUILD_BUG_ON(SKB_GSO_UDP     != (NETIF_F_UFO >> NETIF_F_GSO_SHIFT));
	BUILD_BUG_ON(SKB_GSO_DODGY   != (NETIF_F_GSO_ROBUST >> NETIF_F_GSO_SHIFT));
	BUILD_BUG_ON(SKB_GSO_TCP_ECN != (NETIF_F_TSO_ECN >> NETIF_F_GSO_SHIFT));
	BUILD_BUG_ON(SKB_GSO_TCPV6   != (NETIF_F_TSO6 >> NETIF_F_GSO_SHIFT));
	BUILD_BUG_ON(SKB_GSO_FCOE    != (NETIF_F_FSO >> NETIF_F_GSO_SHIFT));

	return (features & feature) == feature;
}

static inline bool skb_gso_ok(struct sk_buff *skb, netdev_features_t features)
{
	return net_gso_ok(features, skb_shinfo(skb)->gso_type) &&
	       (!skb_has_frag_list(skb) || (features & NETIF_F_FRAGLIST));
}

static inline bool netif_needs_gso(struct sk_buff *skb,
				   netdev_features_t features)
{
	return skb_is_gso(skb) && (!skb_gso_ok(skb, features) ||
		unlikely((skb->ip_summed != CHECKSUM_PARTIAL) &&
			 (skb->ip_summed != CHECKSUM_UNNECESSARY)));
}

UDP fragmentation offload (UFO)

# ip -d link show flannel.1
6: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN 
    link/ether 6e:83:99:5b:8c:51 brd ff:ff:ff:ff:ff:ff
    vxlan 
# ethtool -k flannel.1|grep udp                    
udp-fragmentation-offload: on
tx-udp_tnl-segmentation: off [fixed]
static init __ip_append_data(){
...
	if (((length > mtu) || (skb && skb_has_frags(skb))) && ///len(L4 header + user data) > mtu and UFO
	    (sk->sk_protocol == IPPROTO_UDP) &&
	    (rt->dst.dev->features & NETIF_F_UFO) && !rt->dst.header_len &&
	    (sk->sk_type == SOCK_DGRAM)) {///UDP offload
		err = ip_ufo_append_data(sk, queue, getfrag, from, length,
					 hh_len, fragheaderlen, transhdrlen,
					 maxfraglen, flags);

static inline int ip_ufo_append_data(){
...
		/* specify the length of each IP datagram fragment */
		skb_shinfo(skb)->gso_size = maxfraglen - fragheaderlen; ///L4 header  + user data
		skb_shinfo(skb)->gso_type = SKB_GSO_UDP;
...

从上面的代码可以知道,如果NIC支持UFO,且数据长度length超过MTU时,就会走UFO的逻辑。

** 值得注意的是上面的length包括L4 header和user data,却没有包含L3 header,因为一般来说,考虑MTU时,应该考虑L3 header **

由于flannel.1的MTU为1450,所以,对于UDP,如果user data为1443字节,就会满足上面的条件:1443 + 8 (UDP header) = 1451。

可以看到,从flannel.1来看,在发送的时候没有发生分片,但接收的时候发生了分片(???)。发送时,如果下层物理NIC不支持UFO,在将SKB推送给驱动之前,会走GSO的逻辑,完成分片操作:

对于igb/ixgbe驱动,默认都不支持UFO:

# ethtool -i eth1
driver: ixgbe
version: 4.1.1
firmware-version: 0x8000039c, 14.0.12
bus-info: 0000:01:00.1
supports-statistics: yes
supports-test: yes
supports-eeprom-access: yes
supports-register-dump: yes
supports-priv-flags: no


# ethtool -k eth1|grep udp 
udp-fragmentation-offload: off [fixed]
tx-udp_tnl-segmentation: on

UDP encapsulation offload

对于UDP encapsulation packet,不会对外层UDP进行分片,只会对根据PMTU对内层packet进行分片。

GSO for UDP encapsulation packet

详细过程参考ixgbe.

VXLAN example

对于VXLAN,有50字节的额外开销:Outer L2 header(14 bytes) + Outer L3 header(20 bytes) + Outer UDP header(8 bytes) + VXLAN header(8 bytes)。如果underlay network的MTU为1500,需要将VXLAN设备的MTU设置为1450。

如果基于VXLAN发送一个1422字节(1450 - 20 - 8)的UDP包,内层UDP包不会发生分片:

如果发送一个1423字节的UDP包,我们就会观察内层UDP包发生分片:

** 值得注意的是,第1个frame只有1508字节,也就是说只包含1416字节的用户数据,第2个frame包含剩下的7个字节。(原因呢???) **

VXLAN hardware offload

Intel X540默认支持VXLAN offload

tx-udp_tnl-segmentation: on
static int __devinit ixgbe_probe(struct pci_dev *pdev,
				 const struct pci_device_id __always_unused *ent)
{
...
#ifdef HAVE_ENCAP_TSO_OFFLOAD
	netdev->features |= NETIF_F_GSO_UDP_TUNNEL; ///UDP tunnel offload
#endif

static const char netdev_features_strings[NETDEV_FEATURE_COUNT][ETH_GSTRING_LEN] = {
...
[NETIF_F_GSO_UDP_TUNNEL_BIT] =   "tx-udp_tnl-segmentation",

VXLAN设备在发送数据时,会设置SKB_GSO_UDP_TUNNEL:

static int handle_offloads(struct sk_buff *skb)
{
	if (skb_is_gso(skb)) {
		int err = skb_unclone(skb, GFP_ATOMIC);
		if (unlikely(err))
			return err;

		skb_shinfo(skb)->gso_type |= SKB_GSO_UDP_TUNNEL;
	} else if (skb->ip_summed != CHECKSUM_PARTIAL)
		skb->ip_summed = CHECKSUM_NONE;

	return 0;
}

值得注意的是,该特性只有当内层的packet为TCP协议时,才有意义。前面已经讨论ixgbe不支持UFO,所以对UDP packet,最终会在推送给物理网卡时(dev_hard_start_xmit)进行软件GSO。

Discussion

  • (1) Is it necessary to change MTU of flannel.1 to 1450 ?

对于TCP,veth/flannel.1都开启了TSO和GSO。对于inner packet > flannel.1 MTU,如果物理网卡支持VXLAN offload,最终由物理网卡完成分片;如果物理网卡不支持vxlan offload,走内核的GSO完成分片。但是,对于inner packet < flannel.1 MTU,inner packet + outer header > 物理网卡MTU,就会导致outer packet在ip_fragment中进行第二次分片,影响性能。

对于UDP,veth默认没有UFO,flannel.1开启了UFO,如果不减小flannel.1的MTU,可能会导致inner packet + outer header > 物理网卡的MTU,这可能会导致outer packet在ip_fragment中进行第二次分片,影响性能。

所以,不管怎样,减少flannel.1的MTU都是必要的。

  • (2) Should enable UDP RSS for vxlan ?

Related posts


上一篇     下一篇