最近工做中遇到某个服务器应用程序 UDP 丢包,在排查过程当中查阅了不少资料,总结出来这篇文章,供更多人参考。linux
在开始以前,咱们先用一张图解释 linux 系统接收网络报文的过程。nginx
在接收 UDP 报文的过程当中,图中任何一个过程均可能会主动或者被动地把报文丢弃,所以丢包可能发生在网卡和驱动,也可能发生在系统和应用。缓存
之因此没有分析发送数据流程,一是由于发送流程和接收相似,只是方向相反;另外发送流程报文丢失的几率比接收小,只有在应用程序发送的报文速率大于内核和网卡处理速率时才会发生。bash
本篇文章假定机器只有一个名字为 eth0
的 interface,若是有多个 interface 或者 interface 的名字不是 eth0,请按照实际状况进行分析。服务器
NOTE:文中出现的 RX
(receive) 表示接收报文,TX
(transmit) 表示发送报文。网络
要查看网卡是否有丢包,可使用 ethtool -S eth0
查看,在输出中查找 bad
或者 drop
对应的字段是否有数据,在正常状况下,这些字段对应的数字应该都是 0。若是看到对应的数字在不断增加,就说明网卡有丢包。异步
另一个查看网卡丢包数据的命令是 ifconfig
,它的输出中会有 RX
(receive 接收报文)和 TX
(transmit 发送报文)的统计数据:socket
~# ifconfig eth0 ... RX packets 3553389376 bytes 2599862532475 (2.3 TiB) RX errors 0 dropped 1353 overruns 0 frame 0 TX packets 3479495131 bytes 3205366800850 (2.9 TiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 ...
此外,linux 系统也提供了各个网络协议的丢包信息,可使用 netstat -s
命令查看,加上 --udp
能够只看 UDP 相关的报文数据:tcp
[root@holodesk02 GOD]# netstat -s -u IcmpMsg: InType0: 3 InType3: 1719356 InType8: 13 InType11: 59 OutType0: 13 OutType3: 1737641 OutType8: 10 OutType11: 263 Udp: 517488890 packets received 2487375 packets to unknown port received. 47533568 packet receive errors 147264581 packets sent 12851135 receive buffer errors 0 send buffer errors UdpLite: IpExt: OutMcastPkts: 696 InBcastPkts: 2373968 InOctets: 4954097451540 OutOctets: 5538322535160 OutMcastOctets: 79632 InBcastOctets: 934783053 InNoECTPkts: 5584838675
对于上面的输出,关注下面的信息来查看 UDP 丢包的状况:
packet receive errors
不为空,而且在一直增加说明系统有 UDP 丢包packets to unknown port received
表示系统接收到的 UDP 报文所在的目标端口没有应用在监听,通常是服务没有启动致使的,并不会形成严重的问题receive buffer errors
表示由于 UDP 的接收缓存过小致使丢包的数量NOTE: 并非丢包数量不为零就有问题,对于 UDP 来讲,若是有少许的丢包极可能是预期的行为,好比丢包率(丢包数量/接收报文数量)在万分之一甚至更低。
以前讲过,若是 ethtool -S eth0
中有 rx_***_errors
那么极可能是网卡有问题,致使系统丢包,须要联系服务器或者网卡供应商进行处理。
# ethtool -S eth0 | grep rx_ | grep errors rx_crc_errors: 0 rx_missed_errors: 0 rx_long_length_errors: 0 rx_short_length_errors: 0 rx_align_errors: 0 rx_errors: 0 rx_length_errors: 0 rx_over_errors: 0 rx_frame_errors: 0 rx_fifo_errors: 0
netstat -i
也会提供每一个网卡的接发报文以及丢包的状况,正常状况下输出中 error 或者 drop 应该为 0。
若是硬件或者驱动没有问题,通常网卡丢包是由于设置的缓存区(ring buffer)过小,可使用 ethtool
命令查看和设置网卡的 ring buffer。
ethtool -g
能够查看某个网卡的 ring buffer,好比下面的例子
# ethtool -g eth0 Ring parameters for eth0: Pre-set maximums: RX: 4096 RX Mini: 0 RX Jumbo: 0 TX: 4096 Current hardware settings: RX: 256 RX Mini: 0 RX Jumbo: 0 TX: 256
Pre-set 表示网卡最大的 ring buffer 值,可使用 ethtool -G eth0 rx 8192
设置它的值。
linux 系统丢包的缘由不少,常见的有:UDP 报文错误、防火墙、UDP buffer size 不足、系统负载太高等,这里对这些丢包缘由进行分析。
若是在传输过程当中UDP 报文被修改,会致使 checksum 错误,或者长度错误,linux 在接收到 UDP 报文时会对此进行校验,一旦发明错误会把报文丢弃。
若是但愿 UDP 报文 checksum 及时有错也要发送给应用程序,能够在经过 socket 参数禁用 UDP checksum 检查:
int disable = 1; setsockopt(sock_fd, SOL_SOCKET, SO_NO_CHECK, (void*)&disable, sizeof(disable)
若是系统防火墙丢包,表现的行为通常是全部的 UDP 报文都没法正常接收,固然不排除防火墙只 drop 一部分报文的可能性。
若是遇到丢包比率很是大的状况,请先检查防火墙规则,保证防火墙没有主动 drop UDP 报文。
linux 系统在接收报文以后,会把报文保存到缓存区中。由于缓存区的大小是有限的,若是出现 UDP 报文过大(超过缓存区大小或者 MTU 大小)、接收到报文的速率太快,均可能致使 linux 由于缓存满而直接丢包的状况。
在系统层面,linux 设置了 receive buffer 能够配置的最大值,能够在下面的文件中查看,通常是 linux 在启动的时候会根据内存大小设置一个初始值。
可是这些初始值并非为了应对大流量的 UDP 报文,若是应用程序接收和发送 UDP 报文很是多,须要讲这个值调大。可使用 sysctl
命令让它当即生效:
sysctl -w net.core.rmem_max=26214400 # 设置为 25M
也能够修改 /etc/sysctl.conf
中对应的参数在下次启动时让参数保持生效。
若是报文报文过大,能够在发送方对数据进行分割,保证每一个报文的大小在 MTU 内。
另一个能够配置的参数是 netdev_max_backlog
,它表示 linux 内核从网卡驱动中读取报文后能够缓存的报文数量,默认是 1000,能够调大这个值,好比设置成 2000:
sudo sysctl -w net.core.netdev_max_backlog=2000
系统 CPU、memory、IO 负载太高都有可能致使网络丢包,好比 CPU 若是负载太高,系统没有时间进行报文的 checksum 计算、复制内存等操做,从而致使网卡或者 socket buffer 出丢包;memory 负载太高,会应用程序处理过慢,没法及时处理报文;IO 负载太高,CPU 都用来响应 IO wait,没有时间处理缓存中的 UDP 报文。
linux 系统自己就是相互关联的系统,任何一个组件出现问题都有可能影响到其余组件的正常运行。对于系统负载太高,要么是应用程序有问题,要么是系统不足。对于前者须要及时发现,debug 和修复;对于后者,也要及时发现并扩容。
上面提到系统的 UDP buffer size,调节的 sysctl 参数只是系统容许的最大值,每一个应用程序在建立 socket 时须要设置本身 socket buffer size 的值。
linux 系统会把接受到的报文放到 socket 的 buffer 中,应用程序从 buffer 中不断地读取报文。因此这里有两个和应用有关的因素会影响是否会丢包:socket buffer size 大小以及应用程序读取报文的速度。
对于第一个问题,能够在应用程序初始化 socket 的时候设置 socket receive buffer 的大小,好比下面的代码把 socket buffer 设置为 20MB:
uint64_t receive_buf_size = 20*1024*1024; //20 MB setsockopt(socket_fd, SOL_SOCKET, SO_RCVBUF, &receive_buf_size, sizeof(receive_buf_size));
若是不是本身编写和维护的程序,修改应用代码是件很差甚至不太可能的事情。不少应用程序会提供配置参数来调节这个值,请参考对应的官方文档;若是没有可用的配置参数,只能给程序的开发者提 issue 了。
很明显,增长应用的 receive buffer 会减小丢包的可能性,但同时会致使应用使用更多的内存,因此须要谨慎使用。
另一个因素是应用读取 buffer 中报文的速度,对于应用程序来讲,处理报文应该采起异步的方式
想要详细了解 linux 系统在执行哪一个函数时丢包的话,可使用 dropwatch
工具,它监听系统丢包信息,并打印出丢包发生的函数地址:
# dropwatch -l kas Initalizing kallsyms db dropwatch> start Enabling monitoring... Kernel monitoring activated. Issue Ctrl-C to stop monitoring 1 drops at tcp_v4_do_rcv+cd (0xffffffff81799bad) 10 drops at tcp_v4_rcv+80 (0xffffffff8179a620) 1 drops at sk_stream_kill_queues+57 (0xffffffff81729ca7) 4 drops at unix_release_sock+20e (0xffffffff817dc94e) 1 drops at igmp_rcv+e1 (0xffffffff817b4c41) 1 drops at igmp_rcv+e1 (0xffffffff817b4c41)
经过这些信息,找到对应的内核代码处,就能知道内核在哪一个步骤中把报文丢弃,以及大体的丢包缘由。
此外,还可使用 linux perf 工具监听 kfree_skb
(把网络报文丢弃时会调用该函数) 事件的发生:
sudo perf record -g -a -e skb:kfree_skb sudo perf script
关于 perf 命令的使用和解读,网上有不少文章能够参考。