遇到个奇怪的丢包统计问题:

1
2
3
4
5
6
# netstat -i
Kernel Interface table
Iface             MTU    RX-OK RX-ERR RX-DRP RX-OVR    TX-OK TX-ERR TX-DRP TX-OVR Flg
bond0            1500 4714238062      0 9955805 0      4206040873      0      2      0 BMmRU
ens1f0           1500 2145684247      0      3 0      1917444695      0      0      0 BMsRU
ens1f1           1500 2568553815      0      0 0      2288596177      0      0      0 BMsRU

如上,RX-DRP 只发生在 bond0,而它的 2 个从设备 ens1f0ens1f1RX-DRP 几乎为 0

很多时候,bond0RX-DRP 是其从设备的 RX-DRP 之和。所以,当看到如上 netstat -i 统计数据时,就觉得很奇怪;一气之下,开始排查起来。

1. 查看 bond0 的 RX-DRP 从何而来

快速推理一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# strace netstat -i
...
openat(AT_FDCWD, "/proc/net/dev", O_RDONLY) = 6
...

# cat /proc/net/dev
Inter-|   Receive                                                |  Transmit
 face |bytes    packets errs drop fifo frame compressed multicast|bytes    packets errs drop fifo colls carrier compressed
ens1f0: 1195670686250 2150075191    0    3    0     0          0  15367468 357767232222 1920053622    0    0    0     0       0          0
ens1f1: 933651146360 2571565810    0    0    0     0          0  15376597 444981899232 2291732525    0    0    0     0       0          0
 bond0: 2129321832610 4721641001    0 10018411    0     0          0  30744065 802749131454 4211786147    0    2    0     0       0          0

cat /proc/net/dev 对应的源代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// net/core/net-procfs.c

static void dev_seq_printf_stats(struct seq_file *seq, struct net_device *dev)
{
    struct rtnl_link_stats64 temp;
    const struct rtnl_link_stats64 *stats = dev_get_stats(dev, &temp);

    seq_printf(seq, "%6s: %7llu %7llu %4llu %4llu %4llu %5llu %10llu %9llu "
               "%8llu %7llu %4llu %4llu %4llu %5llu %7llu %10llu\n",
               dev->name, stats->rx_bytes, stats->rx_packets,
               stats->rx_errors,
               stats->rx_dropped + stats->rx_missed_errors,
               stats->rx_fifo_errors,
               ...
               stats->tx_compressed);
}

/*
 *  Called from the PROCfs module. This now uses the new arbitrary sized
 *  /proc/net interface to create /proc/net/dev
 */
static int dev_seq_show(struct seq_file *seq, void *v)
{
    if (v == SEQ_START_TOKEN)
        seq_puts(seq, "Inter-|   Receive                            "
                 "                    |  Transmit\n"
                 " face |bytes    packets errs drop fifo frame "
                 "compressed multicast|bytes    packets errs "
                 "drop fifo colls carrier compressed\n");
    else
        dev_seq_printf_stats(seq, v);
    return 0;
}

// net/core/dev.c

struct rtnl_link_stats64 *dev_get_stats(struct net_device *dev,
                                        struct rtnl_link_stats64 *storage)
{
    storage->rx_dropped += (unsigned long)atomic_long_read(&dev->rx_dropped);
    storage->tx_dropped += (unsigned long)atomic_long_read(&dev->tx_dropped);
    storage->rx_nohandler += (unsigned long)atomic_long_read(&dev->rx_nohandler);
    return storage;
}
EXPORT_SYMBOL(dev_get_stats);

基本可以确认,bond0RX-DRP 就是 dev->rx_dropped 的值。

使用 drgn 工具查看 bond0rx_dropped

1
2
3
4
5
6
# drgn
>>> bond0 = netdev_get_by_name('bond0')
>>> bond0.rx_dropped
(atomic_long_t){
    .counter = (s64)10018860,
}

确实如此。

2. rx_dropped 的来源

直接搜索 net/core/dev.crx_dropped 的使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// net/core/dev.c

/*
 * enqueue_to_backlog is called to queue an skb to a per CPU backlog
 * queue (may be a remote CPU queue).
 */
