#220330 问题

ixgbe Detected Tx Unit Hang

现象是网卡不断重启,查看 /var/log/message 可以看到以下错误信息:

Mar 25 15:35:53: [686973.797868] ixgbe 0000:83:00.0 p4p1: Detected Tx Unit Hang (XDP)
Mar 25 15:35:53: [686973.797868]   Tx Queue             <24>
Mar 25 15:35:53: [686973.797868]   TDH, TDT             <84>, <136>
Mar 25 15:35:53: [686973.797868]   next_to_use          <136>
Mar 25 15:35:53: [686973.797868]   next_to_clean        <84>
Mar 25 15:35:53: [686973.797868] tx_buffer_info[next_to_clean]
Mar 25 15:35:53: [686973.797868]   time_stamp           <0>
Mar 25 15:35:53: [686973.797868]   jiffies              <128edcdc0>
...
Mar 25 15:35:53: [686973.797955] ixgbe 0000:83:00.0 p4p1: tx hang 1 detected on queue 25, resetting adapter
...
15:35:53 kernel: [687078.764725] bond0: link status down for interface p4p2, disabling it in 200 ms

这个问题是 ixgbe 驱动 xdp 实现 BUG 导致,出现在 cpu 比较多的机器。

When the received core is such that the xdp queue falls beyond MAX_TX_QUEUES(64), then the hang results. In other words, if I leave ethtool -L eno1 combined 40 (the default), and a packet is received on core 24 or greater, it hangs. However, if I lower the tx queue count to 24 (since XDP is forced to nr_cpu_ids), or if I force the incoming packets onto core < 24 with an ntuple filter, then no hang occurs.

# ethtool -l p4p1
Channel parameters for p4p1:
Pre-set maximums:
RX:         0
TX:         0
Other:              1
Combined:   63
Current hardware settings:
RX:         0
TX:         0
Other:              1
Combined:   28
# nproc
56
# uname -r
4.19.113-88.4bs.el7.centos.x86_64

combined + xdp queues(nr_cpu_ids) > max_tx_queues(64) 就会出现这个问题。

详细问题讨论在这两个 thread 中:

不升级内核的情况下,可以通过 ethtool -L p4p1 combined N 命令调低 combined 的个数,绕过这个 BUG,但是会影响性能。

BUG 在 4.20-rc1 已经修复。参见:https://github.com/torvalds/linux/commit/8d7179b1e2d64b3493c0114916486fe92e6109a9

另一个解决问题的方法是设定 XDP 加载模式为 skb 模式,同样会影响性能,但是不需要升级内核。

i40e/X722 unable to handle kernel NULL pointer dereference

i40e 驱动,网卡是 X722 型号的,在加载 xdp 的时候会报这个错误。

# ethtool -i enp61s0f0
driver: i40e
version: 2.3.2-k
...
# lshw -class network -short
H/W path         Device      Class          Description
=======================================================
/0/0/0/3/0       enp61s0f0   network        Ethernet Connection X722 for 10GbE SFP+
/0/0/0/3/0.1     enp61s0f1   network        Ethernet Connection X722 for 10GbE SFP+
/3               bond0       network        Ethernet interface

错误信息如下:

