再谈网卡的一致性命名

针对网卡名字这个问题,其实之前也讨论过一次,主要是如何利用udev去重命名网卡,里面提到了新的一致性命名规则,但是没有细说。

当然肯定是遇到问题了,所以针对网卡命名的细节,需要再探讨一下。

其实目前大家还是更熟悉老的那种eth0,eth1…那种命名,目前我们大部分生产环境里也是这么用的。但是随着网卡数量越来越多(在我们使用的SR-IOV场景,加上VF虚拟网卡,机器上已经有超过16个网卡)这种命名规则已经不适应现代的硬件和操作系统了,所以最近我们也从老的命名方式,切换到操作系统默认的一致网络设备命名

不过因此也带来了一些问题,发现不同厂商,或者不同机器会有命名不一致的情况,举个例子:

这是某台机器的网卡名字:

[root@ ~]# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp59s0f0: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master bond0 state UP group default qlen 1000
    link/ether 0c:42:a1:b3:12:b8 brd ff:ff:ff:ff:ff:ff
3: enp59s0f1: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master bond0 state UP group default qlen 1000
    link/ether 0c:42:a1:b3:12:b8 brd ff:ff:ff:ff:ff:ff

一块网卡的两个网口,被分别命名为enp59s0f0enp59s0f1,这个还是挺容易理解的,通过网卡的PCIE地址进行命名,我们看下这块网卡的PCIE信息:

[root@ ~]# lspci |grep Mellanox
3b:00.0 Ethernet controller: Mellanox Technologies MT27800 Family [ConnectX-5]
3b:00.1 Ethernet controller: Mellanox Technologies MT27800 Family [ConnectX-5]

针对lspci的输出,之前的文章lspci命令输出的一些解释也讨论过了:PCIE地址中总线编号3b对应到十进制就是59,后面的00.0和00.1分别对应设备编号0的第0和第1个Function,对应到网卡的名字,就是插在59总线的第0个插槽的第0个网口和第1个网口,因为网卡的插槽是不会变的,而且是个物理状态,自然网卡的名字就稳定了。

那么,按正常的理解,不同厂商之间,应该只是插槽不一样吧?然而却被事实打脸,换了一台机器,又是另外一个样子了:

[root@bx-10-13-207-38.chinaunicom-north-1.node.kubernetes ~]# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: ens2f0: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master bond0 state UP group default qlen 1000
    link/ether 04:3f:72:ac:f6:0e brd ff:ff:ff:ff:ff:ff
3: ens2f1: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master bond0 state UP group default qlen 1000
    link/ether 04:3f:72:ac:f6:0e brd ff:ff:ff:ff:ff:ff

如果还像上面那样理解这个名称的话,那应该网卡插在00总线的第2个插槽?因为总线是0所以省略了?然而继续被打脸:

[root@ ~]# lspci |grep Mellanox
86:00.0 Ethernet controller: Mellanox Technologies MT27800 Family [ConnectX-5]
86:00.1 Ethernet controller: Mellanox Technologies MT27800 Family [ConnectX-5]

发现网卡插在0x86这个总线上,那换算成十进制应该是134啊?网卡名字不应该是enp134s0f0enp134s0f1么,为啥变成ens2f0ens2f1了?

于是乎,又翻开红帽的文档,再仔细阅读一下:

设备命名过程如下:
1. /usr/lib/udev/rules.d/60-net.rules文件中的规则会让 udev 帮助工具/lib/udev/rename_device查看所有/etc/sysconfig/network-scripts/ifcfg-suffix文件。如果发现包含HWADDR条目的ifcfg文件与某个接口的MAC地址匹配,它会将该接口重命名为ifcfg文件中由DEVICE指令给出的名称。
2. /usr/lib/udev/rules.d/71-biosdevname.rules中的规则让biosdevname根据其命名策略重命名该接口,即在上一步中没有重命名该接口、已安装biosdevname、且未在boot命令行中将biosdevname=0作为内核命令给出。
3. /lib/udev/rules.d/75-net-description.rules中的规则让udev通过检查网络接口设备,填写内部udev设备属性值ID_NET_NAME_ONBOARD、ID_NET_NAME_SLOT、ID_NET_NAME_PATH。注:有些设备属性可能处于未定义状态。
4. /usr/lib/udev/rules.d/80-net-name-slot.rules中的规则让udev重命名该接口,优先顺序如下:ID_NET_NAME_ONBOARD、ID_NET_NAME_SLOT、ID_NET_NAME_PATH。并提供如下信息:没有在步骤 1 或 2 中重命名该接口,同时未给出内核参数net.ifnames=0。如果一个参数未设定,则会按列表的顺序设定下一个。如果没有设定任何参数,则不会重命名该接口。

