24

记一次有惊无险的丢包调试经历

 4 years ago
source link: https://blog.huoding.com/2020/04/27/814
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

某个项目把服务器从 CentOS 操作系统从 5 升级到了 7(Linux 内核:3.10.0-693),一切都很顺利,直到我在服务器上闲逛的时候,无意间发现了一个「大问题」:网卡 eth0 在 RX 上存在丢包(dropped)现象,每隔一秒丢一个包!

uAV3m2Y.png!web

watch -d -n1 ‘ifconfig’

第一感觉怀疑是不是网卡的 ring buffer 太小了,通过「ethtool」确认:

shell> ethtool -g eth0
Ring parameters for eth0:
Pre-set maximums:
RX:		256
RX Mini:	0
RX Jumbo:	0
TX:		256
Current hardware settings:
RX:		256
RX Mini:	0
RX Jumbo:	0
TX:		256

看上去确实不大,可惜 Current hardware settings 已经达到 Pre-set maximums 最大值,没法加大了。为了确认网卡是否真的存在丢包,继续通过「ethtool」确认:

shell> ethtool -S eth0
no stats available

shell> ethtool -i eth0
driver: virtio_net
version: 1.0.0
firmware-version:
expansion-rom-version:
bus-info: 0000:00:04.0
supports-statistics: no
supports-test: no
supports-eeprom-access: no
supports-register-dump: no
supports-priv-flags: no

看上去是 kvm 的 virtio_net 不支持 statistics,好在还有别的方法:

shell> find /sys -name eth0
/sys/devices/pci0000:00/0000:00:04.0/virtio1/net/eth0
/sys/class/net/eth0

shell> cd /sys/devices/pci0000:00/0000:00:04.0/virtio1/net/eth0
shell> cd statistics
shell> grep . * | grep rx
rx_bytes:633037730314
rx_compressed:0
rx_crc_errors:0
rx_dropped:206975
rx_errors:0
rx_fifo_errors:0
rx_frame_errors:0
rx_length_errors:0
rx_missed_errors:0
rx_nohandler:0
rx_over_errors:0
rx_packets:4717658080

虽然 rx_dropped 不为零,但是 rx_errors 等却都为零,这说明 ring buffer 并没有出现溢出的情况,否则 rx_fifo_errors 之类的错误不可能为零,由此可以推断:网卡已经把数据完整交给了操作系统,其本身并没有丢包,真正丢包的是操作系统。

如何判断操作系统在哪里丢包的呢?是时候让 dropwatch 出场了:

shell> dropwatch -l kas
Initalizing kallsyms db

dropwatch> start
Enabling monitoring...
Kernel monitoring activated.
Issue Ctrl-C to stop monitoring
6 drops at ip_rcv+cf (0xffffffff815ca47f)
11 drops at ipv6_rcv+3ad (0xffffffff81643d7d)
75 drops at tcp_v4_rcv+87 (0xffffffff815f0197)
426 drops at sk_stream_kill_queues+50 (0xffffffff8157a740)
235 drops at tcp_rcv_state_process+1b0 (0xffffffff815e4fb0)
137 drops at tcp_v4_rcv+87 (0xffffffff815f0197)
11 drops at ipv6_rcv+3ad (0xffffffff81643d7d)
1 drops at __netif_receive_skb_core+3d2 (0xffffffff81586d82)

shell> grep -w -A 10 __netif_receive_skb_core /proc/kallsyms
ffffffff815869b0 t __netif_receive_skb_core
ffffffff81587170 t __netif_receive_skb
ffffffff815871d0 t netif_receive_skb_internal
ffffffff81587290 T netif_receive_skb
ffffffff81587300 t napi_gro_complete
ffffffff81587400 T napi_gro_flush
ffffffff81587490 T napi_complete_done
ffffffff81587550 T napi_complete
ffffffff81587570 T sk_busy_loop
ffffffff81587830 t net_rx_action
ffffffff81587bb0 t dev_gro_receive

关于 dropwatch 的原理,它是通过监控 kfree_skb 的调用来监控操作系统可能的丢包行为,有的丢包可能是正常行为,有的丢包可能是异常行为。如此说来,我们遇到的丢包会是上面哪个函数引起的呢?是正常的还是异常的呢?运气不好的话,可能得一个一个挨个分析,好在我们运气不错,记得文章开头我们提到过,在我们的问题中,每隔一秒丢一个包,于是自然而然的将目光锁定在 __netif_receive_skb_core 之上(丢包地址 0xffffffff81586d82 介于 ffffffff815869b0 和 ffffffff81587170 之间)。

查询一下 Linux 源代码 中 __netif_receive_skb_core 函数的定义来确认一下丢包原因:

static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{
    ...

    if (pfmemalloc && !skb_pfmemalloc_protocol(skb))
        goto drop;

    ...

drop:
    atomic_long_inc(&skb->dev->rx_dropped);
    kfree_skb(skb);
    /* Jamal, now you will not able to escape explaining
     * me how you were going to use this. :-)
     */
    ret = NET_RX_DROP;

    ...
}

static bool skb_pfmemalloc_protocol(struct sk_buff *skb)
{
    switch (skb->protocol) {
    case __constant_htons(ETH_P_ARP):
    case __constant_htons(ETH_P_IP):
    case __constant_htons(ETH_P_IPV6):
    case __constant_htons(ETH_P_8021Q):
    case __constant_htons(ETH_P_8021AD):
        return true;
    default:
        return false;
    }
}

当 pfmemalloc 为真,且 skb_pfmemalloc_protocol 中判断不支持包协议的时候,就会丢包。此外,代码里能看到调用了 kfree_skb,侧面验证了 dropwatch 的工作原理。

如何判断我们问题中丢的包协议是什么呢,是时候让 systemtap 出场了:

#! /usr/bin/env stap

probe kernel.function("__netif_receive_skb_core").label("drop") {
    printf("protocol: %x\n", $skb->protocol)
}

从前面我们对 Linux 源代码的分析,Linux 内核支持的包 protocol 如下:

#define ETH_P_ARP	0x0806	/* Address Resolution packet	*/
#define ETH_P_IP	0x0800	/* Internet Protocol packet	*/
#define ETH_P_IPV6	0x86DD	/* IPv6 over bluebook		*/
#define ETH_P_8021Q	0x8100  /* 802.1Q VLAN Extended Header  */
#define ETH_P_8021AD	0x88A8  /* 802.1ad Service VLAN		*/

而 systemtap 检测到的 包 protocol 是 400,并不在 Linux 支持的范畴,所以被丢掉了。

至于丢掉的包到底是什么,我也不知道,不过我在一些系统的 源代码 中找到一些端倪:

#define	ETHERTYPE_SPRITE	0x0500	/* ??? */
			     /* 0x0400	   Nixdorf */

我们不防推测一下:在机房的某个角落里躺着一个老款电子设备,它是一个名为 Nixdorf 的公司生产的,具体功能不详,但是每秒中会在网络中广播一个 EtherType 为 400 的包,不过我们的操作系统并不支持此 protocol。于是就丢弃了它。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK