LXC 销毁后,hyper 内核报 unregister_netdevice 网卡错误,困扰了很久。根因是网卡 down 后 IPv6 协议栈仍持有 netdevice 引用,在 hyper 中加一个 disable_ipv6=1 的操作就好了。

现象

LXC 销毁后,hyper 内核报网卡错误 unregister_netdevice,这个问题困扰了很久没有解决。

按道理,应该是清理网卡之前,清理相关资源。但我们的程序是直接 down 网卡。

后来有人在网卡 down 后加了一个清理 ipv6 neigh 的操作,然后就好了。卧槽。

排查

先看接口没 down 的时候,比如 eth0 上有 neigh:

root@host:~# ip n s
10.0.0.252 dev eth1 lladdr 36:95:2f:f5:af:85 STALE
10.0.0.1 dev eth0 lladdr 02:54:d7:af:4b:cb STALE
10.0.0.253 dev eth1 lladdr 52:54:d7:c6:e8:47 STALE
fe80::5054:d7ff:fec6:e847 dev eth1 lladdr 52:54:d7:c6:e8:47 router STALE
fd00:2024:822:0:167:ffff:ffff:fffd dev eth1 lladdr 52:54:d7:c6:e8:47 router STALE
fe80::e01e:53ff:fe85:5ca0 dev eth0 lladdr 02:54:d7:af:4b:cb router STALE

把接口 eth0 down 掉:

root@host:~# ip link set dev eth0 down ; ip n s
10.0.0.252 dev eth1 lladdr 36:95:2f:f5:af:85 STALE
10.0.0.253 dev eth1 lladdr 52:54:d7:c6:e8:47 STALE
fe80::5054:d7ff:fec6:e847 dev eth1 lladdr 52:54:d7:c6:e8:47 router STALE
fd00:2024:822:0:167:ffff:ffff:fffd dev eth1 lladdr 52:54:d7:c6:e8:47 router STALE

已经看不到 eth0 的 neigh 了。

尝试的修复

一开始加的是清理 neigh 的操作:

def clear_ip_neigh(instance_id):
    cmd = "ip neigh show | grep -E '%s'" % '|'.join(hwaddrs)
    ret = exec_cmd(cmd)
    ip_neighs = []
    ...
    # 网卡 down 掉之后应该就执行不到下面,但是问题就是好了,这也太牛逼了。
    for ip_neigh in ip_neighs:
        exec_cmd("ip neigh del %s" % ip_neigh)

神奇呀。但最后发现,生效的可能不是上面这段,而是下面这段:

def let_iface_degraded(instance_id):
    """ let interfaces degraded in the local host. """
    ifaces_in_host = get_interfaces_in_host(instance_id)

    # 1. set dev down
    for iface in ifaces_in_host:
        exec_cmd("ip link set dev %s down;" % iface)

    # 2. disable ipv6
    for iface in ifaces_in_host:
        exec_cmd("sysctl -w net.ipv6.conf.%s.disable_ipv6=1;" % iface)

    return

重点是:加了一个 disable_ipv6 的操作,是在 hyper 中执行的。

disable_ipv6=1 才是针对性的关键修复。

根因

问题出在 unregister_netdevice 的竞态窗口:

LXC stop 时,容器网络命名空间(netns)在销毁前,veth pair 的宿主侧接口上 IPv6 协议栈仍在运行,可能还在:

  • 响应 Router Solicitation / Router Advertisement 消息
  • 进行 Duplicate Address Detection (DAD) 检测
  • 持有未清理的 IPv6 neighbor entries

此时如果内核正在执行 unregister_netdevice,而 IPv6 stack 仍然持有对该 netdevice 的引用,就会触发类似:

unregister_netdevice: waiting for eth0 to become free. Usage count = 1

小结

  • 清理网卡前,要先清理相关资源(neigh、IPv6 协议栈等);
  • unregister_netdevice 的竞态窗口是根因,IPv6 协议栈持有 netdevice 引用导致无法释放;
  • 关键修复是 disable_ipv6=1,在 hyper 中执行;
  • 底层操作往往"不讲道理",多尝试认知之外的事情,往往有意外收获。