再理理这个命名过程:

第1步先看看/etc/sysconfig/network-scripts/文件夹下有没有MAC地址匹配的配置文件,如果有,就按配置文件里的命名来。这里我们的系统不会命中。
第2步看是否使用biosdevname进行命名,这个是Dell的机器专属,不过我们统一给内核加上了biosdevname=0参数来统一Dell和其他厂商,所以这条应该也不会命中。
第3步是通过udev规则填一些env,暂时不涉及命名过程。
第4步是根据env进行重命名,并且有个优先顺序:ID_NET_NAME_ONBOARD、ID_NET_NAME_SLOT、ID_NET_NAME_PATH。

所以仔细点会发现,enp59s0f0这种命名规则是命中了ID_NET_NAME_PATH,是以PCI物理位置为规则命名;而ens2f0这种则是命中了ID_NET_NAME_SLOT,是以热插拔插槽索引号命名。

但是为什么会有这样的差异,依然不明朗,继续追踪,使用udevadm info命令看看能不能找到两者不同:

ID_NET_NAME_PATH命名的机器:

[root@ ~]# udevadm info /sys/class/net/enp59s0f0
P: /devices/pci0000:3a/0000:3a:00.0/0000:3b:00.0/net/enp59s0f0
E: DEVPATH=/devices/pci0000:3a/0000:3a:00.0/0000:3b:00.0/net/enp59s0f0
E: ID_BUS=pci
E: ID_MODEL_FROM_DATABASE=MT27800 Family [ConnectX-5] (ConnectX®-5 EN network interface card, 10/25GbE dual-port SFP28, PCIe3.0 x8, tall bracket ; MCX512A-ACAT)
E: ID_MODEL_ID=0x1017
E: ID_NET_DRIVER=mlx5_core
E: ID_NET_NAME_MAC=enxb8599fbaa608
E: ID_NET_NAME_PATH=enp59s0f0
E: ID_OUI_FROM_DATABASE=Mellanox Technologies, Inc.
E: ID_PATH=pci-0000:3b:00.0
E: ID_PATH_TAG=pci-0000_3b_00_0
E: ID_PCI_CLASS_FROM_DATABASE=Network controller
E: ID_PCI_SUBCLASS_FROM_DATABASE=Ethernet controller
E: ID_VENDOR_FROM_DATABASE=Mellanox Technologies
E: ID_VENDOR_ID=0x15b3
E: IFINDEX=6
E: INTERFACE=enp59s0f0
E: MAJOR=0
E: MINOR=0
E: SUBSYSTEM=net
E: SYSTEMD_ALIAS=/sys/subsystem/net/devices/enp59s0f0 /sys/subsystem/net/devices/enp59s0f0
E: TAGS=:systemd:
E: UDEV_BIOSDEVNAME=1
E: USEC_INITIALIZED=54242
E: biosdevname=0
E: net.ifnames=1

ID_NET_NAME_SLOT命名的机器:

[root@ ~]# udevadm info /sys/class/net/ens2f0
P: /devices/pci0000:85/0000:85:00.0/0000:86:00.0/net/ens2f0
E: DEVPATH=/devices/pci0000:85/0000:85:00.0/0000:86:00.0/net/ens2f0
E: ID_BUS=pci
E: ID_MODEL_FROM_DATABASE=MT27800 Family [ConnectX-5] (ConnectX®-5 EN network interface card, 10/25GbE dual-port SFP28, PCIe3.0 x8, tall bracket ; MCX512A-ACAT)
E: ID_MODEL_ID=0x1017
E: ID_NET_DRIVER=mlx5_core
E: ID_NET_NAME_MAC=enx043f72acf60e
E: ID_NET_NAME_PATH=enp134s0f0
E: ID_NET_NAME_SLOT=ens2f0
E: ID_PATH=pci-0000:86:00.0
E: ID_PATH_TAG=pci-0000_86_00_0
E: ID_PCI_CLASS_FROM_DATABASE=Network controller
E: ID_PCI_SUBCLASS_FROM_DATABASE=Ethernet controller
E: ID_VENDOR_FROM_DATABASE=Mellanox Technologies
E: ID_VENDOR_ID=0x15b3
E: IFINDEX=2
E: INTERFACE=ens2f0
E: SUBSYSTEM=net
E: SYSTEMD_ALIAS=/sys/subsystem/net/devices/ens2f0
E: TAGS=:systemd:
E: UDEV_BIOSDEVNAME=0
E: USEC_INITIALIZED=34864
E: biosdevname=0
E: net.ifnames=1

