深入理解 macvlan:从父子接口到内核实现

在容器网络和 Kubernetes Underlay 场景中,macvlan 是被广泛使用的高性能网络方案。但很多人对它的理解停留在"配置能跑通"的层面,对它最核心的设计——父接口与子接口的关系——缺乏深入认识。

理解这个父子关系,是理解 macvlan 一切行为(包括它的性能优势、通信限制、以及为什么需要 Spiderpool 这类方案来补齐能力)的钥匙。

macvlan 的本质是:在一个真实/下层网络接口上,虚拟出多个拥有独立 MAC 的接口。 所以必须有一个"被依附"的接口来提供物理链路,这个接口就是父接口;虚拟出来的那些接口就是子接口。

1
2
3
4
物理网卡 eth0(父接口)
├── macvlan0(子接口,给 Pod A)
├── macvlan1(子接口,给 Pod B)
└── macvlan2(子接口,给 Pod C)

一、核心概念:父接口与子接口

1.1 父接口的作用:提供二层出口

父接口通常是宿主机上的物理网卡,也可以是 bond、bridge、VLAN 接口等。它负责真正把帧发到物理链路,或者从物理链路收帧。子接口自己并没有物理收发能力,它必须依赖父接口。

  • 发送时:子接口要发帧,会把帧交给父接口,由父接口从物理网卡发出去;
  • 接收时:父接口收到帧,根据目的 MAC 判断这个帧属于哪个子接口,然后交给对应的子接口;
  • 子接口:拥有独立的 MAC 地址和 IP 地址,可以放到不同的网络命名空间里,比如每个 Pod 的 netns。

所以父子关系不是"对等连接",而是承载与依赖关系。父接口是物理通道,子接口是虚拟端口。

1.2 为什么不是 veth 那种对等结构?

veth 是成对的:

1
Pod netns(veth0) <----> 宿主机 netns(veth1)

两端是对等的,一端进 Pod,一端留在宿主机,流量必须经过宿主机 IP 栈。

macvlan 不是这样。它是一个父接口下挂多个子接口:

1
2
3
4
父接口 eth0(宿主机 netns)
├── 子接口 macvlan0(Pod A netns)
├── 子接口 macvlan1(Pod B netns)
└── 子接口 macvlan2(Pod C netns)

一个父接口可以对应很多子接口,所以是"父子"或"上下层"关系,而不是"对端"关系。这两种结构带来的本质差异:

维度 veth macvlan
拓扑 点对点 一对多
数据路径 经过宿主机 IP 栈 绕过宿主机 IP 栈,纯二层
性能 有内核协议栈开销 接近线速
MAC 地址 一对 veth 各有 MAC 每个子接口独立 MAC,对物理网络可见

1.3 内核为什么需要父子关系?

因为内核必须知道三个问题的答案:

这个子接口从哪个物理设备收发帧? 答案就是父接口。没有父接口,子接口就没有物理链路可走。

父接口收到帧后,该交给哪个子接口? macvlan 驱动会在父接口上注册一个接收处理钩子(rx_handler)。父接口收到帧后,根据目的 MAC 去查找对应的子接口,然后转发过去。

多个子接口如何共享同一个物理网卡? 每个子接口有独立 MAC,父接口像交换机背板一样,根据 MAC 把帧分发给正确的子接口。

所以父子关系是 macvlan 实现"多个虚拟接口共享一个物理接口"的关键机制。

1.4 类比理解

可以把父接口想成一块物理交换机上行口,子接口想成插在它上面的多个虚拟端口:

1
2
3
4
5
6
7
8
9
物理交换机 / 外部网络
│
父接口 eth0
(物理上行链路)
│
┌────┼────┐
│ │ │
macvlan0 macvlan1 macvlan2
(Pod A) (Pod B) (Pod C)

每个子接口都有自己的 MAC,外部看起来就像多台独立主机接在同一交换机上。对外部网络(交换机、路由器、防火墙)而言,这些 Pod 就是真实的物理主机,可以被直接路由和管控——这正是 Underlay 网络的价值所在。

1.5 父子关系可以跨 netns

典型用法是:

1
2
ip link add link eth0 name macvlan0 type macvlan mode bridge
ip link set macvlan0 netns <pod-netns>
  • eth0 是父接口,留在宿主机 netns;
  • macvlan0 是子接口,被移动到 Pod netns;
  • 父子关系仍然存在,内核知道 macvlan0 的 lower device 是 eth0。

这样 Pod 里的 macvlan0 就能直接通过宿主机的 eth0 与外部二层网络通信,而不进入宿主机的 IP 栈。这是 macvlan 性能优异的根本原因:数据包从 Pod netns 的子接口直达物理网卡,中间没有 netfilter、没有 iptables、没有 IP 转发。


二、深入内核:父子关系的实现

前面讲的是概念模型。那么,这个父子关系由谁维护?答案是:完全由 Linux 内核维护,具体来说是内核里的两块:

  • 网络设备核心:net/core/dev.c 等;
  • macvlan 驱动:drivers/net/macvlan.c。