static int enqueue_to_backlog(struct sk_buff *skb, int cpu,
                              unsigned int *qtail)
{
    sd = &per_cpu(softnet_data, cpu);

    local_irq_save(flags);

    rps_lock(sd);
    if (!netif_running(skb->dev))
        goto drop;

drop:
    sd->dropped++;
    rps_unlock(sd);

    local_irq_restore(flags);

    atomic_long_inc(&skb->dev->rx_dropped);
    kfree_skb(skb);
    return NET_RX_DROP;
}


static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc,
                                    struct packet_type **ppt_prev)
{
    if (pt_prev) {
        if (unlikely(skb_orphan_frags_rx(skb, GFP_ATOMIC)))
            goto drop;
        *ppt_prev = pt_prev;
    } else {
drop:
        if (!deliver_exact)
            atomic_long_inc(&skb->dev->rx_dropped);
        else
            atomic_long_inc(&skb->dev->rx_nohandler);
        kfree_skb(skb);
        ret = NET_RX_DROP;
    }
out:
    return ret;
}

共有 2 个地方会增加 rx_dropped;究竟是哪个地方导致了 bond0RX-DRP 增加呢?

留意到 enqueue_to_backlog 函数中有 sd->dropped++,如果所有 CPU 核的 sd->dropped 之和等于 bond0rx_dropped,那么就可以确定是这个地方导致的。

1
2
3
# cat /proc/net/softnet_stat
04f93eff 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
...

对应内核源代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// net/core/net-procfs.c

static int softnet_seq_show(struct seq_file *seq, void *v)
{
    seq_printf(seq,
               "%08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x\n",
               sd->processed, sd->dropped, sd->time_squeeze, 0,
               ...
               softnet_backlog_len(sd), (int)seq->index);
    return 0;
}

也就是,/proc/net/softnet_stat 中的第二个字段就是 sd->dropped 的值。

但,/proc/net/softnet_stat 中的 sd->dropped 之和是 0;所以,bond0rx_dropped 不是由 enqueue_to_backlog 函数导致的。

为什么 __netif_receive_skb_core 函数会导致 bond0rx_dropped 增加呢?

3. bond 设备收包过程

再确认一下 server 上的网络配置:

  1. bond0 是一个 802.3ad 模式的 bond 设备;
  2. ens1f0ens1f1bond0 的从设备;
  3. bond0 没有配置 VLAN。

再次看下 __netif_receive_skb_core 函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// net/core/dev.c

static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc,
                                    struct packet_type **ppt_prev)
{

    trace_netif_receive_skb(skb);

    orig_dev = skb->dev;

    pt_prev = NULL;

another_round:
    skb->skb_iif = skb->dev->ifindex;

    __this_cpu_inc(softnet_data.processed);

    rx_handler = rcu_dereference(skb->dev->rx_handler);
    if (rx_handler) {
        if (pt_prev) {
            ret = deliver_skb(skb, pt_prev, orig_dev);
            pt_prev = NULL;
        }
        switch (rx_handler(&skb)) {
        case RX_HANDLER_CONSUMED:
            ret = NET_RX_SUCCESS;
            goto out;
        case RX_HANDLER_ANOTHER:
            goto another_round;
        case RX_HANDLER_EXACT:
            deliver_exact = true;
            break;
        case RX_HANDLER_PASS:
            break;
        default:
            BUG();
        }
    }

    type = skb->protocol;

    /* deliver only exact match when indicated */
    if (likely(!deliver_exact)) {
        deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
                               &ptype_base[ntohs(type) &
                                           PTYPE_HASH_MASK]);
    }

    deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
                           &orig_dev->ptype_specific);

    if (unlikely(skb->dev != orig_dev)) {
        deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
                               &skb->dev->ptype_specific);
    }

    if (pt_prev) {
        if (unlikely(skb_orphan_frags_rx(skb, GFP_ATOMIC)))
            goto drop;
        *ppt_prev = pt_prev;
    } else {
drop:
        if (!deliver_exact)
            atomic_long_inc(&skb->dev->rx_dropped);
        else
            atomic_long_inc(&skb->dev->rx_nohandler);
        kfree_skb(skb);
        ret = NET_RX_DROP;
    }

out:
    *pskb = skb;
    return ret;
}

bond 设备收包的核心逻辑:

