Hi!请登陆

ARP相关内核参数unres_qlen研究

2020-4-23 57 4/23

背景

我们在某客户生产环境中发现,应用服务器启动并与后端数据库建立连接池时,有概率发生部分连接建立失败的情况。经过排查发现,这种情况与ARP相关的内核参数unres_qlen有关。这篇文章将通过测试手段复现连接超时的情况,并详尽解析其底层的原因与机制。

复现环境

操作系统:RHEL 6.6

内核版本:2.6.32-504.el6.x86_64

测试程序(为客户端,并发发起16个TCP连接,超时时间设置为500ms):

ARP相关内核参数unres_qlen研究

运行测试程序的机器IP为10.0.0.102,服务端IP为10.0.0.101

现象描述

在运行测试程序tcp_unres_qlen前,先在10.0.0.102上清掉10.0.0.101的ARP缓存

ARP相关内核参数unres_qlen研究

然后运行tcp_unres_qlen程序,只有3个TCP连接成功建立,其他13个TCP连接都失败了;如果后续再多次运行tcp_unres_qlen,所有TCP连接都能正常建立,没有超时的情况,无法复现第一次运行tcp_unres_qlen的情况;后续再次清掉ARP缓存后运行tcp_unres_qlen又能复现连接超时的情况。

ARP相关内核参数unres_qlen研究

问题分析

通过在10.0.0.101上抓包可以验证上述结果(tcpdump src 10.0.0.102):

ARP相关内核参数unres_qlen研究

可以看到10.0.0.102先是广播了一个ARP请求,询问10.0.0.101的MAC地址,然后紧接着发出了三个SYN包到10.0.0.101,后续这三个TCP连接也成功建立,而测试程序tcp_unres_qlen发起的另外13个TCP连接在10.0.0.101上没有抓到任何数据包,说明都超时失败了。

在运行测试程序tcp_unres_qlen的同时,10.0.0.102上使用dropwatch工具抓到了如下信息:

ARP相关内核参数unres_qlen研究

经过多次测试反复观察,排除dropwatch抓到的其他干扰信息,会发现__neigh_set_probe_once+16e (0xffffffff8146892e)出现的次数每次相加都是13,正好与13个超时失败的TCP连接相对应。这表示__neigh_set_probe_once函数偏移16e的位置有包被丢弃,通过SystemMap来定位:

ARP相关内核参数unres_qlen研究

__neigh_set_probe_once的位置87c0+16e偏移量正好等于892e处于__neigh_event_send和neigh_resolve_output之间,那么也就是说包在__neigh_event_send函数丢掉了,查找kernel源码发现:

ARP相关内核参数unres_qlen研究

neigh/default/unres_qlen – INTEGER__neigh_set_probe_once函数中neigh->nud_state = NUD_INCOMPLETE;所以看__neigh_event_send中if (neigh->nud_state == NUD_INCOMPLETE) {}中的内容,可以看到当skb_queue_len(&neigh->arp_queue) >= neigh->parms->queue_len时会清空sk_buff,相当于丢弃相应的skb,体现在TCP层面就是TCP连接发起方的第一个SYN包被丢掉了(SYN包的重传次数是由内核参数net.ipv4.tcp_syn_retries决定的,而第一次发送SYN包失败后到第二次重传的时间间隔RTO是由RTT决定的,所以不同的网络条件下,这个RTO时间会不同,且每多重传一次RTO时间还会根据算法加大,这也是为什么tcp_unres_qlen程序中故意将超时时间设置为很短的500ms<RTO,人为制造连接超时的原因,这部分的知识可参看本文最后的参考文献[1]),导致连接超时失败。可以看出这个判断结果的得出是由neigh->parms->queue_len的值决定的,这个值对应的内核参数就是net.ipv4.neigh.*.unres_qlen(unresolved_queue_length),参考内核文档中的描述(参考文献[2]):

neigh/default/unres_qlen – INTEGER

The maximum number of packets which may be queued for each unresolved address by other network layers. (deprecated in linux 3.3) : use unres_qlen_bytes instead. Prior to linux 3.3, the default value is 3 which may cause unexpected packet loss. The current default value is calculated according to default value of unres_qlen_bytes and true size of packet.

Default: 31

neigh/default/unres_qlen_bytes – INTEGER

The maximum number of bytes which may be used by packets queued for each unresolved address by other network layers. (added in linux 3.3) Setting negative value is meaningless and will return error.

Default: 65536 Bytes(64KB)

可以看出,在kernel 3.3之前,unres_qlen的默认值为3,这个参数的具体作用是什么呢,让我们来看一下一个TCP连接建立的过程:

1、程序发出SYN建链报文后,报文到IP层需要进行路由查询;

2、路由查询完成后,报文到ARP层查询下一跳IP对应的MAC地址;

3、如果本地没有缓存相关的ARP记录,就需要把这个SYN报文放进一个队列里缓存起来,然后发起ARP请求;

4、ARP层收到ARP回应报文之后,从缓存中取出SYN报文,完成二层数据帧的封装并发送给驱动,后面的事情就不说了。

这其中的关键问题就在于第3步,unres_qlen参数将在这里起作用,缓存上层数据报文的队列长度即unres_qlen只有3,也就是说这个队列最多同时只能缓存3个SYN报文,如果并发发起超过3个TCP连接,没能塞进队列里的SYN报文就都被丢弃了,TCP层面会计算RTO时间,重发SYN包。

结论

在许多场景中,例如应用程序启动时,并发发起多个TCP连接到数据库建立连接池,如果应用程序的TCP连接超时时间设置过短,就有可能出现上述部分连接失败的情况,这种情况偶然发生难以复现排查,对于kernel<3.3版本,出现上述问题的解决方案是加大unres_qlen的值。

参考文献

[1] http://blog.catchpoint.com/2014/04/29/understanding-rtt-impact-on-tcp-retransmissions/

[2] https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt 



PS.

复现这个问题的另一种简便方法是ping一个大包

从10.0.0.102 ping 10.0.0.101:

ARP相关内核参数unres_qlen研究

在10.0.0.101上抓包:

ARP相关内核参数unres_qlen研究

可以看到10.0.0.101接收到了10.0.0.102广播的ARP请求以及ICMP请求的最后几个包,因为10.0.0.101没有收到完整的ICMP请求,所以也就没有回包,10.0.0.102上体现出来就是丢包了。

让我们对比一下完整的ICMP请求的数据包:

ARP相关内核参数unres_qlen研究

然后我们修改unres_qlen的值为64,重复上面的操作:

ARP相关内核参数unres_qlen研究

正常ping通,说明unres_qlen参数的修改起作用了。

Tag:

相关推荐