大部分都是一样的,但是以ID_NET_NAME_SLOT命名的机器,ID_NET_NAME_SLOT这个ENV不为空,而以ID_NET_NAME_PATH命名的机器直接就没有ID_NET_NAME_SLOT这个ENV。那么按照/usr/lib/udev/rules.d/80-net-name-slot.rules里的udev规则:

[root@ ~]# cat /usr/lib/udev/rules.d/80-net-name-slot.rules
# do not edit this file, it will be overwritten on update

ACTION!="add", GOTO="net_name_slot_end"         # 如果不是设备添加操作,直接跳过
SUBSYSTEM!="net", GOTO="net_name_slot_end"      # 如果不是一个网络设备,直接跳过
NAME!="", GOTO="net_name_slot_end"              # 如果已经有名字了,直接跳过

IMPORT{cmdline}="net.ifnames"
ENV{net.ifnames}=="0", GOTO="net_name_slot_end" # 如果内核参数里有net.ifnames=0,直接跳过

NAME=="", ENV{ID_NET_NAME_ONBOARD}!="", NAME="$env{ID_NET_NAME_ONBOARD}" # 没有名字,ID_NET_NAME_ONBOARD这个ENV存在,用ID_NET_NAME_ONBOARD作为名字
NAME=="", ENV{ID_NET_NAME_SLOT}!="", NAME="$env{ID_NET_NAME_SLOT}"       # 没有名字,ID_NET_NAME_SLOT这个ENV存在,用ID_NET_NAME_SLOT作为名字
NAME=="", ENV{ID_NET_NAME_PATH}!="", NAME="$env{ID_NET_NAME_PATH}"       # 没有名字,ID_NET_NAME_PATH这个ENV存在,用ID_NET_NAME_PATH作为名字

LABEL="net_name_slot_end"

和文档说的一样,如果ENV{ID_NET_NAME_SLOT}有值的话,那就直接用它了,就不会再用ENV{ID_NET_NAME_PATH}了。

那么问题来了,这些ENV又是咋来的?文档里说是在/lib/udev/rules.d/75-net-description.rules中设置的。那就看看这个文件:

[root@ ~]# cat /usr/lib/udev/rules.d/75-net-description.rules
# do not edit this file, it will be overwritten on update

ACTION=="remove", GOTO="net_end"
SUBSYSTEM!="net", GOTO="net_end"

IMPORT{builtin}="net_id"            # 这些ENV应该是在这里设置的,因为没有其他显式赋值的地方了。

SUBSYSTEMS=="usb", IMPORT{builtin}="usb_id", IMPORT{builtin}="hwdb --subsystem=usb"
SUBSYSTEMS=="usb", GOTO="net_end"

SUBSYSTEMS=="pci", ENV{ID_BUS}="pci", ENV{ID_VENDOR_ID}="$attr{vendor}", ENV{ID_MODEL_ID}="$attr{device}"
SUBSYSTEMS=="pci", IMPORT{builtin}="hwdb --subsystem=pci"

LABEL="net_end"

这里的IMPORT{builtin}="net_id"是udev内部的实现。我们需要找到实现对应的代码了。

找了一下,代码在udev-builtin.c

static const UdevBuiltin *const builtins[_UDEV_BUILTIN_MAX] = {
#if HAVE_BLKID
        [UDEV_BUILTIN_BLKID] = &udev_builtin_blkid,
#endif
        [UDEV_BUILTIN_BTRFS] = &udev_builtin_btrfs,
        [UDEV_BUILTIN_HWDB] = &udev_builtin_hwdb,
        [UDEV_BUILTIN_INPUT_ID] = &udev_builtin_input_id,
        [UDEV_BUILTIN_KEYBOARD] = &udev_builtin_keyboard,
#if HAVE_KMOD
        [UDEV_BUILTIN_KMOD] = &udev_builtin_kmod,
#endif
        [UDEV_BUILTIN_NET_ID] = &udev_builtin_net_id,       // 就是这里
        [UDEV_BUILTIN_NET_LINK] = &udev_builtin_net_setup_link,
        [UDEV_BUILTIN_PATH_ID] = &udev_builtin_path_id,
        [UDEV_BUILTIN_USB_ID] = &udev_builtin_usb_id,
#if HAVE_ACL
        [UDEV_BUILTIN_UACCESS] = &udev_builtin_uaccess,
#endif
};

