遇到个奇怪的丢包统计问题:
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 个从设备 ens1f0
和 ens1f1
的 RX-DRP 几乎为 0。
很多时候,bond0
的 RX-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);
|
基本可以确认,bond0
的 RX-DRP 就是 dev->rx_dropped
的值。
使用 drgn
工具查看 bond0
的 rx_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.c
中 rx_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
;究竟是哪个地方导致了 bond0
的 RX-DRP 增加呢?
留意到 enqueue_to_backlog
函数中有 sd->dropped++
,如果所有 CPU 核的 sd->dropped
之和等于 bond0
的 rx_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;所以,bond0
的 rx_dropped
不是由 enqueue_to_backlog
函数导致的。
为什么 __netif_receive_skb_core
函数会导致 bond0
的 rx_dropped
增加呢?
3. bond 设备收包过程
再确认一下 server 上的网络配置:
bond0
是一个 802.3ad
模式的 bond 设备;
ens1f0
和 ens1f1
是 bond0
的从设备;
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 设备收包的核心逻辑:
第一轮从设备收包:
skb->dev
是从设备;
skb->dev->rx_handler
是 bond0
的接收处理函数;
bond0
的接收处理函数会优先处理 LACP 协议包;
- 如果不是 LACP 协议包,则
skb->dev->rx_handler
返回 RX_HANDLER_ANOTHER
,表示需要下一轮处理。
第二轮 bond 设备收包:
skb->dev
是 bond0
;
skb->dev->rx_handler
是 NULL
,表示没有接收处理函数;
- 直接进行协议类型匹配;
- 如果没有匹配到协议类型,则
pt_prev
为空,进入 drop
分支;
- 没有精确匹配,
develiver_exact
为 false
,所以会增加 bond0
的 rx_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 设备及其从设备都不支持的协议包,导致丢包了,并且统计到了 bond0
的 rx_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 link
和 bridge
等命令确认:server 侧没有配置 bridge 设备。
但为什么会收到 STP 协议包呢?
直接咨询物理网络的同事,确认是交换机侧开启了 stp edged-port enable
配置,导致交换机定时向所有端口发送 STP 协议包。
6. 总结
bond0
的 RX-DRP 是由于收到了 bond 设备及其从设备都不支持的协议包,导致丢包了,并且统计到了 bond0
的 rx_dropped
。
- 通过
bpfsnoop
动态追踪 bond_handle_frame
函数,确认了 bond0
的收包处理逻辑。
- 使用
tcpdump
抓包确认了收到的协议包是 STP 协议包。
Linux 网络经验++!