i40e 0000:3d:00.0: failed to get tracking for 256 queues for VSI 0 err -12
i40e 0000:3d:00.0: setup of MAIN VSI failed
i40e 0000:3d:00.0: can't remove VEB 160 with 0 VSIs left
BUG: unable to handle kernel NULL pointer dereference at 0000000000000000
bond0: link status down for interface enp61s0f0, disabling it in 200 ms
PGD 1fe4ee7067 P4D 1fe4ee7067 PUD 1ef96f8067 PMD 0
Oops: 0000 [#1] SMP NOPTI
CPU: 21 PID: 809441 Comm: l4lb Tainted: G           OE     4.19.113-88.6bs.el7.x86_64 #1
Hardware name: Sugon I420-G30/60P16-US, BIOS 0JGST219 05/30/2019
RIP: 0010:i40e_xdp+0xae/0x110 [i40e]
i40e 0000:3d:00.0: failed to get tracking for 256 queues for VSI 0 err -12
i40e 0000:3d:00.0: setup of MAIN VSI failed
i40e 0000:3d:00.0: can't remove VEB 160 with 0 VSIs left
BUG: unable to handle kernel NULL pointer dereference at 0000000000000000
bond0: link status down for interface enp61s0f0, disabling it in 200 ms
PGD 1fe4ee7067 P4D 1fe4ee7067 PUD 1ef96f8067 PMD 0
Oops: 0000 [#1] SMP NOPTI
Oops: 0000 [#1] SMP NOPTI
CPU: 21 PID: 809441 Comm: l4lb Tainted: G           OE     4.19.113-88.6bs.el7.x86_64 #1
Hardware name: Sugon I420-G30/60P16-US, BIOS 0JGST219 05/30/2019
RIP: 0010:i40e_xdp+0xae/0x110 [i40e]
Code: 87 ab d0 0c 00 00 84 c0 75 60 31 c0 66 83 bb f6 0c 00 00 00 74 27 48 8b 93 90 0c 00 00 48 63 f0 48 8b 8b d0 0c 00 00 83 c0 01 <48> 8b 14 f2 48 89 4a 20 0f b7 93 f6 0c 00 00 39 d0 7c d9 48 85 ed
bond0: link status down for interface enp61s0f0, disabling it in 200 ms
RSP: 0018:ffff9d00ea6af8c0 EFLAGS: 00010202
RAX: 0000000000000001 RBX: ffff88f5f75fc000 RCX: ffff9d00c01ef000
RDX: 0000000000000000 RSI: 0000000000000000 RDI: ffff88f5ffcd68f0
RBP: 0000000000000000 R08: 0000000000000070 R09: 0000000000027e7c
R10: ffff89063ff468c0 R11: 0000000000000707 R12: ffff88f5f7600000
R13: ffffffffc0308aa0 R14: ffffffffc032c5a0 R15: 000000000000000f
FS:  00007f25f9ffb700(0000) GS:ffff88f5ffcc0000(0000) knlGS:0000000000000000
bond0: link status down for interface enp61s0f0, disabling it in 200 ms
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 0000000000000000 CR3: 0000001fb3828004 CR4: 00000000007606e0
DR0: 0000000000000000 DR1: 0000000000000000 DR2: 0000000000000000
DR3: 0000000000000000 DR6: 00000000fffe0ff0 DR7: 0000000000000400
PKRU: 55555554
Call Trace:
 dev_xdp_install+0x4f/0x70
bond0: link status down for interface enp61s0f0, disabling it in 200 ms
 dev_change_xdp_fd+0x103/0x210
 ? dev_disable_lro+0xa0/0xa0
bond0: link status down for interface enp61s0f0, disabling it in 200 ms
 do_setlink+0xd63/0xd90
 ? cpumask_next+0x16/0x20
 ? do_kernel_range_flush+0x50/0x50
bond0: link status down for interface enp61s0f0, disabling it in 200 ms
 ? nla_parse+0xa4/0x120
 rtnl_setlink+0xd7/0x140
 ? security_capset+0x50/0x70
bond0: link status down for interface enp61s0f0, disabling it in 200 ms
 rtnetlink_rcv_msg+0x28f/0x350
 ? _cond_resched+0x15/0x30
 ? __kmalloc_node_track_caller+0x188/0x280
bond0: link status down for interface enp61s0f0, disabling it in 200 ms
 ? rtnl_calcit.isra.27+0x110/0x110
 netlink_rcv_skb+0xd4/0x110
 netlink_unicast+0x182/0x230
bond0: link status down for interface enp61s0f0, disabling it in 200 ms
 netlink_sendmsg+0x2ed/0x3e0
 sock_sendmsg+0x36/0x50
 __sys_sendto+0xdc/0x160
bond0: link status down for interface enp61s0f0, disabling it in 200 ms
 ? sock_setsockopt+0x3b9/0xc80
 ? syscall_trace_enter+0x1c9/0x2c0
 ? __audit_syscall_exit+0x213/0x350
 __x64_sys_sendto+0x24/0x30
bond0: link status down for interface enp61s0f0, disabling it in 200 ms
 do_syscall_64+0x5b/0x1a0
 entry_SYSCALL_64_after_hwframe+0x44/0xa9
RIP: 0033:0x4aeeea
bond0: link status down for interface enp61s0f0, disabling it in 200 ms
Code: e8 5b 9c fb ff 48 8b 7c 24 10 48 8b 74 24 18 48 8b 54 24 20 4c 8b 54 24 28 4c 8b 44 24 30 4c 8b 4c 24 38 48 8b 44 24 08 0f 05 <48> 3d 01 f0 ff ff 76 20 48 c7 44 24 40 ff ff ff ff 48 c7 44 24 4

RSP: 002b:000000c0003d8f98 EFLAGS: 00000216 ORIG_RAX: 000000000000002c

这个是驱动 BUG,升级驱动可解决。


线上升级的时候是新内核和新驱动一起升级,默认情况下需要重启两次新驱动才生效,第一次新内核生效,第二次新驱动才生效,因为新驱动安装的时候最后 postinstall 脚本里的 depmod 执行还是在老内核上,所以第一次重启还是使用的新内核默认自带的老驱动。解决方法是新驱动里添加一个 /etc/depmod.d/ 配置文件。

# cat /etc/depmod.d/kmod-i40e.conf
override i40e * weak-updates/i40e

https://linux.die.net/man/8/depmod


升级驱动版本注意中间有些版本升级后会出现网卡统计的 RX 一直为 0 的问题,这个在下面这个提交中已经修复:

https://github.com/torvalds/linux/commit/cdec2141c24ef177d929765c5a6f95549c266fb3

虽然这个是 2018 年的提交,但 intel 官方在 2021 年之后提供的驱动版本似乎还是不包含这个补丁内容。需要代码确认下。

第二跳直接摘除 VIP + 服务器开启了 ip_forward 造成的包循环

如果第二跳机器上没有绑定 VIP,机器上又开启了 ip_forward,这个时候解包出一个 VIP 的包过来,发现不是本机 IP,会走路由将包转发出去,包又会从负载均衡进来,重新转发,这个过程中包只有 ttl 会减小,会导致包一直在 director -> 第一跳后端 -> 第二跳后端 -> director 这样循环,直到 ttl 为 0 为止。

# tcpdump -i bond0 'dst <VIP>'  -nnn -v -e
19:26:28.377695 24:6e:96:c6:aa:60 > e4:43:4b:0a:fb:60, ethertype IPv4 (0x0800), length 54: (tos 0x0, ttl 49, id 21256, offset 0, flags [DF], proto TCP (6), length 40)
  112.8.88.102.14979 > 140.249.147.253.80: Flags [F.], cksum 0x6105 (correct), seq 0, ack 1, win 333, length 0
...
19:26:28.380421 24:6e:96:c6:aa:60 > e4:43:4b:0a:fb:60, ethertype IPv4 (0x0800), length 54: (tos 0x0, ttl 4, id 21256, offset 0, flags [DF], proto TCP (6), length 40)
  112.8.88.102.14979 > 140.249.147.253.80: Flags [F.], cksum 0x6105 (correct), seq 0, ack 1, win 333, length 0
19:26:28.380493 24:6e:96:c6:aa:60 > e4:43:4b:0a:fb:60, ethertype IPv4 (0x0800), length 54: (tos 0x0, ttl 3, id 21256, offset 0, flags [DF], proto TCP (6), length 40)
  112.8.88.102.14979 > 140.249.147.253.80: Flags [F.], cksum 0x6105 (correct), seq 0, ack 1, win 333, length 0
19:26:28.380540 24:6e:96:c6:aa:60 > e4:43:4b:0a:fb:60, ethertype IPv4 (0x0800), length 54: (tos 0x0, ttl 2, id 21256, offset 0, flags [DF], proto TCP (6), length 40)
  112.8.88.102.14979 > 140.249.147.253.80: Flags [F.], cksum 0x6105 (correct), seq 0, ack 1, win 333, length 0
19:26:28.380595 24:6e:96:c6:aa:60 > e4:43:4b:0a:fb:60, ethertype IPv4 (0x0800), length 54: (tos 0x0, ttl 1, id 21256, offset 0, flags [DF], proto TCP (6), length 40)
  112.8.88.102.14979 > 140.249.147.253.80: Flags [F.], cksum 0x6105 (correct), seq 0, ack 1, win 333, length 0

教训就是:

  • 在对称部署架构下,OSPF VIP 和后端绑定 VIP 需要在逻辑在分开(物理上可能绑定的是一个),有些场景下 OSPF VIP 和后端 VIP 的绑定解绑逻辑不一致,一个需要解,一个不能解,这个需要特殊处理,避免类似上面这样的问题。

  • 没事别开 ip_forward。

IPv4 VIP 访问正常、IPv6 访问异常可能的问题

集群中有一台机器 IPv6 访问异常,IPv4 访问确正常,异常的机器上 IPv6 的 VIP 绑定正常,sit0 网卡也正常,一切看上去都挺正常,这种情况可能是没有初始化 IPv6 导致缺少默认 IPv6 路由,下行的包不知道怎么出去导致的。

交换机和服务器上 MTU 不一致导致的 bird ospf 失败

有些交换机 MTU 设置为 9000 且无法修改,但是挂载了 XDP 的网卡的 MTU 无法改到和交换机的一致( mlx5_core MTU(9000) > 3498 is not allowed while XDP enabled ),此时 bird 会报下面的错误。

# tail /tmp/bird.log
2022-12-03 20:30:51.893 <RMT> ospfv2: MTU mismatch with nbr 61.184.215.126 on bond0 (remote 9000, local 1500)
...

这个是 bird v2.0.7 版本开始加上的一个验证,之前只是一个 warning。

https://github.com/CZ-NIC/bird/commit/267da8138d7f429941f2d829b44cf9bdd94a14d6

可以加个选项支持将这个验证关闭。

diff -ru a/proto/ospf/config.Y b/proto/ospf/config.Y
--- a/proto/ospf/config.Y   2019-10-16 18:45:08.000000000 +0800
+++ b/proto/ospf/config.Y   2022-12-05 16:36:46.334051267 +0800
@@ -191,7 +191,7 @@
 CF_DECLS

 CF_KEYWORDS(OSPF, V2, V3, OSPF_METRIC1, OSPF_METRIC2, OSPF_TAG, OSPF_ROUTER_ID)
-CF_KEYWORDS(AREA, NEIGHBORS, RFC1583COMPAT, STUB, TICK, COST, COST2, RETRANSMIT)
+CF_KEYWORDS(AREA, NEIGHBORS, RFC1583COMPAT, STUB, TICK, COST, COST2, RETRANSMIT, IGNOREMTU)
 CF_KEYWORDS(HELLO, TRANSMIT, PRIORITY, DEAD, TYPE, BROADCAST, BCAST, DEFAULT)
 CF_KEYWORDS(NONBROADCAST, NBMA, POINTOPOINT, PTP, POINTOMULTIPOINT, PTMP)
 CF_KEYWORDS(NONE, SIMPLE, AUTHENTICATION, STRICT, CRYPTOGRAPHIC, TTL, SECURITY)
@@ -258,6 +258,7 @@
    proto_item
  | ospf_channel { this_proto->net_type = $1->net_type; }
  | RFC1583COMPAT bool { OSPF_CFG->rfc1583 = $2; }
+ | IGNOREMTU bool { OSPF_CFG->ignoremtu = $2; }
  | RFC5838 bool { OSPF_CFG->af_ext = $2; if (!ospf_cfg_is_v3()) cf_error("RFC5838 option requires OSPFv3"); }
  | VPN PE bool { OSPF_CFG->vpn_pe = $3; }
  | STUB ROUTER bool { OSPF_CFG->stub_router = $3; }
diff -ru a/proto/ospf/dbdes.c b/proto/ospf/dbdes.c
--- a/proto/ospf/dbdes.c    2019-10-16 18:45:08.000000000 +0800
+++ b/proto/ospf/dbdes.c    2022-12-05 16:54:41.919077802 +0800
@@ -360,9 +360,11 @@
       (rcv_iface_mtu != ifa->iface->mtu) &&
       (rcv_iface_mtu != 0) && (ifa->iface->mtu != 0))
   {
-    LOG_PKT("MTU mismatch with nbr %R on %s (remote %d, local %d)",
-       n->rid, ifa->ifname, rcv_iface_mtu, ifa->iface->mtu);
-    return;
+    LOG_PKT("MTU mismatch with nbr %R on %s (remote %d, local %d) %d",
+       n->rid, ifa->ifname, rcv_iface_mtu, ifa->iface->mtu, p->ignoremtu);
+    if (!p->ignoremtu) {
+       return;
+    }
   }

   switch (n->state)
diff -ru a/proto/ospf/ospf.c b/proto/ospf/ospf.c
--- a/proto/ospf/ospf.c     2019-10-16 18:45:08.000000000 +0800
+++ b/proto/ospf/ospf.c     2022-12-05 16:57:16.415081613 +0800
@@ -287,6 +287,7 @@
   p->af_ext = c->af_ext;
   p->af_mc = c->af_mc;
   p->rfc1583 = c->rfc1583;
+  p->ignoremtu = c->ignoremtu;
   p->stub_router = c->stub_router;
   p->merge_external = c->merge_external;
   p->instance_id = c->instance_id;
@@ -708,6 +709,9 @@
   if (p->rfc1583 != new->rfc1583)
     return 0;

+  if (p->ignoremtu != new->ignoremtu)
+    return 0;
+
   if (p->instance_id != new->instance_id)
     return 0;

@@ -832,6 +836,7 @@

   cli_msg(-1014, "%s:", p->p.name);
   cli_msg(-1014, "RFC1583 compatibility: %s", (p->rfc1583 ? "enabled" : "disabled"));
+  cli_msg(-1014, "Ignore MTU: %s", (p->ignoremtu? "enabled" : "disabled"));
   cli_msg(-1014, "Stub router: %s", (p->stub_router ? "Yes" : "No"));
   cli_msg(-1014, "RT scheduler tick: %d", p->tick);
   cli_msg(-1014, "Number of areas: %u", p->areano);
diff -ru a/proto/ospf/ospf.h b/proto/ospf/ospf.h
--- a/proto/ospf/ospf.h     2019-10-16 18:45:08.000000000 +0800
+++ b/proto/ospf/ospf.h     2022-12-05 15:19:47.161937314 +0800
@@ -94,6 +94,7 @@
   u8 af_ext;
   u8 af_mc;
   u8 rfc1583;
+  u8 ignoremtu;
   u8 stub_router;
   u8 merge_external;
   u8 instance_id;
@@ -232,6 +233,7 @@
   u8 af_ext;                       /* OSPFv3-AF extension */
   u8 af_mc;                        /* OSPFv3-AF multicast */
   u8 rfc1583;                      /* RFC1583 compatibility */
+  u8 ignoremtu;
   u8 stub_router;          /* Do not forward transit traffic */
   u8 merge_external;               /* Should i merge external routes? */
   u8 instance_id;          /* Differentiate between more OSPF instances */

tcpdump 能看到 vlan header,但是 xdp 看不到

Since XDP needs to see the VLAN headers as part of the packet headers, it is important to turn off VLAN hardware offload (which most hardware NICs support), since that will remove the VLAN tag from the packet header and instead communicate it out of band to the kernel via the packet hardware descriptor.

https://github.com/xdp-project/xdp-tutorial/tree/master/packet01-parsing

一般网卡都有 vlan 解析/插入的硬件加速:

# ethtool -k eth0|grep vlan-offload
rx-vlan-offload: on
tx-vlan-offload: on [fixed]

网卡会直接硬件解析 vlan header,然后将 vlan id 等信息直接给驱动,DMA 给驱动的网络包会删除掉 vlan header 变成一个普通的网络包。这样 XDP 就看不到 vlan header 了。解决的方法是关闭这个硬件加速:

# ethtool -K eth0 rxvlan off

或者通过配置将 vlan 信息给到 XDP 程序。

XDP_PASS & sysctl net.ipv4.conf.interface.accept_local

https://sysctl-explorer.net/net/ipv4/accept_local/

sysctl net.ipv4.conf.interface.accept_local 参数默认为 FALSE,此时 XDP_PASS 给本机的 GUE 包的源地址需要改成其他非本机 IP,否则包会被丢掉。或者设置 sysctl net.ipv4.conf.interface.accept_local 参数为 TRUE。

clang loop unroll(full) 失败导致的 BPF 加载失败

下面的代码在编译的时候,循环会展开失败。

#pragma clang loop unroll(full)
for (int i = 1; i < L4LB_GUE_IPS_ARRAY_SIZE; i++) {
    uint32_t *ip = (uint32_t *)bpf_map_lookup_elem(&l4lb_gue_ips, &i);
    ...
}

警告

warning: loop not unrolled: the optimizer was unable to perform the requested transformation; the transformation might be part of an unsupported transformation ordering [-Wpass-failed=transform-warning]

这样编译出的 bpf 会加载失败,报 错误

invalid argument: back-edge from insn 1456 to 1458

搜索 Linux 内核代码可以看到这个错误是说 bpf 代码中有往回的 jump 了,在我们的场景下就是上面的这个循环,bpf 不允许循环。

一个让编译器的优化器可以成功展开这个循环的方法是:

#pragma clang loop unroll(full)
for (int i = 1; i < L4LB_GUE_IPS_ARRAY_SIZE; i++) {
    int j = i;   // 👈
    uint32_t *ip = (uint32_t *)bpf_map_lookup_elem(&l4lb_gue_ips, &j);
    ...
}

邮件列表里也有人遇到过同样问题(https://lists.linuxfoundation.org/pipermail/iovisor-dev/2017-April/000733.html),给出的解释是将 &i 这个循环计数变量的指针传给 bpf_map_lookup_elem 函数,函数中可能会改变指针指向的内容,但是从这个函数的原型 static void *(*bpf_map_lookup_elem)(void *map, const void *key) = (void *) 1 来看,这个参数前面带了 const 关键字,编译器应该可以知道这个变量是一个只读变量的。


int a = 24;
const int *p = &a;

// 报 error: assignment of read-only location ‘*p’
*p = 10;
// 但是做一个强制转换后,像下面这样是可以编译通过的
*(int*)p = 10;

https://stackoverflow.com/questions/34842224/when-to-use-const-void

const 指针指向的内容也是可以改变的,但这种情况编译器应该考虑吗?

can’t get map by id(2637): Device or resource busy

# bpftool map show
2001: hash  flags 0x0
    key 24B  value 48B  max_entries 256  memlock 40960B
2636: percpu_array  name l4lb_director_m  flags 0x0
    key 4B  value 120B  max_entries 1  memlock 8192B
Error: can't get map by id(2637): Device or resource busy

这个可能是用户空间程序打开了太多的 2637 这个 id 的 bpf map fd 导致的。内核代码限制了一个 map 最多可以打开 32768 个 fd。

#define BPF_MAX_REFCNT 32768

SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size)
|- bpf_map_get_fd_by_id()
   |- ...
   |- bpf_map_inc_not_zero()
      |- ...
      |- int refold = atomic_fetch_add_unless(&map->refcnt, 1, 0);
      |- if (refold >= BPF_MAX_REFCNT)
      |-   return ERR_PTR(-EBUSY);
      |- ...

如何获得一个进程打开的 bpf map fd 对应的 map id

# lsof -p 3248691 |grep bpf-map
nginx   3248691 nobody    4u  a_inode               0,13        0      14337 bpf-map
# cat /proc/3248691/fdinfo/4
...
map_id:     16392

BPF_MAP_TYPE_PROG_ARRAY 类型 map 写入成功但是 bpftool dump map 发现没有写入的元素

用户态程序如果关闭 BPF_MAP_TYPE_PROG_ARRAY 的 map,会给 map 中所有的 prog 的引用计数减 1,添加的时候所有的 prog 会引用加 1,如果没有其他操作,这个 prog 会被自动清理掉。

解决方法就是要么用户空间程序不要关闭这个 map,或者关闭前将这个 map 固定到 bpf 文件系统中。

const struct file_operations bpf_map_fops = {
    ...
    .release = bpf_map_release,
    ...
};

bpf_map_release
|- map->ops->map_release()
   |- prog_array_map_clear()
      |- for (i = 0; i < array->map.max_entries; i++)
      |-   __fd_array_map_delete_elem(map, &i, need_defer)
           |- map->ops->map_fd_put_ptr(map, old_ptr, need_defer)

bpf 验证器报 math between pkt pointer and register with unbounded min value is not allowed

有时候校验器报错不一定是程序代码有问题,尤其是在低版本的内核上,也可能是 bpf 校验器本身有问题。比如如下代码:

if (ipv4->ihl <= 5) {
    return true;
}
return buf_skip(buf, (ipv4->ihl - 5) * 4);

这段代码有些时候编译的时候没问题,有些时候(其他代码变了)编译之后在高版本内核上没问题,在低版本比如 4.19 内核上就报 math between pkt pointer and register with unbounded min value is not allowed 错误。

反汇编 bpf 程序,根据报错的信息可以定位到代码行数,可以发现上面的代码编译出来的 bpf 指令码有问题和没问题的时候不一样。

首先没问题的:

; if (ipv4->ihl < 5) {
    187:       r4 = *(u8 *)(r8 + 0)
    188:       r4 &= 15
    189:       r5 = 5
    190:       if r5 > r4 goto +157 <LBB0_154>
; if (ipv4->ihl <= 5) {
    191:       if r4 == 5 goto +4 <LBB0_28>
; return buf_skip(buf, (ipv4->ihl - 5) * 4);
    192:       r4 <<= 2
; if (buf->head + len > buf->tail) {
    193:       r8 += r4

有问题的:

; if (ipv4->ihl < 5) {
    221:       r3 = *(u8 *)(r4 + 0)
    222:       r3 &= 15
    223:       r5 = 5
    224:       *(u64 *)(r10 - 104) = r3
    225:       if r5 > r3 goto +178 <LBB0_165>
; if (ipv4->ihl <= 5) {
    226:       r5 = *(u64 *)(r10 - 104)
    227:       if r5 == 5 goto +5 <LBB0_29>
    228:       r3 = *(u64 *)(r10 - 104)
; return buf_skip(buf, (ipv4->ihl - 5) * 4);
    229:       r3 <<= 2
; if (buf->head + len > buf->tail) {
    230:       r4 += r3 👈 报错行

对比 bpf 指令码可以看出,没问题的版本中 ipv4->ihl 这个变量始终是保存在 r4 这个寄存器中,所以在调用 buf_skip 函数的时候,校验器可以很容易跟踪得到 ipv4->ihl - 5 肯定是大于 0 的,但是对于有问题的版本中,生成的 bpf 代码是从栈上加载的变量值,低版本的时候,校验器还不能跟踪栈上的变量,所以这里跟踪失败,导致校验器无法确定值的范围,报这个错误。

解决的一个方法,就是用 asm 强制将上面 226-228 行这里的 r5r3 寄存器强制为一个,这样就能让校验器跟踪到变量的范围从而避免这个错误。

uint8_t a;
asm volatile ("%0 = %1" : "=r"(a) : "r"(ipv4->ihl)); /* a = ipv4->ihl */
if (a <= 5) {
    return true;
}
return buf_skip(buf, (a - 5) * 4);

另外高内核(5.3 之后)应该就没有这个问题了。

转发表分配均匀但是有后端机器的流量很小

程序、转发表所有一切都正常,但是就是有后端机器的流量很小,采样统计也是如此,这个时候可以进一步采样转发的 GUE 流量并解包,看看转发的流量是否有特征。

线上曾经出现过的一个问题就是升级的时候由于代码 BUG,导致某些机器上 fou 模块没有成功加载,调度到这些后端的流量无法被处理,syn 包会一直重试,就会出现上面说的这种现象。

systemd 使用 root 用户启动服务报 bpf map create failed: permission denied

但是手工启动没有问题,这个可能是 selinux 的问题。

# getenforce
Enforcing

检查 selinux 是不是在 Enforcing 状态,是的话检查 /var/log/audit/audit.log 看是不是被 selinux 拦了。简单的处理方式是关了 selinux setenforce 0

https://superuser.com/questions/1125250/systemctl-access-denied-when-root

用户空间 bpf_map_lookup_elem 返回 ENOENT

这个不是错误,是 key 不存在。

https://elixir.bootlin.com/linux/v4.19/source/kernel/bpf/syscall.c#L718

static int map_lookup_elem(union bpf_attr *attr)
{
    ..
    rcu_read_lock();
    ptr = map->ops->map_lookup_elem(map, key);
    if (ptr)
        memcpy(value, ptr, value_size);
    rcu_read_unlock();
    err = ptr ? 0 : -ENOENT;
    ...
}

跨机房调度 ttl 太小导致转发失败

如题。

virtio-net 驱动加载 xdp driver 模式失败

加载 xdp 程序报错 load xdp root failed: cannot allocate memory

dmesg 内核错误信息:

[  717.248974] virtio_net virtio0 eth0: request 4 queues but max is 2

报错的点:

curr_qp = vi->curr_queue_pairs - vi->xdp_queue_pairs;
if (prog)
    xdp_qp = nr_cpu_ids;

/* XDP requires extra queues for XDP_TX */
if (curr_qp + xdp_qp > vi->max_queue_pairs) {
    netdev_warn(dev, "request %i queues but max is %i\n",
        curr_qp + xdp_qp, vi->max_queue_pairs);
    return -ENOMEM;
}
# ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
RX:         0
TX:         0
Other:              0
Combined:   2
Current hardware settings:
RX:         0
TX:         0
Other:              0
Combined:   2

# nproc
2

xdp 默认要申请独立的 queue,但网卡最大就 2 个 queue,已经申请了 2 个用作 rx+tx(combined 就是共用)队列,不支持再额外申请 xdp tx queue 了。

怎么解决?

简单的方式共用现有的 queue,参见提交:https://github.com/torvalds/linux/commit/97c2c69e1926260c78c7f1c0b2c987934f1dc7a1。要做的就是升级驱动即可。

或者可以改硬件的 max(# Enable multiqueue virtio-net)? https://en.opensuse.org/Testing_XDP_with_virtio-net

bpf 验证器报 invalid indirect read from stack

struct ethhdr eth;
// memset(&eth, 0, sizeof(eth));
eth.h_proto = bpf_ntohs(ETH_P_IPV6);
if (bpf_skb_store_bytes(skb, 0, &eth, sizeof(eth), 0)) {  // 👈 这里报错
        return -1;
}

未初始化栈内元素,就传递该栈地址。把上面 memset 前面的注释去掉就没问题了。