用户空间的 ip link、CNI 插件、kubelet 都只是"发起创建请求",关系建立之后,运行时的父子绑定和数据转发都归内核管。

2.1 内核里的数据结构

每个网络接口在内核里都是一个 struct net_device。macvlan 子接口会多一层私有数据:

1
2
3
4
5
6
struct macvlan_dev {
struct net_device *dev; // 子接口本身
struct net_device *lowerdev; // 指向父接口,比如 eth0
struct macvlan_port *port; // 父接口上的 macvlan 端口信息
...
};

父接口上会挂一个 struct macvlan_port,里面有一张哈希表,用来根据 MAC 地址找到对应的子接口:

1
2
3
4
5
struct macvlan_port {
struct net_device *dev; // 父接口
struct hlist_head macvlan_hash[MACVLAN_HASH_SIZE];
...
};

同时,netdev core 还维护一套通用的 upper/lower 设备层次,用 netdev_adjacent 结构记录"谁挂在谁下面"。你在 ip link 里看到的 macvlan0@eth0,以及 sysfs 里的 lower_eth0 符号链接,就是这套关系的外在表现。

2.2 创建时:内核怎么建立父子关系

当你执行:

1
ip link add link eth0 name macvlan0 type macvlan mode bridge

流程大致是:

  1. ip 通过 netlink 发一条 RTM_NEWLINK 消息,里面用 IFLA_LINK 指定父接口 eth0 的索引;
  2. 内核 netdev core 收到后,根据类型名 macvlan 找到注册好的 macvlan_link_ops;
  3. 调用 macvlan_newlink(),最终进入 macvlan_common_newlink()。

在 macvlan_common_newlink() 里:

  • 分配子接口的 net_device;
  • 设置 vlan->lowerdev = lowerdev,也就是把父接口指针存下来;
  • 调用 netdev_upper_dev_link(lowerdev, dev),在 netdev core 里正式建立"父—子"层次;
  • 调用 netdev_rx_handler_register(lowerdev, macvlan_handle_frame, port),在父接口上注册接收处理函数。

这一步完成后,父子关系就由内核记住了。之后即使把子接口移到 Pod 的 netns,这个关系仍然保留在内核全局的设备层次里——netns 只是命名空间的隔离,不会切断内核设备模型中的 lower/upper 绑定。

2.3 运行时:内核怎么用这个父子关系转发

发送方向。 Pod 里的 macvlan0 要发包时,调用的是 macvlan 驱动的 macvlan_start_xmit():

1
2
skb->dev = vlan->lowerdev;   // 把发送设备改成父接口 eth0
dev_queue_xmit(skb); // 交给父接口从物理网卡发出去

接收方向。 物理网卡 eth0 收到帧后,netdev core 会检查它有没有注册 rx_handler。因为创建时注册了 macvlan_handle_frame,所以帧会进入 macvlan 驱动:

1
2
3
4
macvlan_handle_frame()
→ 根据目的 MAC 查 macvlan_hash 哈希表
→ 找到对应的子接口 macvlan0
→ 把 skb 交给子接口

macvlan_handle_frame 会先处理多播包,再处理单播包,查哈希表匹配目的 MAC,命中则重定向到对应子接口继续上送协议栈。此外它还会跳过 loopback 包,这与 bridge 代码中 br_handle_frame() 的检查逻辑类似。

整个过程都在内核里完成,不经过宿主机 IP 栈,也不经过 iptables/IPVS。

2.4 谁不负责维护这个关系?

排除法能帮我们更清晰地定位职责边界:

  • 不是 systemd/NetworkManager:它们只管宿主机自己的网络配置;
  • 不是 iptables/IPVS:它们工作在 IP 层,而 macvlan 父子关系是二层设备层次;
  • 不是 CNI 插件:CNI 插件只是调用 netlink 创建接口、移动 netns,创建完就退场了;
  • 不是 kubelet:kubelet 只负责调用 CNI。

真正一直维护父子关系、哈希表、rx_handler、发送重定向的,是内核 netdev core + macvlan 驱动。


三、四种工作模式:父子关系的不同"玩法"

macvlan 支持四种模式,它们决定了同一父接口下的多个子接口之间如何互访,以及与外部网络的交互行为。

3.1 bridge 模式(最常用)

同一父接口下的子接口之间可以直接在内核里交换帧,不需要把帧发到外部交换机再绕回来。这就是为什么 Kubernetes/CNI 场景下默认都用 bridge 模式——它让同宿主机上的 Pod 互通不依赖外部交换机的 hairpin 能力。

1
2
Pod A (macvlan0) ←→ macvlan 驱动内部转发 ←→ Pod B (macvlan1)
(不经过物理链路)

3.2 vepa 模式

子接口之间的通信必须经过外部交换机(要求交换机支持 hairpin/反射继播)。帧从父接口发出,经物理交换机再回到父接口,再分发给目标子接口。

3.3 private 模式

同一父接口下的子接口之间完全隔离,即使外部交换机支持 hairpin 也无法通信。适用于强隔离场景。

3.4 passthru 模式

一个父接口只能绑定一个子接口,子接口独占父接口,常用于 SR-IOV 类似的直通场景。

