网络命名空间是 Linux 虚拟化能力里最容易被低估的一块。很多人第一次接触容器网络,看到两个隔离环境之间能互相 ping 通,第一反应是"这不就是配了个网桥嘛",但真正把两个命名空间直接点对点连起来、不依赖任何桥接设备的最小方案,其实是 veth pair。这个标题说的就是这件事:用一对虚拟网卡把两个独立的网络命名空间缝在一起,让它们像插了根网线一样通信。它解决的问题很具体——当你需要两个隔离网络栈之间有一条专属通道,又不想引入 bridge、不想多占一个网段时,veth pair 是最轻的解法。适合谁看?做过容器网络调试、写过网络相关的自动化脚本、或者单纯想搞明白"两个 namespace 之间到底怎么通"的运维和开发同学。下面我按自己实际搭环境的顺序,把原理、操作、踩坑和验证一条线讲透。
1. 先搞清楚 veth pair 到底是个什么东西
1.1 它不是网卡,是一根"两头都是水晶头的网线"
刚接触的时候我也有个误解,以为 veth 是一块虚拟网卡,创建出来就挂在本机某个地方。实际上 veth 永远成对出现,你可以把它理解成一根两端都做了水晶头的网线:一头插进命名空间 A,另一头插进命名空间 B,中间这段"线"由内核负责搬运数据。你把其中一头塞进某个 namespace,另一头如果留在初始命名空间,那这根线就连接了"宿主"和"容器";如果两头分别塞进两个不同的 namespace,那这根线就只连接这两个隔离环境,跟宿主没关系了。
这个"成对"属性是 veth 的核心特征,也是它和 tun/tap、bridge 这些设备最本质的区别。bridge 是一个可以挂多个端口的交换机,veth 是严格的一对一。所以当你需要连接三个以上命名空间时,veth 就不够用了,得靠 bridge 做汇聚,veth 只负责把每个 namespace "接入"到 bridge 上。理解这一点,后面选型就不会走弯路。
从内核实现角度看,veth 驱动在发送方向做的事情很直接:从一端收到的 skb(socket buffer),直接挂到对端的接收队列上,触发对端的软中断处理。它不经过物理网卡、不经过协议栈的完整收发路径,所以延迟极低。这也是为什么很多对性能敏感的场景(比如某些 service mesh 的数据面)宁愿用 veth 直连也不愿绕桥。
1.2 为什么两个 namespace 默认互相看不见
网络命名空间隔离的是一整套网络栈:网卡设备、路由表、ARP 表、iptables 规则、socket 端口空间,全都是独立的。你ip netns add ns1之后,ns1 里默认只有一个 loopback,而且是 down 状态。两个 namespace 各自有各自的127.0.0.1,互不干扰,也互不可达。
这种隔离是彻底的,不是"防火墙挡着"那种逻辑隔离,而是内核里根本就是两套数据结构。所以你不能指望在 ns1 里ip addr看到 ns2 的网卡,也看不到 ns2 的路由。想让它们通信,必须有一条物理上(逻辑上)存在的链路把两套栈连起来,veth pair 就是这条链路。这个认知很关键:通信不是配置出来的,是先把链路接上,再给链路两端配地址和路由。顺序搞反了,你会对着一堆"Network is unreachable"发呆。
1.3 veth 与 bridge、macvlan 的适用边界
实际工作里经常要在几种虚拟网络设备之间做选择,我整理了一个对照,方便你判断什么时候该用 veth:
| 设备类型 | 连接能力 | 典型场景 | 是否需要额外设备 |
|---|---|---|---|
| veth pair | 严格一对一 | 两个 namespace 直连、容器接入 bridge | 否 |
| bridge | 多端口汇聚 | 多个容器同网段互通、宿主机做网关 | 需要 veth 做接入 |
| macvlan | 一个父接口派生多个 | 容器需要独立 MAC、直接暴露到外部网络 | 依赖物理网卡 |
| tun/tap | 用户态与内核态之间 | 用户态网络程序、隧道实现 | 需要用户态程序配合 |
从表里能看出来,veth 的定位是"链路",不是"交换"。它最适合的场景就是点对点,或者作为 bridge 的接入端口。如果你一上来就想用 veth 连三个 namespace,那说明需求本身就该用 bridge,别硬凑。我见过有人为了省一个 bridge,硬生生建了三对 veth 做全互联,结果路由表复杂到没人敢动,这就是选型没想清楚。
2. 动手之前:环境与前置检查
2.1 确认内核能力和权限
veth 是内核模块veth提供的,绝大多数发行版默认就编进去了,但保险起见还是查一下。执行lsmod | grep veth,如果没有任何输出,试着手动加载modprobe veth。如果连 modprobe 都报"module not found",那说明内核编译时没带这个模块,得换内核或者装对应的 modules 包。这种情况在精简过的嵌入式系统里偶尔会遇到,普通服务器和开发机上基本不会。
权限方面,创建和操作网络命名空间需要CAP_SYS_ADMIN或者 root。日常实验我建议直接用 root 或者sudo -i进一个 root shell,别在每条命令前加 sudo,因为ip netns exec这种命令嵌套 sudo 的时候环境变量容易出问题,排查起来很烦。另外确认ip命令来自 iproute2 包,版本不要太老,ip netns子命令在很老的版本上行为有差异。
2.2 命名空间的生命周期管理习惯
ip netns add创建的命名空间会在/var/run/netns/下留一个挂载点文件,这个文件就是命名空间的"句柄"。这里有个特别容易踩的坑:如果你手动删了/var/run/netns/下的文件,命名空间并不会真正销毁,里面的进程还在跑,但你已经没法用ip netns管理它了。我早期做实验就干过这事,最后只能重启机器清理。
正确的清理方式是ip netns delete <name>,它会先卸载挂载点再清理。另外建议实验时给命名空间起有意义的名字,比如ns-left、ns-right,别用ns1、ns2,因为一旦你同时开好几个实验,回头根本分不清哪个是哪个。我现在的习惯是每次实验前先ip netns list看一眼有没有残留,有就清掉,保持环境干净。
2.3 一个容易忽略的点:loopback 默认是 down 的
新建的命名空间里,lo接口存在但是 down 状态。很多人配完 veth、配完地址,发现本机内部的服务都起不来,或者ping 127.0.0.1不通,就是忘了把 lo 拉起来。这个细节在只做跨 namespace 通信时不影响,但一旦你在 namespace 里跑任何依赖本地回环的服务(比如某些数据库、某些 agent),就会出问题。所以我的习惯是建完 namespace 第一件事就是ip netns exec <name> ip link set lo up,养成肌肉记忆。
3. 完整实操:从零搭一条跨命名空间链路
3.1 创建两个命名空间并拉起回环
先把两个隔离环境建出来:
ip netns add ns-left ip netns add ns-right ip netns exec ns-left ip link set lo up ip netns exec ns-right ip link set lo up执行完用ip netns list确认两个都在。这时候如果你ip netns exec ns-left ip addr,会看到只有一个 lo,状态是 UNKNOWN(回环的正常状态),没有其他网卡。这就是干净的起点。
3.2 创建 veth pair 并分发到两端
关键的一步来了。创建 veth pair 有两种写法,我推荐一步到位的写法:
ip link add veth-left type veth peer name veth-right这条命令在当前命名空间(通常是初始命名空间)创建了一对网卡,名字分别是 veth-left 和 veth-right。注意此刻它们都还在宿主命名空间里,还没进任何隔离环境。接下来把两头分别塞进去:
ip link set veth-left netns ns-left ip link set veth-right netns ns-right执行完这两条,宿主命名空间里就看不到这两块网卡了,它们分别"住进"了对应的 namespace。这里有个操作顺序的细节:必须先创建再分发,不能直接在某命名空间里创建然后指望对端自动过去。因为ip link add ... type veth peer name ...创建时两端都在当前上下文,分发是后续动作。我见过有人试图在 ns-left 里执行创建命令,结果两端都跑进 ns-left 了,还得再挪一头出来,多绕一步。
3.3 给两端配地址并拉起
现在两端各在各的 namespace 里,但都是 down 状态、没有地址。分别配置:
ip netns exec ns-left ip addr add 10.10.0.1/24 dev veth-left ip netns exec ns-left ip link set veth-left up ip netns exec ns-right ip addr add 10.10.0.2/24 dev veth-right ip netns exec ns-right ip link set veth-right up地址规划上我习惯用/24,虽然点对点链路用/30或/31更省地址,但/24在实验环境里更直观,而且不会因为掩码算错导致"明明配了地址却不通"。生产环境如果地址紧张,用/31是标准做法(RFC 3021),两端各一个地址,没有网络号和广播号的浪费。这里用10.10.0.0/24只是示例网段,你换成任何私有网段都行,只要别和现有网络冲突。
3.4 验证链路:ping 与状态检查
配完直接测:
ip netns exec ns-left ping -c 3 10.10.0.2正常情况下三发三收。如果不通,先别急着怀疑配置,按这个顺序查:ip netns exec ns-left ip link show veth-left看状态是不是 UP;ip netns exec ns-left ip addr show veth-left看地址有没有配上;ip netns exec ns-right ip link show veth-right看对端是不是也 UP。veth 的一个特性是:只要一端 down,整条链路就不通,因为链路状态是两端联动的。这个和物理网线类似,一头没插好,另一头也协商不起来。
再补一个验证手段,看 ARP 表:
ip netns exec ns-left ip neigh showping 通之后应该能看到10.10.0.2对应的 MAC 地址,状态是 REACHABLE 或 STALE。这个 MAC 是对端 veth 的 MAC,可以在 ns-right 里用ip link show veth-right核对。如果 ARP 表里一直是 INCOMPLETE,说明二层根本没通,问题多半在链路状态或者命名空间分发上。
4. 那些文档里不写、但一定会遇到的坑
4.1 命名空间里没有默认路由,出站全挂
这是最高频的问题。你配好地址、ping 通了对端,然后想在 ns-left 里访问外网或者访问宿主的其他网段,发现全部失败。原因很简单:你只配了直连网段的路由,没有默认路由。ip addr add会自动生成一条到10.10.0.0/24的直连路由,但0.0.0.0/0不会自动出现。
解决方式取决于你的目标。如果只是两个 namespace 互通,那不需要默认路由,现状就是对的。如果 ns-left 要访问 ns-right 之外的东西,就得手动加:
ip netns exec ns-left ip route add default via 10.10.0.2但这里有个前提:10.10.0.2 那端得具备转发能力,并且知道怎么把包送出去。单纯加默认路由只是把包丢给对端,对端不转发照样不通。所以跨 namespace 访问外部网络,本质上是把其中一个 namespace 当路由器用,需要开net.ipv4.ip_forward=1并配 NAT,这就超出 veth 本身的范畴了。我的建议是:先用 veth 把点对点通信跑通,再考虑要不要让它当网关,别一上来就堆路由和 NAT,出问题根本分不清是哪一层。
4.2 忘记 up 接口,链路静默失败
前面提过,但值得单独强调。ip addr add只是配地址,不会自动把接口拉起来。很多人配完地址直接 ping,不通,然后开始怀疑地址配错、掩码算错、命名空间建错,折腾半天发现就是ip link set ... up没执行。这个坑我踩过不止一次,后来养成习惯:配地址和 up 接口永远成对写,中间不插入其他命令,减少遗漏。
更隐蔽的一种情况是:接口 up 了,但对端没 up。因为 veth 链路状态联动,一端 up 一端 down 时,up 的那端ip link show会显示NO-CARRIER或者状态是 DOWN。看到这个提示,直接去查对端,别在本端死磕。
4.3 地址冲突导致"时通时不通"
实验环境里网段随手一配,很容易和宿主机已有的网段撞车。比如你用了10.10.0.0/24,而宿主机上某个 docker 网络或者某个物理网段正好也是这个,就会出现诡异现象:有时候通有时候不通,或者 ping 延迟忽高忽低。根因是路由表里出现了两条指向同一网段但出口不同的路由,内核按最长前缀匹配和 metric 选路,选到哪条看运气。
排查方法:在 namespace 里ip route get 10.10.0.2,看内核实际选了哪个出口。如果出口不是你以为的 veth,那就是路由冲突。解决就是换网段,实验环境我一般用10.99.x.x这种不太可能被占用的段,或者干脆用192.168.199.0/24这种冷门段。别用172.17.0.0/16,那是 docker 默认网段的重灾区。
4.4 删除顺序不对,留下"僵尸"设备
清理的时候,如果你先ip netns delete ns-left,再想删 ns-right 里的 veth-right,会发现删不掉或者报错。因为 veth 是成对的,一端所在的命名空间被销毁时,内核会把这一对都回收,但回收过程有先后,有时候会留下半截状态。稳妥的清理顺序是:先删 veth pair(在任意一端执行ip link del即可,会连带删除对端),再删命名空间。
ip netns exec ns-left ip link del veth-left ip netns delete ns-left ip netns delete ns-right如果已经乱了,用ip netns list看残留,用ip link show type veth在宿主和各 namespace 里找残留设备,逐个清理。实在清不干净,重启是最快的兜底,但作为从业者还是建议把清理流程固化下来,别依赖重启。
5. 把 veth 用进真实场景:几个延伸方向
5.1 容器网络里的 veth:你每天都在用但没注意
docker 默认的 bridge 网络模式下,每启动一个容器,宿主上就会多出一对 veth:一头叫vethXXXXXX挂在 docker0 桥上,另一头在容器命名空间里叫eth0。你在宿主上ip link show type veth能看到一堆这样的设备,名字是随机后缀。容器能上网、能被端口映射访问,底层就是这对 veth 在搬数据。
理解了这个,很多容器网络问题就有了排查抓手。比如容器 ping 不通外网,你可以顺着 veth 往上看:容器内ip route看默认路由指向哪,宿主上找到对应的 veth 看它是不是挂在桥上,桥上有没有 IP,宿主有没有开转发和 NAT。这条链路一旦建立起来,排查效率比盲猜高一个数量级。我处理过的容器网络故障里,八成都能通过"顺着 veth 两头看"定位到具体是哪一环断了。
5.2 用 veth 做网络实验的隔离沙箱
做网络协议实验、路由实验的时候,最怕污染宿主机网络。用 network namespace 加 veth 可以搭一个完全隔离的沙箱:建几个 namespace,用 veth 两两相连或者接到一个 bridge 上,在里面随便折腾路由协议、iptables 规则、甚至跑个软路由,玩坏了直接ip netns delete一删了事,宿主机毫发无损。
这种沙箱的价值在于可复现。你可以把整套搭建命令写成一个脚本,每次实验前跑一遍,环境秒级就绪。我自己的习惯是把常用拓扑(两点直连、三点星型、带网关的三角)都写成小脚本,需要哪个跑哪个。比开虚拟机轻量太多,启动时间从分钟级降到秒级,而且资源占用几乎可以忽略。
5.3 性能敏感场景下 veth 直连的取舍
前面提过 veth 直连延迟低,但也不是所有场景都该用它。veth 的数据路径虽然短,但每经过一次命名空间边界,就涉及一次上下文切换和 skb 的重新归属,在高吞吐场景下这部分开销会累积。实测在普通服务器上,veth 直连的吞吐能跑到物理网卡的线速附近,延迟在微秒级,对绝大多数应用足够。但如果你的场景是超低延迟(比如某些金融交易系统),可能要考虑更底层的方案,比如共享内存或者 DPDK 这类绕过内核协议栈的路子。
我的经验是:先用 veth 把功能跑通,用真实流量压一压,看瓶颈在哪再决定要不要换方案。过早优化网络路径,往往优化了个寂寞,因为真正的瓶颈可能在应用层或者磁盘 IO。veth 的简单可靠本身就是一种性能——少一层设备,少一类故障。
6. 排查思路:不通的时候按这个顺序查
6.1 从链路层往上逐层验证
网络排查最忌讳东一榔头西一棒子。我固定用自下而上的顺序:先看链路(ip link,两端是否 UP、是否有 NO-CARRIER),再看地址(ip addr,地址和掩码是否正确),再看路由(ip route,目标网段走哪个出口),再看邻居(ip neigh,ARP 是否解析成功),最后才看应用层(端口是否监听、防火墙是否放行)。
这个顺序的逻辑是:下层不通,上层怎么配都没用。比如链路没 up,你配再完美的路由也是白搭;ARP 解析不了,路由选对了包也发不出去。按层排查的好处是每一步都有明确的"通过/不通过"判据,不会陷入"感觉哪里不对"的玄学。
6.2 用 tcpdump 在两端同时抓包
当分层排查到某一层卡住、又看不出原因时,抓包是终极手段。veth 的好处是两端都能抓:在 ns-left 的 veth-left 上抓,在 ns-right 的 veth-right 上抓,对比两边的包。
ip netns exec ns-left tcpdump -i veth-left -nn -c 10 ip netns exec ns-right tcpdump -i veth-right -nn -c 10如果左边发出了 ICMP request 但右边没收到,问题在链路中间(几乎不可能,veth 是内核直连);如果右边收到了 request 但没回 reply,问题在右边的协议栈(可能是路由、可能是防火墙);如果右边回了 reply 但左边没收到,对称地查。两端同时抓包能瞬间把问题范围缩小一半,比单端抓包瞎猜高效得多。这个技巧我在排查容器网络问题时用得最多。
6.3 常见现象与对应根因速查
把高频问题整理成表,方便对照:
| 现象 | 最可能的原因 | 快速验证 |
|---|---|---|
| ping 报 Network is unreachable | 没有到目标网段的路由 | ip route get <目标IP> |
| ping 无响应但无报错 | 接口 down 或对端 down | 两端ip link show看状态 |
| ARP 一直 INCOMPLETE | 二层不通,链路或分发有问题 | 两端抓 ARP 包 |
| 时通时不通 | 网段冲突或路由有多条 | ip route get看选路 |
| 能 ping 通但端口连不上 | 应用没监听或防火墙拦截 | ss -lntp加 iptables 检查 |
这张表不是穷举,但覆盖了我实际遇到的绝大多数情况。遇到表外的问题,回到 6.1 的分层思路,从链路层重新走一遍,基本都能定位。
7. 几个让操作更顺手的个人习惯
7.1 把搭建过程脚本化
手工敲命令做一次实验没问题,做十次就会烦,而且容易漏步骤。我的做法是把"建命名空间、建 veth、配地址、up 接口、验证"写成一个 bash 脚本,参数化命名空间名和网段。这样每次实验一条命令搞定,还顺便把正确顺序固化下来了。脚本里加上set -e,任何一步失败立即停,避免在错误状态上继续操作。
脚本化还有个隐性好处:逼你把步骤想清楚。手工操作时很多步骤是"顺手就做了",写脚本时必须明确每一步的前置条件和预期结果,这个过程本身就能发现理解上的漏洞。我好几回都是在写脚本时意识到"哦这里顺序其实有依赖",然后回头修正了手工流程。
7.2 给实验环境定一套命名和网段规范
命名空间名用ns-前缀加用途,veth 名用veth-加两端标识,网段固定用某个冷门私有段。这套规范看起来是小事,但在同时开多个实验时能救命。我曾经同时开着三组实验,因为命名混乱,把 A 组的 veth 配到了 B 组的命名空间里,排查了半小时才发现是"接错线"了。从那以后我就严格按规范来,再没出过这种低级错误。
网段规范尤其重要。建议在实验机上维护一个"已占用网段"的清单,新实验前查一下,避免和 docker 网络、已有实验、物理网段冲突。这个清单可以就是一个文本文件,简单但有效。
7.3 验证不止于 ping
ping 通只说明 ICMP 通了,不代表 TCP 通、不代表应用能跑。完整的验证应该包括:ping 测三层连通性,nc或curl测四层端口可达性,实际跑一下目标应用测端到端。我见过 ping 通但 TCP 不通的情况,根因是中间有 iptables 规则只放行了 ICMP。所以验证要覆盖到你真正关心的那一层,别拿 ping 通当万事大吉。
另外建议验证时带上-c限制次数,别让 ping 无限跑着占终端。ping -c 3三发足够判断连通性,再多就是浪费时间。如果要做稳定性测试,那是另一个话题,用ping -f或者专门的压测工具,别用普通 ping 硬扛。
8. 写在最后的一点体会
veth pair 这个技术本身不复杂,命令就那么几条,但它是理解 Linux 网络虚拟化的一把钥匙。把两个命名空间用 veth 连起来这件事跑通之后,你再去看容器网络、看 bridge、看各种 overlay 方案,会发现底层逻辑是相通的:无非是链路怎么接、地址怎么配、路由怎么指。这套心智模型建立起来,遇到再复杂的网络拓扑也能拆解成一个个点对点的连接来分析。
我个人在实际操作中的体会是,这类底层网络实验最大的价值不在于"学会一条命令",而在于建立排查的直觉。你知道数据包从 A 到 B 要经过哪几步,出问题时就能快速判断是哪一步断了。这种直觉没法从文档里读出来,只能靠一次次搭环境、一次次踩坑、一次次抓包积累。所以别怕折腾,把环境搭起来,故意配错几个地方,看看报什么错,比看十篇教程都管用。