问题背景
业务服务器使用IntelXL710网卡,上线使用过程中网卡突然断链,Link状态为down,而且不可恢复,必须
复位服务器才可以。但是过一段时间后,会再次出现同样的问题,而且在几个局点都出现了类似的问题。
开始出现该问题时,根据以往经验,无非是光模块、光纤兼容问题,网卡硬件批次问题。但是随着出问题
的设备增多,次数增多,开始觉得该问题没这么简单。
先看看生产环境收集到的信息:
网卡状态信息
# ip a | grep eth4
5: eth4: <NO-CARRIER,BROADCAST,MULTICAST,SLAVE,UP> mtu 1500 qdisc mq master bond5 state DOWN qlen 1000
GZ-SN-OTT18:~ #
GZ-SN-OTT18:~ # ethtool eth4
Settings for eth4:
Supported ports: [ ]
Supported link modes: 1000baseT/Full
10000baseT/Full
Supports auto-negotiation: Yes
Advertised link modes: 1000baseT/Full
10000baseT/Full
Advertised pause frame use: No
Advertised auto-negotiation: Yes
Speed: Unknown!
Duplex: Unknown! (255)
Port: Other
PHYAD: 0
Transceiver: external
Auto-negotiation: off
Supports Wake-on: g
Wake-on: g
Current message level: 0x0000000f (15)
drv probe link timer
Link detected: no
内核日志信息
Jul 20 11:28:38 JAedge5 kernel: [8395582.875067] i40e 0000:0a:00.2: TX driver issue detected, PF reset issued ##网卡错误
Jul 20 11:28:38 JAedge5 kernel: klogd 1.4.1, ---------- state change ----------
Jul 20 11:28:38 JAedge5 kernel: [8395583.453724] kworker/u:4: page allocation failure: order:5, mode:0x80d0 ##分配order=5的连续page失败
Jul 20 11:28:38 JAedge5 kernel: [8395583.453734] Pid: 21027, comm: kworker/u:4 Tainted: G ENX 3.0.101-0.47.52-default #1
Jul 20 11:28:38 JAedge5 kernel: [8395583.453738] Call Trace:
Jul 20 11:28:38 JAedge5 kernel: [8395583.453762] dump_trace+0x75/0x300
Jul 20 11:28:38 JAedge5 kernel: [8395583.453777] dump_stack+0x69/0x6f
Jul 20 11:28:38 JAedge5 kernel: [8395583.453790] warn_alloc_failed+0xc6/0x170
Jul 20 11:28:38 JAedge5 kernel: [8395583.453802] __alloc_pages_slowpath+0x561/0x7f0
Jul 20 11:28:38 JAedge5 kernel: [8395583.453812] __alloc_pages_nodemask+0x1e9/0x200
Jul 20 11:28:38 JAedge5 kernel: [8395583.453823] dma_generic_alloc_coherent+0xa6/0x160
Jul 20 11:28:38 JAedge5 kernel: [8395583.453837] x86_swiotlb_alloc_coherent+0x28/0x80
Jul 20 11:28:38 JAedge5 kernel: [8395583.453875] i40e_setup_tx_descriptors+0xf1/0x140 [i40e]
Jul 20 11:28:38 JAedge5 kernel: [8395583.453971] i40e_vsi_setup_tx_resources+0x2d/0x60 [i40e]
Jul 20 11:28:38 JAedge5 kernel: [8395583.454003] i40e_vsi_open+0x27/0x1c0 [i40e]
Jul 20 11:28:38 JAedge5 kernel: [8395583.454046] i40e_open+0x5b/0x90 [i40e]
Jul 20 11:28:38 JAedge5 kernel: [8395583.454078] i40e_pf_unquiesce_all_vsi+0x30/0x50 [i40e]
Jul 20 11:28:38 JAedge5 kernel: [8395583.454114] i40e_reset_and_rebuild+0x2f2/0x600 [i40e]
Jul 20 11:28:38 JAedge5 kernel: [8395583.454156] i40e_reset_subtask+0xf8/0x100 [i40e]
Jul 20 11:28:38 JAedge5 kernel: [8395583.454189] i40e_service_task+0x108/0x300 [i40e]
Jul 20 11:28:38 JAedge5 kernel: [8395583.454217] process_one_work+0x16c/0x350
Jul 20 11:28:38 JAedge5 kernel: [8395583.454229] worker_thread+0x17a/0x410
Jul 20 11:28:38 JAedge5 kernel: [8395583.454261] kthread+0x96/0xa0
Jul 20 11:28:38 JAedge5 kernel: [8395583.454272] kernel_thread_helper+0x4/0x10
Jul 20 11:28:39 JAedge5 kernel: [8395583.454280] Mem-Info:
Jul 20 11:28:39 JAedge5 kernel: [8395583.454283] Node 0 DMA per-cpu:
Jul 20 11:28:39 JAedge5 kernel: [8395583.454288] CPU 0: hi: 0, btch: 1 usd: 0
Jul 20 11:28:39 JAedge5 kernel: [8395583.454292] CPU 1: hi: 0, btch: 1 usd: 0
Jul 20 11:28:39 JAedge5 kernel: [8395583.454296] CPU 2: hi: 0, btch: 1 usd: 0
内核异常日志分析
从网卡和内核日志分析,这台设备eth4网卡是down的,是由于网卡出现PF reset打印,引起网卡
重新创建Rx/Tx descriptor,但是由于内存不足(应该是需要的连续内存页不能满足),分配descriptor
失败,导致网卡启动失败,最终引起网卡Link down状态。
XL710 PF reset:
i40e 0000:0a:00.2: TX driver issue detected, PF reset issued
网口PF_RESET在驱动里是探测到MDD事件,即是 Malicious Driver Detection Event。
通过 I40E_GL_MDET_TX(0x000E6480)获取。
这个问题问过Intel技术支持,由于目前底层都被封装起来了,所以也不知道是什么类型的错误。
XL710网卡内存分配失败
page allocation failure: order:5, mode:0x80d0
PF重启时,需要重新分配网卡descriptor资源,需要连续的内存页。当系统内存碎片严重时,会出现分配page失败,导致网口再也up不起来。
网卡重建队列调用堆栈:
i40e_reset_and_rebuild
--> i40e_pf_unquiesce_all_vsi
--> i40e_open
--> i40e_vsi_open
--> i40e_vsi_setup_tx_resources
--> i40e_setup_tx_descriptors
--> x86_swiotlb_alloc_coherent
--> dma_generic_alloc_coherent
--> __alloc_pages_nodemask
--> __alloc_pages_slowpath
void *dma_generic_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t gfp)
{
void *ret, *ret_nocache;
int order = get_order(size);
gfp |= __GFP_ZERO;
ret = (void *)__get_free_pages(gfp, order);
if (!ret)
return NULL;
....
}
在网上也搜索了相关的资料,有说是网卡自身问题,要把TCP segment offload关掉,生产环境也做了相关操作,
确实也没有再出现PF reset的问题。这也只是临时规避措施。分配连续page失败是附带的问题,根本还是要找到网卡
产生PF reset的原因。
根本原因
这里要感谢Intel技术工程师,出现这个问题后,反复和他们进行了沟通和交流,收集了很多信息,也联系了美国
团队技术专家。最终,我们在Intel一篇技术文档中找到了问题的可疑原因:
Intel specification update文档
Intel ® Ethernet Controller X710/XXV710/XL710
Specification Update
Ethernet Networking Division (ND)
Revision 3.3
这是一个安全漏洞,是在2017年1月10号加入这个文档的。
怎么理解和复现问题?
从文档描述中,可疑肯定一点,这个问题与TSO相关,在生产环境关闭TSO后,也确实没有
再次出现问题。但是,怎么理解文档描述的问题?又怎么复现这个问题,得到和生产环境一样的
错误信息呢?
先试着翻译一下:
当一个TSO报文的第一个Tx descriptor包含报文头和数据载荷时,在MAX_BUFFS的异常检测中
被计算两次。因此,如果第一个segment分布在8个descriptor中,就会报告一个MDD时间。然而,如果
一个segement中超过8个descriptor时,它应该只产生一个MDD事件。
这会导致虚假的MDD(Malicious Driver Detection)事件。
软件驱动必须限制一个TSO报文的第一个段(segment)包含7个descriptor,而不是8个descriptor。
这个现实已经在Intel 21.3驱动版本中实现。
bug说明:
从文档描述看,与发送TSO数据报文有关,而LInux协议栈发送TSO数据报文是通过skb数据结构传递到
网卡驱动的,我们可以从skb入手,看看什么样的skb数据会导致整个问题。
文档描述中的7个descriptor又是什么意思?什么样的skb数据才能算是7个descriptor?这要从I40E驱动
里面寻找答案。
I40E驱动如何计算descriptor个数
PS:以I40E 1.4.25驱动源码为基础进行说明。
I40E驱动发包函数为i40e_xmit_frame_ring,进入后调用i40e_xmit_descriptor_count计算descriptor是否充足,
否则返回NETDEV_TX_BUSY。
计算descriptor函数如下:
1 #ifndef DIV_ROUND_UP 2 #define DIV_ROUND_UP(n,d) (((n) + (d) - 1) / (d)) 3 #endif 4 5 #define I40E_MAX_DATA_PER_TXD 8192 6 7 /* Tx Descriptors needed, worst case */ 8 #define TXD_USE_COUNT(S) DIV_ROUND_UP((S), I40E_MAX_DATA_PER_TXD) 9 10 11 #ifdef I40E_FCOE 12 inline int i40e_xmit_descriptor_count(struct sk_buff *skb, 13 struct i40e_ring *tx_ring) 14 #else 15 static inline int i40e_xmit_descriptor_count(struct sk_buff *skb, 16 struct i40e_ring *tx_ring) 17 #endif 18 { 19 unsigned int f; 20 int count = 0; 21 22 /* need: 1 descriptor per page * PAGE_SIZE/I40E_MAX_DATA_PER_TXD, 23 * + 1 desc for skb_head_len/I40E_MAX_DATA_PER_TXD, 24 * + 4 desc gap to avoid the cache line where head is, 25 * + 1 desc for context descriptor, 26 * otherwise try next time 27 */ 28 for (f = 0; f < skb_shinfo(skb)->nr_frags; f++) 29 count += TXD_USE_COUNT(skb_shinfo(skb)->frags[f].size); 30 31 count += TXD_USE_COUNT(skb_headlen(skb)); 32 if (i40e_maybe_stop_tx(tx_ring, count + 4 + 1)) { 33 tx_ring->tx_stats.tx_busy++; 34 return 0; 35 } 36 return count; 37 }
一个skb需要的TXD(Transmit Descriptor)计算分为两部分,一个是非线性区每个page需要的TXD计算,
另一个是线性区数据需要的TXD计算。同时,要预留5个TXD,其中一个是context TXD,另外4个TXD是为避免
head的cache line而保留的TXD间隔(gap)。
每个TXD最大的数据长度是8192字节(I40E_MAX_DATA_PER_TXD)。
注意:I40E_MAX_DATA_PER_TXD这个值在后来的高版本驱动中是12K。
综上,bug说明中所说的一个segment分布在8个TXD中,即skb线性区数据占用一个TXD,skb frag占用7个TXD。
这样,可以很容易构造使得I40E网卡产生MDD异常的SKB数据。
复现代码 & skb结构
构造特殊skb数据
#define WIT_TCP_SEG_NUM 7
#define WIT_TCP_DATA_LEN 4000
#define WIT_TCP_SEG_LEN 150
#define WIT_SKB_LINE_LEN 260
#define WIT_MSS 1460
#define WIT_SKB_CACHE_LENGTH 512
/* 构造能够产生MDD事件的tcp skb */
static int mdd_tcp_skb(struct sk_buff **skb)
{
/* 核心代码 */
int skb_page_offset = 0;
int pgrefcnt = 0;
int page_index = 0;
int rc = 0;
struct page *linear_page=NULL;
unsigned int headroom;
unsigned int iphdr_length;
unsigned int ethhdr_length;
unsigned int tcphdr_length;
unsigned int payload_length = 0;
unsigned char * pdata=NULL;
/* 分配页面,存放skb非线性区数据 */
linear_page = alloc_pages(GFP_ATOMIC|__GFP_COMP, get_order(WIT_TCP_DATA_LEN));
if (!linear_page)
{
rc = -ENOMEM;
goto alloc_page_err;
}
/* 转化为线性虚拟地址,便于访问页面 */
linear_vaddr = page_address(linear_page);
struct sk_buff *nskb=NULL;
/*分配skb,并设置。*/
/*
skb->head = data;
skb->data = data;
skb_reset_tail_pointer(skb);
skb->end = skb->tail + size;
*/
nskb = __alloc_skb(WIT_SKB_CACHE_LENGTH, GFP_ATOMIC, 0, nid/* numa id */);
/*需要重新分配skb,没有可复用的。*/
if (unlikely(nskb==NULL))
{
BUG_ON(nskb==NULL);
rc = -ENOMEM;
goto alloc_skb_err;
}
/* nskb线性区保留空间大小,即将头部指针往后移动,然后从高层协议开始填充。*/
/* skb_reserve operation result:
skb->data += len;
skb->tail += len;
*/
headroom = 2;
iphdr_length = sizeof(struct iphdr);
ethhdr_length = sizeof(struct ethhdr);
tcphdr_length = sizeof(struct tcphdr);
skb_reserve(nskb, headroom + ethhdr_length + iphdr_length + tcphdr_length);
/* 存放线性区数据 */
/*
unsigned char *tmp = skb_tail_pointer(skb);
skb->tail += len;
skb->len += len;
返回 tmp,即移动之前的skb->tail
*/
pdata = __skb_put(nskb, WIT_SKB_LINE_LEN);
memset(pdata, 0xAA, WIT_SKB_LINE_LEN);
/* 指定一个page frag包含的数据长度 */
payload_length = WIT_TCP_SEG_LEN;
/*数据页面直接改用增加页面引用计数。*/
for(page_index = 0; page_index < 8; page_index++)
{
skb_fill_page_desc(nskb, page_index, linear_page, skb_page_offset, payload_length);
/* liangjs modi 2015-7-16, if rtphdr_length, second reference page, or it's first ref page, unnecssary */
if(pgrefcnt > 0)
{
get_page(linear_page);
}
pgrefcnt++;
skb_page_offset += payload_length;
nskb->len += payload_length;
nskb->data_len += payload_length;
nskb->truesize += payload_length;
}
//填写包头 ether,ip and tcp header
/* skb是从原始sk_buff,里面存放有 二层到四层 报文头 信息 */
fill_mdd_skb_tcphdr(nskb);
/* 计算 nskb中 tcp seg个数,tcp最大段长度(MSS), 以及GSO类型 */
struct skb_shared_info *shinfo = skb_shinfo(nskb);
shinfo->gso_segs = DIV_ROUND_UP(nskb->len, WIT_MSS);
shinfo->gso_size = WIT_MSS;
shinfo->gso_type = SKB_GSO_TCPV4; //sk->sk_gso_type;
return rc;
alloc_skb_err:
if (linear_page)
{
/* liangjs note 2014-08-22: need not put_page */
printk("%s:%d: linear_page = %p
", __FUNCTION__, __LINE__, linear_page);
__free_pages(linear_page, get_order(WIT_TCP_DATA_LEN));
}
alloc_page_err:
*skb = NULL;
return rc;
}
static inline int fill_mdd_skb_tcphdr(struct sk_buff *nskb)
{
struct ethhdr * pethhdr=NULL;
struct iphdr * piphdr=NULL;
struct tcphdr *ptcphdr=NULL;
#if 0
struct tcphdr {
__be16 source;
__be16 dest;
__be32 seq;
__be32 ack_seq;
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u16 res1:4,
doff:4,
fin:1,
syn:1,
rst:1,
psh:1,
ack:1,
urg:1,
ece:1,
cwr:1;
#elif defined(__BIG_ENDIAN_BITFIELD)
__u16 doff:4,
res1:4,
cwr:1,
ece:1,
urg:1,
ack:1,
psh:1,
rst:1,
syn:1,
fin:1;
#else
#error "Adjust your <asm/byteorder.h> defines"
#endif
__be16 window;
__sum16 check;
__be16 urg_ptr;
};
#endif
/*调整UDP指针*/
skb->protocol = __constant_htons(ETH_P_IP)
ptcphdr = (struct tcphdr *)__skb_push(nskb, sizeof(struct tcphdr));
skb_set_transport_header(nskb, 0);
memset(ptcphdr, 0, sizeof(struct tcphdr));
ptcphdr->source = htons(61369);
ptcphdr->dest = htons(9527);
ptcphdr->seq = 0x3b5f862d;
ptcphdr->ack_seq = 0xe8f76aee;
ptcphdr->doff = 5;
ptcphdr->fin = 0;
ptcphdr->syn = 0;
ptcphdr->rst = 0;
ptcphdr->psh = 0;
ptcphdr->ack=1;
#if 0
ptcphdr->urg:1,
ptcphdr->ece:1,
ptcphdr->cwr:1;
#endif
ptcphdr->window = 0x0073;
ptcphdr->check = 0;
ptcphdr->urg_ptr = 0;
/*ip头部*/
piphdr = (struct iphdr * )__skb_push(nskb, sizeof(struct iphdr));
//skb_copy_from_linear_data_offset(skb, skb_network_offset(skb),
// (unsigned char *)piphdr, sizeof(struct iphdr));
piphdr->version = 4;
piphdr->protocol = 0x06; /* TCP protocol */
piphdr->saddr = 0x01010101;
piphdr->daddr = 0x02020202;
skb_set_network_header(nskb, 0);
piphdr->ihl = sizeof(struct iphdr)/4;
piphdr->tot_len = htons(nskb->len);
piphdr->check = 0;
piphdr->frag_off = htons(0x4000);
piphdr->check = ip_fast_csum((unsigned char *)piphdr, piphdr->ihl);
nskb->csum = 0;
/* 这个赋值很重要,因为在I40E驱动的i40e_tso函数中,非CHECKSUM_PARTIAL,直接返回了 */
nskb->ip_summed = CHECKSUM_PARTIAL;//CHECKSUM_UNNECESSARY;
/*eth头部*/
pethhdr = (struct ethhdr * )__skb_push(nskb, sizeof(struct ethhdr));
skb_set_mac_header(nskb, 0);
//skb_copy_from_linear_data_offset(skb, 0,
// (unsigned char *)pethhdr, sizeof(struct ethhdr) );
unsigned char dest[ETH_ALEN] = {0x98,0xf5,0x00,0x01,0x02,0x03};
unsigned char src[ETH_ALEN] = {0x98,0xf5,0x00,0x01,0x02,0x04};
memcpy(pethhdr->h_dest, dest, ETH_ALEN);
memcp(pethhdr->h_source, src, ETH_ALEN);
pethhdr->h_proto = 0x0800;
nskb->mac_len = sizeof(struct ethhdr);
nskb->dev = skb->dev;
nskb->queue_mapping = skb->queue_mapping;
nskb->protocol = skb->protocol;
nskb->vlan_tci = skb->vlan_tci;
nskb->pkt_type = skb->pkt_type;
nskb->mark = 0;
return 1;
}
测试复现
测试用例1:
#define WIT_TCP_SEG_NUM 7
#define WIT_TCP_DATA_LEN 4000
#define WIT_TCP_SEG_LEN 150
#define WIT_SKB_LINE_LEN 400
#define WIT_MSS 1450
内核日志:
Nov 15 20:32:18 linux kernel: [18363.480323] i40e 0000:0a:00.3: TX driver issue detected, PF reset issued
测试用例2 :
#define WIT_TCP_SEG_NUM 7
#define WIT_TCP_DATA_LEN 4000
#define WIT_TCP_SEG_LEN 150
#define WIT_SKB_LINE_LEN 410
#define WIT_MSS 1460
内核日志:
[70466.672761] i40e 0000:0a:00.3: TX driver issue detected, PF reset issued
[70467.512354] bonding: bond0: link status up again after 0 ms for interface eth0.
测试用例3 :
#define WIT_TCP_SEG_NUM 8
#define WIT_TCP_DATA_LEN 4000
#define WIT_TCP_SEG_LEN 150
#define WIT_SKB_LINE_LEN 260
#define WIT_MSS 1460
测试结果:
no PF reset
测试用例4 :
#define WIT_TCP_SEG_NUM 7
#define WIT_TCP_DATA_LEN 4000
#define WIT_TCP_SEG_LEN 150
#define WIT_SKB_LINE_LEN 260
#define WIT_MSS 1460
# dmesg -c
[73345.238145] i40e 0000:0a:00.3: TX driver issue detected, PF reset issued
[73345.847481] bonding: bond0: link status up again after 0 ms for interface eth0.
更换驱动和固件:驱动:i40e-2.0.30 固件 5.05
重复以上测试用例,没有再产生PF Reset错误。
skb结构复现总结
1. skb->data 线性区 有报文头 和 要发送的数据;
2. 总共有7个page frag;
3. 线性区数据 + page data_len <= mss_now(gso_size)
后记
软件也是人写的,是人都会犯错误,所以需要测试的介入,尽可能保证故障不外泄。
针对Intel X710网卡的故障,个人愚见,如果进行分支覆盖测试和边界测试,可能也就
不会出现这么严重的BUG了;或者驱动和固件进行整合测试,可能也会发现两边不一致的
情况。软件的任何改动都需要标记和记录,都是测试的重点。
出现该问题的时候,着实困惑了很久,排查过程中,甚至怀疑bond驱动和Linux系统BUG,
或者交换机BUG,为此还专门咨询了在友商交换机部门工作的同学Σ( ° △ °|||)︴。虽然不是这几个
问题,但也学习了相关的模块和技术,也算是一种收获吧。
向Intel技术支持咨询时,总是建议我们升级驱动和固件,后来也确实是通过升级解决问题;
从网上也搜到了相关的问题,关闭TSO也能规避问题。但是作为一位程序员总想搞清楚为什么
会这样(其实有个领导也要求排查出问题),这样才能安心升级驱动呀,否则向局方说升级驱动
能解决问题,如果又出相同问题,就是打自己的脸呀!
还好,有Intel的相关勘误表,我们终于知道了原因,并且也可以复现出来。总算圆满解决。
其实Intel有很多技术文档,包括勘误表,都在它的官方网站上,关键是怎么去查找。而且,
文档中的描述和现象不是一致,而是另外一种说法,所以说要理解技术后面的意思非常重要。
PS:您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,
且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。