技术开发 频道

看Linux网管员如何进行网络性能优化

  接收路径上的优化

  LRO (Large Receive Offload)

  Linux 在 2.6.24 中加入了支持 IPv4 TCP 协议的 LRO (Large Receive Offload) ,它通过将多个 TCP 数据聚合在一个 skb 结构,在稍后的某个时刻作为一个大数据包交付给上层的网络协议栈,以减少上层协议栈处理 skb 的开销,提高系统接收 TCP 数据包的能力。

  当然,这一切都需要网卡驱动程序支持。理解 LRO 的工作原理,需要理解 sk_buff 结构体对于负载的存储方式,在内核中,sk_buff 可以有三种方式保存真实的负载:

  1、数据被保存在 skb->data 指向的由 kmalloc 申请的内存缓冲区中,这个数据区通常被称为线性数据区,数据区长度由函数 skb_headlen 给出

  2、数据被保存在紧随 skb 线性数据区尾部的共享结构体 skb_shared_info 中的成员 frags 所表示的内存页面中,skb_frag_t 的数目由 nr_frags 给出,skb_frags_t 中有数据在内存页面中的偏移量和数据区的大小

  3、数据被保存于 skb_shared_info 中的成员 frag_list 所表示的 skb 分片队列中

  合并了多个 skb 的超级 skb,能够一次性通过网络协议栈,而不是多次,这对 CPU 负荷的减轻是显然的。

  LRO 的核心结构体如下:

  LRO 的核心结构体

/*
* Large Receive Offload (LRO) Manager
*
* Fields must be set by driver
*/

struct net_lro_mgr {
     struct net_device
*dev;
     struct net_lro_stats stats;

    
/* LRO features */
     unsigned
long features;
#define LRO_F_NAPI            
1  /* Pass packets to stack via NAPI */
#define LRO_F_EXTRACT_VLAN_ID
2  /* Set flag if VLAN IDs are extracted
                    from received packets
and eth protocol
                    
is still ETH_P_8021Q */

    
/*
    
* Set for generated SKBs that are not added to
    
* the frag list in fragmented mode
    
*/
     u32 ip_summed;
     u32 ip_summed_aggr;
/* Set in aggregated SKBs: CHECKSUM_UNNECESSARY
                
* or CHECKSUM_NONE */

    
int max_desc; /* Max number of LRO descriptors  */
    
int max_aggr; /* Max number of LRO packets to be aggregated */

    
int frag_align_pad; /* Padding required to properly align layer 3
                
* headers in generated skb when using frags */

     struct net_lro_desc
*lro_arr; /* Array of LRO descriptors */

    
/*
    
* Optimized driver functions
    
*
    
* get_skb_header: returns tcp and ip header for packet in SKB
    
*/
    
int (*get_skb_header)(struct sk_buff *skb, void **ip_hdr,
                  void
**tcpudp_hdr, u64 *hdr_flags, void *priv);

    
/* hdr_flags: */
#define LRO_IPV4
1 /* ip_hdr is IPv4 header */
#define LRO_TCP  
2 /* tcpudp_hdr is TCP header */

    
/*
    
* get_frag_header: returns mac, tcp and ip header for packet in SKB
    
*
    
* @hdr_flags: Indicate what kind of LRO has to be done
    
*             (IPv4/IPv6/TCP/UDP)
    
*/
    
int (*get_frag_header)(struct skb_frag_struct *frag, void **mac_hdr,
                   void
**ip_hdr, void **tcpudp_hdr, u64 *hdr_flags,
                   void
*priv);
};

  在该结构体中:

  dev:指向支持 LRO 功能的网络设备

  stats:包含一些统计信息,用于查看 LRO 功能的运行情况

  features:控制 LRO 如何将包送给网络协议栈,其中的 LRO_F_NAPI 表明驱动是 NAPI 兼容的,应该使用 netif_receive_skb() 函数,而 LRO_F_EXTRACT_VLAN_ID 表明驱动支持 VLAN

  ip_summed:表明是否需要网络协议栈支持 checksum 校验

  ip_summed_aggr:表明聚集起来的大数据包是否需要网络协议栈去支持 checksum 校验

  max_desc:表明最大数目的 LRO 描述符,注意,每个 LRO 的描述符描述了一路 TCP 流,所以该值表明了做多同时能处理的 TCP 流的数量

  max_aggr:是最大数目的包将被聚集成一个超级数据包

  lro_arr:是描述符数组,需要驱动自己提供足够的内存或者在内存不足时处理异常

  get_skb_header()/get_frag_header():用于快速定位 IP 或者 TCP 的头,一般驱动只提供其中的一个实现

  一般在驱动中收包,使用的函数是 netif_rx 或者 netif_receive_skb,但在支持 LRO 的驱动中,需要使用下面的函数,这两个函数将进来的数据包根据 LRO 描述符进行分类,如果可以进行聚集,则聚集为一个超级数据包,否者直接传递给内核,走正常途径。需要 lro_receive_frags 函数的原因是某些驱动直接将数据包放入了内存页,之后去构造 sk_buff,对于这样的驱动,应该使用下面的接口:

  LRO 收包函数