udev_builtin_net_id的实现在udev-builtin-net_id.c,这里不仔细分析了,就看看ID_NET_NAME_SLOT这个ENV:

static int dev_pci_slot(sd_device *dev, struct netnames *names) {
// ...
        r = sd_device_get_sysname(names->pcidev, &sysname);
        if (r < 0)
                return r;


        r = sd_device_get_syspath(pci, &syspath);
        if (r < 0)
                return r;
        if (!snprintf_ok(slots, sizeof slots, "%s/slots", syspath))
                return -ENAMETOOLONG;

        // 这里的slots对应'/sys/bus/pci/slots/'这个目录
        dir = opendir(slots);
        if (!dir)
                return -errno;

        hotplug_slot_dev = names->pcidev;
        while (hotplug_slot_dev) {
                if (sd_device_get_sysname(hotplug_slot_dev, &sysname) < 0)
                        continue;
                // 遍历'/sys/bus/pci/slots/'目录
                FOREACH_DIRENT_ALL(dent, dir, break) {
                        unsigned i;
                        char str[PATH_MAX];
                        _cleanup_free_ char *address = NULL;

                        if (dot_or_dot_dot(dent->d_name))
                                continue;

                        r = safe_atou_full(dent->d_name, 10, &i);
                        if (r < 0 || i <= 0)
                                continue;

                        /* match slot address with device by stripping the function */
                        // 如果有任何目录中'/sys/bus/pci/slots/{xx}/address'文件里的值和网卡的设备号对应的话,
                        // 说明这块网卡是插在可热拔插插槽的,插槽号是{xx}
                        if (snprintf_ok(str, sizeof str, "%s/%s/address", slots, dent->d_name) &&
                            read_one_line_file(str, &address) >= 0 &&
                            startswith(sysname, address)) {
                                hotplug_slot = i;
                                break;
                        }
                }
                if (hotplug_slot > 0)
                        break;
                // 如果没有,则继续查询网卡的父设备
                // 需要注意的是,新老版本的udev在这里的行为有些区别,高版本里会继续看网卡的父设备,
                // 但是在CentOS 7使用的v219版本里,并不会去查找网卡的父设备,
                // 所以会发现网卡名称在CentOS 8和CentOS 7之间也有可能会有一定的区别。
                if (sd_device_get_parent_with_subsystem_devtype(hotplug_slot_dev, "pci", NULL, &hotplug_slot_dev) < 0)
                        break;
                rewinddir(dir);
        }

        if (hotplug_slot > 0) {
                s = names->pci_slot;
                l = sizeof(names->pci_slot);
                if (domain > 0)
                        l = strpcpyf(&s, l, "P%d", domain);
                l = strpcpyf(&s, l, "s%d", hotplug_slot);   // 如果是热拔插网卡,那名字就是s+插槽号
                if (func > 0 || is_pci_multifunction(names->pcidev))
                        l = strpcpyf(&s, l, "f%d", func);
                if (port_name)
                        l = strpcpyf(&s, l, "n%s", port_name);
                else if (dev_port > 0)
                        l = strpcpyf(&s, l, "d%lu", dev_port);
                if (l == 0)
                        names->pci_slot[0] = '\0';
        }

        return 0;
}

噢,发现区别了,ens2f0的网卡是插在热拔插2插槽的。再确认一下,看看机器上是不是有对应文件:

[root@ ~]# cat /sys/bus/pci/slots/2/address
0000:86:00

发现地址确实是吻合的,而且名字和插槽号确实能对应上。

好吧,到这里问题算是解决了一半。至少知道了为啥网卡名字会不同了,但是也没办法去控制不同厂商去规定网卡地址必须一样了。所以呢,业务层还是需要针对这种场景去做些适配。或者,我们再自定义一个udev文件,另起门户,单独做一个我们自己的命名规则,将所有厂商给统一起来了。