Unix Like, 网络

UDP SndbufErrors & ENOBUFS

最近一部分服务器上,遇到UDP发包速率太高会出现大量丢包的情况。这个丢包不是发生在中间网络设备上丢,也不在接收方上,而是发生在发送方kernel中。为什么会知道是在kernel丢的?因为用户空间程序的统计的发包量,跟内核统计的有很大差距,所以可以肯定用户空间把包交给内核后,内核并没有全发出去。

通过kernel的snmp,发现UDP的SndbufErrors计数器有很高的值:

这个计数器是在哪、什么情况下增长的?翻了一下kernel的源码,在net/ipv4/udp.c中找到两个,一个在udp_sendmsg()中:

另一个在udp_send_skb()中:

在udp_sendmsg()的流程中,是有流向udp_send_skb()的,另外,在udp_send_skb()调用的ip_send_skb()中,有递增另一个计数器OutDiscards的代码:

再查看一下snmp中的IP计数器,发现OutDiscards的值跟SndbufErrors非常接近:

所以这里可以肯定这个计数器的增长是发生在udp_send_skb()。

按照代码中的注释,ENOBUFS代表no kernel mem,首先,可以排除是send buffer不足的问题,因为如果send buffer满了,相关写调用会被阻塞或者返回EWOULDBLOCK;另外,net_xmit_errno()这个宏,会把非NET_XMIT_CN的错误都转成ENOBUFS:

所以这里ip_local_out()返回的很可能并不是ENOBUFS。

ip_local_out()后有函数指针,不太好看出来实际调用的是哪个函数,用perf看了下调用栈,后面涉及的函数还是挺多的,懒得一个个翻:

找了个更直接的工具dropwatch

其实结果还是不算太直接,不过可以确定丢包的时候会调用kfree_skb_list(),在调用栈的函数中,顺利找到了对这个函数的调用,在net/core/dev.c中:

到这里算是找到了问题出在哪了,q->enqueue()调用的是qdisc部分的机制,赶紧看了下qdisc的统计数据:

统计中的dropped计数器与SndbufErrors的值非常接近,eno1的队列是fq,fq的源码在net/sched/sch_fq.c中:

如果超出了flow_limit的限制,会增加flows_plimit计数器的值,从统计数据中可以看到flows_plimit计数器与dropped一致,到这里又可以肯定,是排队的包超出了flow_limit,所以无法入队的包都被丢弃了。

 

前面的tc命令看到flow_limit只有100,系统设置的send buffer是212992,包的MTU是1400,212992 / 1400 ≈ 152,的确会超出100,于是调大到200:

再次运行UDP协议的程序发送数据,丢包率恢复正常,查看qdisc的统计,dropped计数器一直为0,问题解决!