void lro_receive_skb(struct net_lro_mgr *lro_mgr,
                  struct sk_buff
*skb,
                  void
*priv);

void lro_receive_frags(struct net_lro_mgr
*lro_mgr,
                       struct skb_frag_struct
*frags,
              
int len, int true_size,
               void
*priv, __wsum sum);

  因为 LRO 需要聚集到 max_aggr 数目的数据包,但有些情况下可能导致延迟比较大,这种情况下,可以在聚集了部分包之后,直接传递给网络协议栈处理,这时可以使用下面的函数,也可以在收到某个特殊的包之后,不经过 LRO,直接传递个网络协议栈:

  LRO flush 函数

void lro_receive_skb(struct net_lro_mgr *lro_mgr,
                  struct sk_buff
*skb,
                  void
*priv);

void lro_receive_frags(struct net_lro_mgr
*lro_mgr,
                       struct skb_frag_struct
*frags,
              
int len, int true_size,
               void
*priv, __wsum sum);

  GRO (Generic Receive Offload)

  前面的 LRO 的核心在于:在接收路径上,将多个数据包聚合成一个大的数据包,然后传递给网络协议栈处理,但 LRO 的实现中存在一些瑕疵:

  1、数据包合并可能会破坏一些状态;

  2、数据包合并条件过于宽泛,导致某些情况下本来需要区分的数据包也被合并了,这对于路由器是不可接收的;

  3、在虚拟化条件下,需要使用桥接功能,但 LRO 使得桥接功能无法使用;

  4、实现中,只支持 IPv4 的 TCP 协议。

  而解决这些问题的办法就是新提出的 GRO(Generic Receive Offload),首先,GRO 的合并条件更加的严格和灵活,并且在设计时,就考虑支持所有的传输协议,因此,后续的驱动,都应该使用 GRO 的接口,而不是 LRO,内核可能在所有先有驱动迁移到 GRO 接口之后将 LRO 从内核中移除。而 Linux 网络子系统的维护者 David S. Miller 就明确指出,现在的网卡驱动,有 2 个功能需要使用,一是使用 NAPI 接口以使得中断缓和 (interrupt mitigation) ,以及简单的互斥,二是使用 GRO 的 NAPI 接口去传递数据包给网路协议栈。

  在 NAPI 实例中,有一个 GRO 的包的列表 gro_list,用堆积收到的包,GRO 层用它来将聚集的包分发到网络协议层,而每个支持 GRO 功能的网络协议层,则需要实现 gro_receive 和 gro_complete 方法。

  协议层支持 GRO/GSO 的接口

struct packet_type {
     __be16              type;      
/* This is really htons(ether_type). */
     struct net_device      
*dev;      /* NULL is wildcarded here          */
    
int              (*func) (struct sk_buff *,
                     struct net_device
*,
                     struct packet_type
*,
                     struct net_device
*);
     struct sk_buff          
*(*gso_segment)(struct sk_buff *skb,
                        
int features);
    
int              (*gso_send_check)(struct sk_buff *skb);
     struct sk_buff          
**(*gro_receive)(struct sk_buff **head,
                           struct sk_buff
*skb);
    
int              (*gro_complete)(struct sk_buff *skb);
     void              
*af_packet_priv;
     struct list_head      list;
};

  其中,gro_receive 用于尝试匹配进来的数据包到已经排队的 gro_list 列表,而 IP 和 TCP 的头部则在匹配之后被丢弃;而一旦我们需要向上层协议提交数据包,则调用 gro_complete 方法,将 gro_list 的包合并成一个大包,同时 checksum 也被更新。在实现中,并没要求 GRO 长时间的去实现聚合,而是在每次 NAPI 轮询操作中,强制传递 GRO 包列表跑到上层协议。GRO 和 LRO 的最大区别在于,GRO 保留了每个接收到的数据包的熵信息,这对于像路由器这样的应用至关重要,并且实现了对各种协议的支持。以 IPv4 的 TCP 为例,匹配的条件有:

  1、源 / 目的地址匹配;

  2、TOS/ 协议字段匹配;

  3、源 / 目的端口匹配。

  而很多其它事件将导致 GRO 列表向上层协议传递聚合的数据包,例如 TCP 的 ACK 不匹配或者 TCP 的序列号没有按序等等。

  GRO 提供的接口和 LRO 提供的接口非常的类似,但更加的简洁,对于驱动,明确可见的只有 GRO 的收包函数了 , 因为大部分的工作实际是在协议层做掉了:

  GRO 收包接口

gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
gro_result_t napi_gro_frags(struct napi_struct
*napi)

  小结

  从上面的分析,可以看到,Linux 网络性能优化方法,就像一部进化史,但每步的演化,都让解决问题的办法更加的通用,更加的灵活;从 NAPI 到 Newer newer NAPI,从 TSO 到 GSO,从 LRO 到 GRO,都是一个从特例到一个更通用的解决办法的演化,正是这种渐进但连续的演化,让 Linux 保有了如此的活力。

0
相关文章