各模式对比:

模式 子接口互通 外部依赖 典型场景
bridge 内核内直接互通 无 容器/K8s 默认选择
vepa 经外部交换机 交换机支持 hairpin 需要在交换机上做流量管控
private 互相隔离 无 安全隔离
passthru 只有一个子接口 无 直通独占

四、一个绕不开的限制:父接口与子接口不能通信

理解了父子关系的实现后,一个著名的限制就变得"理所当然"了:

宿主机自己无法直接访问同网卡上的 macvlan 子接口,反之亦然。

原因在内核转发路径里:

  1. 父接口 eth0 发出的帧,目的 MAC 是某个子接口的 MAC。但帧走的是 eth0 的发送队列,不会经过 eth0 上注册的 rx_handler(rx_handler 只对"从物理链路收到的帧"生效);
  2. 因此这个帧会被直接从物理网卡发出去,而不是交给子接口;
  3. 反过来,子接口发给宿主机(父接口 MAC)的帧,交给父接口发送后同样直接出网卡,宿主机的 IP 栈收不到。

这是 macvlan 的内核级限制,不是配置问题,无法通过调参解决。常见的 workaround 是在宿主机上额外创建一个 macvlan 子接口(比如 macvlan-host),配置宿主机路由让去往 Pod 子网的流量走这个接口——本质上是用"子接口对子接口"绕开"父对子"的限制。

这也正是 Spiderpool 的 coordinator 插件存在的重要理由之一:它自动为 Pod 创建一对 veth 设备并调谐路由,系统性解决"Pod 无法访问宿主机、本地健康检查失败、无法访问 ClusterIP"等连通性问题。


五、如何查看内核维护的这层关系

1
2
3
4
5
6
7
8
9
ip link show
# 会看到:macvlan0@eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> ...
# @eth0 就是父接口标识

ip -d link show macvlan0
# 会显示:macvlan mode bridge,以及父接口信息

ls /sys/class/net/macvlan0/
# 通常会有 lower_eth0 之类的符号链接,指向父设备

这些信息都来自内核的 net_device 和 upper/lower 邻接列表。

另外值得注意的实践细节:macvlan 子接口不能再被 bridge 到第二个接口,且理论上单个物理适配器上可挂载的子接口数量存在上限(Docker 场景下常见 4094 个的讨论)。如果 OS 上运行了 NetworkManager,建议通过 /etc/NetworkManager/conf.d/ 配置将相关接口标记为 unmanaged,避免其干扰 CNI 创建的虚拟接口。


六、回到 Kubernetes:macvlan 在 CNI 中的位置

结合前文对 Spiderpool 的讨论,现在可以更完整地描绘 macvlan 在 Kubernetes Underlay 架构中的角色:

1
2
3
4
5
6
7
8
9
10
Pod netns
├── eth0 ← Cilium/Calico(Overlay,集群东西向)
└── net1 ← macvlan 子接口(Underlay,南北向/RDMA)
│
│ 父子关系(内核维护)
▼
宿主机 eth0 / bond / vlan 子接口(父接口)
│
▼
物理交换机(Pod 以独立 MAC 直接收发)
  • CNI 插件(如 multus + macvlan binary):在 Pod 启动时执行 ip link add link <parent> ... type macvlan mode bridge,把子接口移入 Pod netns,配置 IP(IP 来自 Spiderpool IPAM 分配),然后退出;
  • 内核:从此接管一切——维护父子绑定、哈希表、rx_handler 分发、发送重定向;
  • Spiderpool coordinator:作为 chain plugin 补齐内核不管的部分——多网卡策略路由、宿主机连通性(veth workaround)、Service 访问等。

三方分工清晰:CNI 负责"建",内核负责"跑",coordinator 负责"补"。


小结

macvlan 的父子关系由 Linux 内核的 netdev 核心和 macvlan 驱动共同维护:

  • netdev core 维护通用的 upper/lower 设备层次;
  • macvlan 驱动维护 lowerdev 指针、父接口上的哈希表和 rx_handler;
  • 发送时子接口把包转给父接口;
  • 接收时父接口根据 MAC 把包分给对应子接口;
  • 子接口间互通与否取决于模式(bridge 模式内核内直通);
  • 父接口与子接口间因 rx_handler 只作用于接收路径,存在无法直接通信的内核级限制。

macvlan 有父子之分,是因为它必须依附于一个真实接口来承载二层流量。父接口提供物理出口和 MAC 分发,子接口提供独立的网络身份。一个父接口可以挂多个子接口,所以是"一对多"的父子关系,而不是 veth 那种"点对点"的对等关系。这个父子关系跨 netns 有效,让 Pod 能以独立主机的姿态直连物理网络——这是 macvlan 高性能与 Underlay 价值的根源,也是它所有通信限制(父对子不通、需交换机感知 MAC 等)的根源。

理解了父子关系,macvlan 就不再是一个"配置能跑就行"的黑盒:你能预判它的行为边界,能解释踩过的坑,也能明白 Spiderpool 这类方案在网络栈的哪个位置、为解决什么问题而存在。


延伸资源