一台机器上装着 Ubuntu Server,你改完/etc/netplan/01-netcfg.yaml敲下netplan apply,ip a一看地址纹丝不动;换一台机器,同事用nmcli配好的静态路由重启后凭空消失;再换一台容器宿主机,/etc/systemd/network/里躺着七八个.network文件,却没人说得清哪个在生效。这几个场景我前后踩过不止一次,最后发现根子都指向同一个问题:没搞明白这台机器上到底是谁在管网络。netplan、NetworkManager、systemd-networkd 这三个名字经常被并列提起,但它们的角色完全不在一个层面上——有人把它们当成三选一的竞品,有人以为装了 netplan 就可以不管后端,结果配置写了三份互相打架。这篇就把三者的定位、职责边界、配置落地方式和共存时的排查链路一次讲透,不管你是刚接手 Linux 服务器的新手,还是被网络配置折腾过的老运维,都能从中找到能直接抄的配置和能直接用的排查命令。
1. 三个工具挤在一台机器上,根源是网络配置的两次分层
要理解这三者的关系,得先回到 Linux 网络配置的演进路径上。早期大家用ifconfig、route、/etc/network/interfaces这一套,配置直接写死在发行版自己的脚本里,改个 IP 要重启整个网络服务,桥接、VLAN 这些稍微复杂点的结构就得手写一堆pre-up、post-up钩子。systemd 出现之后,systemd-networkd 作为配套组件被引入,用声明式的.network、.netdev、.link文件来描述网络,配置和运行时状态分离,这套东西天然适合服务器和容器场景。与此同时,桌面环境需要一套能感知硬件插拔、能管理无线漫游、能被图形界面调用的服务,NetworkManager 就承担了这个角色。而 Ubuntu 在 17.10 之后推 netplan,目标很明确:让用户只写一份 YAML,由 netplan 负责翻译成后端能听懂的配置。
这里最关键的一层认知是:netplan 不是一个网络管理器,它是一个配置翻译层。它自己不碰网卡、不管 DHCP 租约、不处理路由表,只做三件事——读 YAML、校验语法、根据renderer字段把配置写到对应后端的目录里,然后再去调用后端重载。真正把 IP 地址配置到内核上的,永远是 NetworkManager 或者 systemd-networkd。所以"netplan 配置不生效"这个说法本身就不准确,准确的说法是"netplan 生成的后端配置没被后端正确加载"。
看一下 netplan 生成配置的落点就很清楚了。当renderer: networkd时,netplan 会把内容写到/run/systemd/network/目录下,文件名形如10-netplan-enp3s0.network;当renderer: NetworkManager时,写到/run/NetworkManager/system-connections/下,生成的是 keyfile 格式的连接配置。这两个目录都是 tmpfs 上的运行时目录,重启就没了,每次开机由 netplan 重新生成。你在这些目录里看到的文件,就是判断"谁在管网络"的第一手证据。
提示:
/run下的配置是 netplan 生成的产物,不要手动去改,改了也会被下一次netplan apply覆盖。要改就改/etc/netplan/下的源文件。
理解了这个分层,后面很多困惑就自动解开了。为什么装了 netplan 还要装 NetworkManager?因为 Ubuntu 桌面版需要图形化的网络管理能力,netplan 把配置翻译给 NetworkManager 执行。为什么服务器上 netplan 默认走 networkd?因为服务器通常网络结构静态、不需要漫游,networkd 更轻、启动更早、依赖更少。为什么有时候三个都在跑还互相抢网卡?因为它们默认都会去接管没有被显式标记为 unmanaged 的设备。这一节的结论就一句话:netplan 管配置来源,NetworkManager 和 systemd-networkd 管配置执行,三者是上下游关系,不是并列关系。
2. netplan 的 YAML 到底翻译了什么
netplan 的核心价值在于把"配置意图"和"配置实现"解耦,所以它的 YAML 结构设计得比较贴近人的思维方式。顶层永远是network:,下面跟version: 2,再往下按设备类型分组:ethernets、wifis、bridges、bonds、vlans、tunnels。每台设备下面写自己想要的属性——是要 DHCP 还是静态地址、网关是谁、DNS 用哪些、MTU 多少、要不要开 IPv6。写法上很像在填一张表单,而不是在写脚本。
一段典型的服务器静态配置长这样:
network: version: 2 renderer: networkd ethernets: enp3s0: dhcp4: false addresses: - 192.168.10.20/24 routes: - to: default via: 192.168.10.1 nameservers: addresses: [223.5.5.5, 119.29.29.29] search: [internal.example.com] mtu: 1500这里有几个细节值得单独拎出来说。renderer如果不写,Ubuntu Server 默认走 networkd,桌面版默认走 NetworkManager,这个默认值藏在/usr/lib/netplan/下的配置里,不是写在你的 YAML 里。gateway4这个字段在 netplan 0.103 之后就废弃了,取而代之的是routes列表里写to: default,如果你还按老教程写gateway4,新版本会直接报错退出,报错信息还比较含糊,容易让人以为是缩进问题。nameservers.search对纯静态环境影响不大,但一旦涉及内部域名解析,漏掉这一项会让ping内网主机名一直失败,排查起来很费时间。
写完配置之后的三个命令必须分清楚用途,很多人混用导致问题被掩盖:
| 命令 | 实际动作 | 适用场景 |
|---|---|---|
netplan generate | 只做语法校验和配置生成,不应用 | 改完配置先验证语法,避免直接 apply 断网 |
netplan try | 应用配置并监听 120 秒,无确认则回滚 | 远程 SSH 改网络配置的保命操作 |
netplan apply | 直接应用,不提供回滚 | 本地操作或配置已经验证无误 |
远程改网络配置,我强烈建议一律先跑netplan try。它会先把配置应用上去,然后给你 120 秒的窗口,你需要在另一个终端里验证ip a、ping通不通,确认没问题后敲回车让它固化;如果 120 秒内没确认(比如你把自己 SSH 断了),它会自动恢复到改动前的配置。这个机制救过我不止一次,尤其是改网关或者改网卡名的时候,写错一个字符就是远程失联。相比之下netplan apply没有任何保护,配置写错了就是直接断线,只能走带外管理或者物理接触机器。
还有一个容易被忽略的点:/etc/netplan/下如果有多个.yaml文件,netplan 会按文件名字典序依次读取并合并,后面的覆盖前面的。Ubuntu 安装器默认生成的通常叫00-installer-config.yaml或50-cloud-init.yaml,你自己加的文件如果命名成99-myconfig.yaml,就能保证优先级最高。但反过来,如果两个文件里定义了同一张网卡的不同属性,合并结果可能和你想的不一样,排查时先ls /etc/netplan/确认有没有多余的文件。
YAML 本身的坑也值得提一句。netplan 用的是标准 YAML 解析器,第一,绝对不能用 Tab 缩进,必须用空格,报错信息通常是Found character '\t' that cannot start any token;第二,冒号后面必须有空格,dhcp4:false和dhcp4: false是两个完全不同的东西,前者会被解析成一个整体的字符串键;第三,列表项前的-后面也要有空格。这些错误在netplan generate阶段就会暴露,所以养成"改完先 generate 再 try"的习惯,能省掉大量无谓的远程失联。
3. NetworkManager:被低估的服务器网络管理方案
NetworkManager 在很多人印象里是"桌面版那个带图标的服务",所以在服务器上往往被直接禁掉。这个印象有历史原因,早年的 NetworkManager 确实存在启动慢、和静态配置配合不好的问题,但现在的版本早就不是这样了。它的核心能力其实是三块:设备状态感知、多连接自动切换、统一的 D-Bus 接口。设备插拔能被实时感知,一根网线插上来自动匹配最合适的连接配置,笔记本在无线和有线之间切换时自动选择优先级最高的那个,这些都是 networkd 和 netplan 不具备的。
NetworkManager 里有个容易混淆的概念叫"连接"(connection)。一个连接就是一份配置档,包含 IP 方式、地址、网关、DNS 等属性,它和设备(device)是多对一的关系——同一张网卡上可以存在多个连接配置,但同一时刻只有一个处于激活状态。理解了这个模型,很多命令行为就说得通了。nmcli device status看的是物理设备的当前状态,nmcli connection show看的是配置档列表,nmcli connection up <名字>是把某个配置档激活到对应设备上。
服务器上用 nmcli 配静态地址的完整流程是这样:
# 先看清楚设备和连接的全貌 nmcli device status nmcli connection show # 修改已有连接,注意这一步只是改了配置档,还没生效 nmcli connection modify "Wired connection 1" \ ipv4.method manual \ ipv4.addresses 192.168.10.30/24 \ ipv4.gateway 192.168.10.1 \ ipv4.dns "223.5.5.5 119.29.29.29" # 激活才真正下发到内核 nmcli connection up "Wired connection 1" # 验证 nmcli device show enp3s0这里有个新人最容易踩的坑:nmcli connection modify之后不执行up,配置是不会生效的。你cat一下/etc/NetworkManager/system-connections/下的 keyfile,会发现修改已经写进去了,但ip a显示的地址还是老的,于是以为是 nmcli 不好用,其实只是少了最后一步。另一个坑是连接名带空格,命令行里必须加引号,否则会被拆成多个参数。连接名本身也可以改,用nmcli connection modify <旧名>配合在 keyfile 里改id,但更稳妥的做法是一开始就用nmcli connection add创建时就指定一个不含空格的名字。
NetworkManager 的配置文件默认放在/etc/NetworkManager/system-connections/下,格式是 keyfile(ini 风格)。这个目录下的文件权限必须是600,权限不对 NetworkManager 会直接忽略这个连接,日志里只给一句含糊的警告。远程手工写过 keyfile 的人应该有体会,复制粘贴之后忘了chmod 600,重启后连接消失,找半天原因。
还有一点是关于 DNS 的。NetworkManager 默认会和 systemd-resolved 配合,把 DNS 配置下发到/run/systemd/resolve/下,/etc/resolv.conf通常是一个指向它的软链接。如果你手动改过/etc/resolv.conf并把它变成了普通文件,那 NetworkManager 管理的 DNS 就永远生效不了,表现是nmcli里 DNS 明明配了,cat /etc/resolv.conf却是另一批地址。排查 DNS 问题时,先ls -l /etc/resolv.conf看一眼是不是软链接,能帮你省掉一大圈弯路。
4. systemd-networkd:给静态环境和容器的最小实现
systemd-networkd 的设计哲学和 NetworkManager 完全相反:不做状态感知、不做自动切换、不提供交互式命令行,只按配置文件描述的状态去配置内核,然后老老实实待着。这种"少做事"的风格恰好适合服务器、容器宿主机、嵌入式设备这类网络结构固定的场景。它启动得早,依赖少,在没有图形界面的环境里几乎不占什么资源。
它的配置文件分三类,都放在/etc/systemd/network/下:.network文件描述一台设备或一组设备应该怎么配 IP,.netdev文件用来创建虚拟设备(bridge、bond、vlan、vxlan 等),.link文件在更底层设置网卡的重命名、MTU、MAC 等属性。三类文件都遵循同一个命名规则——数字前缀决定优先级,比如10-eth0.network比20-eth0.network优先,匹配到第一个就停止。这和 netplan 多文件的"后者覆盖前者"完全相反,是很多人搞混的地方。
一段静态地址配置:
# /etc/systemd/network/10-static.network [Match] Name=enp3s0 [Network] Address=192.168.10.40/24 Gateway=192.168.10.1 DNS=223.5.5.5 DNS=119.29.29.29加上虚拟网桥的场景:
# /etc/systemd/network/20-br0.netdev [NetDev] Name=br0 Kind=bridge # /etc/systemd/network/21-br0.network [Match] Name=br0 [Network] Address=192.168.10.50/24 Gateway=192.168.10.1 # /etc/systemd/network/22-enp4s0.network [Match] Name=enp4s0 [Network] Bridge=br0操作命令就两个:systemctl enable --now systemd-networkd启动并设为开机自启,networkctl list和networkctl status <设备名>查看状态。networkctl status的输出信息量很大,会告诉你这台设备当前用的是哪个.network文件、地址从哪来、DHCP 租约剩多久,排查时比ip a有用得多。
要特别注意一点:systemd-networkd 和 NetworkManager 默认都会接管没有被标记为 unmanaged 的设备。如果两个服务同时开启,同一张网卡上就会出现两家抢着配 IP 的情况,表现为地址反复变化、路由表莫名其妙多几条、重启后配置和预期不一致。解决办法是明确划分地盘,要么停掉其中一个,要么在 NetworkManager 的配置文件里把该设备显式排除:
# /etc/NetworkManager/NetworkManager.conf [keyfile] unmanaged-devices=interface-name:enp3s0;interface-name:enp4s0改完systemctl reload NetworkManager生效。判断一张网卡当前归谁管,最快的办法是nmcli device status,输出里状态为unmanaged的就是 NetworkManager 不碰的设备,那它八成在被 networkd 管;状态是connected或disconnected的则是 NetworkManager 在管。
另外,容器场景下还有个容易忽略的细节:systemd-networkd 管理的网卡如果被容器网络插件动态创建和删除,.network文件里的[Match]段最好用Name=加通配符或者Type=来匹配,而不是写死具体的接口名,否则新建的虚拟网卡匹配不到任何配置,会一直处于无地址状态。这是我做容器宿主机时踩过的坑——veth设备名每次都不固定,靠写死名字的配置文件永远跟不上。
5. 三者共存时的优先级冲突与逐步排查链路
把三个工具都装上的机器,问题往往不是"配不上",而是"配了不生效"或者"生效了但和预期不一样"。这类问题的排查之所以难,是因为三个工具各自有日志、各自的配置文件位置、各自的生效时机,光看一个地方看不出全貌。我把自己的排查顺序固定成下面这几步,基本能覆盖八成以上的场景。
第一步,确认设备当前的实际状态到底是谁配的。ip a看地址,ip route看路由,这是起点。如果地址存在但和配置文件不符,说明后台有东西在覆盖它。紧接着nmcli device status和networkctl status <设备名>各跑一遍,前者能告诉你 NetworkManager 是否在管这张卡,后者能告诉你 networkd 用的是哪个.network文件。两条信息一交叉,管理者就浮出水面了。
第二步,查 netplan 的 renderer 设置和后端生成结果。netplan get能看到合并后的完整配置(比直接看单个文件更准),然后ls /run/systemd/network/和ls /run/NetworkManager/system-connections/看看 netplan 到底把配置生成了哪种格式。如果 renderer 写的是 networkd 但 systemd-networkd 服务根本没启动,那配置生成了一堆也没人执行,表现就是配置"完全没反应"。这种情况下systemctl status systemd-networkd会显示 inactive,启动它就好。
第三步,检查服务之间的 unmanaged 划分。如果nmcli device status显示设备是connected,但你的配置写在/etc/systemd/network/里,那结论很清楚——networkd 的配置不会生效,因为设备被 NetworkManager 接管了。要么改 NetworkManager 的 unmanaged-devices 把设备让出来,要么把配置迁到 netplan 并指定 renderer 为 NetworkManager,二选一,别两边同时写。
第四步,看日志。journalctl -u systemd-networkd -u NetworkManager --since "10 min ago"能把两个服务的近期日志一起拉出来,配置文件解析失败、匹配失败、DHCP 超时这类信息都会在这里出现。尤其注意could not load configuration file或者no matching network这类关键字,前者是文件格式问题,后者是[Match]段没匹配上设备。
我遇到过一个典型的组合故障:Ubuntu 22.04 服务器,/etc/netplan/下同时存在安装器生成的00-installer-config.yaml和手工加的99-static.yaml,前者写了 DHCP,后者写了静态地址,但两者都没有显式指定 renderer。结果是 netplan 合并之后静态地址覆盖了 DHCP,生成给 networkd 的.network文件里地址是对的,但 DNS 却来自另一个文件,导致内网域名解析一直失败。排查时用netplan get才发现两份配置被合并成了一个奇怪的组合。这个教训就是:同一张网卡不要在多份 netplan 文件里定义,一个设备一份配置,写在一个文件里最省心。
再补充一个关于生效时机的经验。netplan apply 是异步的,它会生成配置并通知后端重载,但后端真正把地址配上内核还需要几百毫秒到几秒。如果你的脚本在netplan apply之后立刻去ip a检查,很可能读到的是旧状态,误判成"没生效"。稳妥的做法是netplan apply之后加个短暂的等待,或者用networkctl status这类会等状态收敛的命令去确认。
6. 按场景选型的取舍标准和我自己的配置习惯
把三者都用过一遍之后,选型其实就变得很清晰了,核心判断维度无非是三个:网络是否动态、是否需要图形化管理、是否运行在容器或嵌入式环境。下面这张表是我自己在实际项目里的取舍标准,可以直接参考。
| 场景 | 推荐组合 | 理由 |
|---|---|---|
| Ubuntu Server 静态网络 | netplan + systemd-networkd | 默认组合,配置声明式,启动早,资源占用低 |
| Ubuntu 桌面 / 笔记本 | netplan + NetworkManager | 需要无线漫游、设备插拔感知、图形界面 |
| 容器宿主机 / 无头设备 | systemd-networkd 直配 | 不需要 netplan 这层翻译,启动快,依赖少 |
| 需要频繁切换网络配置的测试机 | NetworkManager + nmcli | 命令式修改,连接档可快速切换 |
已有大量.network文件的存量机器 | 保持 systemd-networkd | 迁移到 netplan 收益不大,风险却不小 |
我更倾向于把 netplan 当作"配置的唯一入口"来用,尤其是新装的 Ubuntu 机器。所有网络改动都只改/etc/netplan/下的 YAML,绝不直接去碰/etc/systemd/network/或/etc/NetworkManager/system-connections/,因为那两个目录在 netplan 接管的情况下属于"生成产物"或者"被覆盖对象",改了就可能在下次 apply 或重启后失效。这个习惯养成之后,配置的可追溯性好很多——想知道一台机器的网络是怎么来的,看一个 YAML 文件就够了。
对于存量机器,我一般不强求统一。如果一台机器的 systemd-networkd 配置文件已经跑得很稳,那就继续用,没必要为了"技术栈统一"去迁 netplan,迁移过程中的断网风险远大于那点维护上的便利。反过来,如果一台机器上三个工具的配置都有,那优先级排序是:先把 netplan 的文件清理成唯一来源,再把 NetworkManager 或者 networkd 里重复的配置删掉,最后确认服务只启用了需要的那一个。乱比缺更麻烦,这是我在维护多台机器之后最深的体会。
最后分享一个日常检查的小习惯。接手任何一台不熟悉的 Linux 机器,我会先跑这一串命令把网络管理者的全貌摸清:
# 当前实际状态 ip -br a && ip route # netplan 视角 ls /etc/netplan/ && netplan get # NetworkManager 视角 systemctl is-active NetworkManager && nmcli device status # networkd 视角 systemctl is-active systemd-networkd && networkctl list # 生成产物落在哪 ls -l /run/systemd/network/ /run/NetworkManager/system-connections/ 2>/dev/null这一串跑完,八九不离十就能判断出这台机器的网络归谁管、配置文件在哪、有没有冲突。花不了一分钟,但能避免后面几十分钟的瞎猜。网络配置这东西,最怕的不是难,而是不知道改哪里会生效——把这三个工具的边界理清楚,多数问题就从"玄学"变成了"按图索骥"。