第一轮从设备收包:

  1. skb->dev 是从设备;
  2. skb->dev->rx_handlerbond0 的接收处理函数;
  3. bond0 的接收处理函数会优先处理 LACP 协议包;
  4. 如果不是 LACP 协议包,则 skb->dev->rx_handler 返回 RX_HANDLER_ANOTHER,表示需要下一轮处理。

第二轮 bond 设备收包:

  1. skb->devbond0
  2. skb->dev->rx_handlerNULL,表示没有接收处理函数;
  3. 直接进行协议类型匹配;
  4. 如果没有匹配到协议类型,则 pt_prev 为空,进入 drop 分支;
  5. 没有精确匹配,develiver_exactfalse,所以会增加 bond0rx_dropped

bond 从设备的 rx_handler 处理函数是 bond_handle_frame:

1
2
3
4
5
6
7
8
9
// drivers/net/bonding/bond_main.c

/* enslave device <slave> to bond device <master> */
int bond_enslave(struct net_device *bond_dev, struct net_device *slave_dev,
                 struct netlink_ext_ack *extack)
{
    res = netdev_rx_handler_register(slave_dev, bond_handle_frame,
                                     new_slave);
}

使用 bpfsnoop 可以确认:

1
2
3
# ./bpfsnoop -t netif_receive_skb --filter-arg 'skb->dev->ifindex == 3' --output-arg 'skb->dev->rx_handler'
- netif_receive_skb[tp] args=((struct sk_buff *)skb=0xffff9f527cf2ea00) cpu=5 process=(0:swapper/5) timestamp=10:55:39.608250045
Arg attrs: (rx_handler_func_t *)'skb->dev->rx_handler'=0xffffffffc0f08490(bond_handle_frame)

bond 设备的接收处理函数 bond_handle_frame 的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// drivers/net/bonding/bond_main.c

static rx_handler_result_t bond_handle_frame(struct sk_buff **pskb)
{
    int ret = RX_HANDLER_ANOTHER;

    *pskb = skb;

    slave = bond_slave_get_rcu(skb->dev);
    bond = slave->bond;

    recv_probe = READ_ONCE(bond->recv_probe);
    if (recv_probe) {
        ret = recv_probe(skb, bond, slave);
        if (ret == RX_HANDLER_CONSUMED) {
            consume_skb(skb);
            return ret;
        }
    }

    if (bond_should_deliver_exact_match(skb, slave, bond)) {
        if (is_link_local_ether_addr(eth_hdr(skb)->h_dest))
            return RX_HANDLER_PASS;
        return RX_HANDLER_EXACT;
    }

    skb->dev = bond->dev;

    return ret;
}

从上面的 __netif_receive_skb_core 函数可以看到,bond_handle_frame 函数要么返回了 RX_HANDLER_ANOTHER,要么返回了 RX_HANDLER_PASS

使用 bpfsnoop 确认:

1
2
3
4
5
6
# ./bpfsnoop -k '(b)bond_handle_frame' --output-arg '(*pskb)->dev->name' --kfunc-kmods bonding -o bond.bpfsnoop.log
# less bond.bpfsnoop.log
→ bond_handle_frame args=((struct sk_buff **)pskb=0xffffbc780c85ccc8) cpu=5 process=(0:swapper/5) timestamp=11:03:00.645740007
Arg attrs: (array(char[16]))'(*pskb)->dev->name'="ens1f0"
← bond_handle_frame args=((struct sk_buff **)pskb=0xffffbc780c85ccc8) retval=(enum rx_handler_result)RX_HANDLER_ANOTHER cpu=5 process=(0:swapper/5) duration=5.795µs timestamp=11:03:00.645808268
Arg attrs: (array(char[16]))'(*pskb)->dev->name'="bond0"

bond.bpfsnoop.log 里没有看到 RX_HANDLER_PASS 的返回值。

而且,bond_handle_frame 函数将 skb->dev 设置为 bond->dev,也就是 bond0 设备。

直至这里,可以推断出丢包的原因:收到了 bond 设备及其从设备都不支持的协议包,导致丢包了,并且统计到了 bond0rx_dropped

4. 不支持的协议包

使用 bpfsnoop 进一步确认 skb->protocol 的值:

1
2
3
4
5
6
# ./bpfsnoop -k '(b)bond_handle_frame' --output-arg '(*pskb)->dev->name' --output-arg '(*pskb)->protocol' --kfunc-kmods bonding -o bond.bpfsnoop.log
→ bond_handle_frame args=((struct sk_buff **)pskb=0xffffbc780d390cc8) cpu=60 process=(0:swapper/60) times
tamp=11:10:43.435883189
Arg attrs: (array(char[16]))'(*pskb)->dev->name'="ens1f1", (__be16)'(*pskb)->protocol'=0x400/1024
← bond_handle_frame args=((struct sk_buff **)pskb=0xffffbc780d390cc8) retval=(enum rx_handler_result)RX_HANDLER_ANOTHER cpu=60 process=(0:swapper/60) duration=4.629µs timestamp=11:10:43.435915497
Arg attrs: (array(char[16]))'(*pskb)->dev->name'="bond0", (__be16)'(*pskb)->protocol'=0x400/1024

过滤掉正常的协议包,留意到有 0x400 的协议包:

1
2
// include/uapi/linux/if_ether.h
#define ETH_P_802_2 0x0004      /* 802.2 frames         */

这是什么包?

不清楚也没关系,直接用 tcpdump 抓包看看:

1
2
3
4
5
6
7
8
9
# tcpdump -i any -nnnev ether proto 0x0004
tcpdump: data link type LINUX_SLL2
tcpdump: listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
11:14:07.922458 ens1f0 M   ifindex 3 48:2c:d0:1e:f0:b1 802.2, length 66: LLC, dsap STP (0x42) Individual, ssap STP (0x42) Command, ctrl 0x03: STP 802.1w, Rapid STP, Flags [Learn, Forward, Agreement], bridge-id 8000.48:2c:d0:1e:f0:b1.8001, length 43
    message-age 0.00s, max-age 20.00s, hello-time 2.00s, forwarding-delay 15.00s
    root-id 8000.48:2c:d0:1e:f0:b1, root-pathcost 0, port-role Designated
11:14:07.922458 bond0 M   ifindex 8 48:2c:d0:1e:f0:b1 802.2, length 66: LLC, dsap STP (0x42) Individual, ssap STP (0x42) Command, ctrl 0x03: STP 802.1w, Rapid STP, Flags [Learn, Forward, Agreement], bridge-id 8000.48:2c:d0:1e:f0:b1.8001, length 43
    message-age 0.00s, max-age 20.00s, hello-time 2.00s, forwarding-delay 15.00s
    root-id 8000.48:2c:d0:1e:f0:b1, root-pathcost 0, port-role Designated

啊哈,这是 STP 协议包。

bond 设备及其从设备支持 STP 协议包吗?

使用 drgn 确认一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# drgn
>>> ens = netdev_get_by_name('ens1f0')
>>> ens.ptype_specific
(struct list_head){
    .next = (struct list_head *)0xffff9f5284af25b8,
    .prev = (struct list_head *)0xffff9f5284af25b8,
}
>>> bond0 = netdev_get_by_name('bond0')
>>> bond0.ptype_specific
(struct list_head){
    .next = (struct list_head *)0xffff9f5248c8b090,
    .prev = (struct list_head *)0xffff9f5248c8b090,
}
>>> ptype_base = prog.variable('ptype_base')
>>> ptype_base[4]
(struct list_head){
    .next = (struct list_head *)ptype_base+0x40 = 0xffffffff86e37000,
    .prev = (struct list_head *)ptype_base+0x40 = 0xffffffff86e37000,
}

确认:协议栈和设备都不支持 STP 协议包。

5. 丢包根因

通过 ip linkbridge 等命令确认:server 侧没有配置 bridge 设备。

但为什么会收到 STP 协议包呢?

直接咨询物理网络的同事,确认是交换机侧开启了 stp edged-port enable 配置,导致交换机定时向所有端口发送 STP 协议包。

6. 总结

  1. bond0RX-DRP 是由于收到了 bond 设备及其从设备都不支持的协议包,导致丢包了,并且统计到了 bond0rx_dropped
  2. 通过 bpfsnoop 动态追踪 bond_handle_frame 函数,确认了 bond0 的收包处理逻辑。
  3. 使用 tcpdump 抓包确认了收到的协议包是 STP 协议包。

Linux 网络经验++!