update 网络
pub async fn wait_for_pci_net_interface(
sandbox: &Arc<Mutex
root_complex: &str,
pcipath: &pci::Path,
) -> Result<()> {
let root_bus_sysfs = format!("{}{}", SYSFS_DIR, create_pci_root_bus_path(root_complex));
let sysfs_rel_path = pcipath_to_sysfs(&root_bus_sysfs, pcipath)?;
let matcher = NetPciMatcher::new(&sysfs_rel_path, root_complex);
let pattern = format!(
r"[./]+{}/[a-z0-9/]net/[a-z0-9/]",
matcher.devpath.as_str()
);
let re = Regex::new(&pattern).expect("BUG: Failed to compile regex for NetPciMatcher");
if check_existing(re)? {
return Ok(());
}
let _uev = wait_for_uevent(sandbox, matcher).await?;
Ok(())
}
结论
会拉起,而且“先建 tap、隔一段时间再发 UpdateInterface”在实践中是最稳的时序,不是最危险的。
原因是一句话:UpdateInterface 的等待环节有两条“回顾性”路径,不要求 uevent 发生在 RPC 之后;而真正的配置动作与 uevent 完全解耦。
但这里必须先区分两个不同的“先创建”:
| 你说的“先创建 tap” | guest 里有没有网卡 | 结果 |
|---|---|---|
A. 只在 host/pod netns 建了 tapN_kata,VMM 还没 hotplug |
❌ 没有 | 不会(无中生有不可能) |
B. tap 建好 + VMM 已完成 device_add/vm.add-net,只是 RPC 晚发 |
✅ 已有 eth0 |
会,而且通常是零等待 |
一、为什么“晚发”反而更容易成功
update_interface RPC(rpc.rs:1252-1340)的等待逻辑是三级递进,前两级都是“查现在”,不看事件到达时间:
// rpc.rs:1267
if !interface.devicePath.is_empty() {
wait_for_pci_net_interface(&self.sandbox, root_complex, &pcipath).await?;
// └─ 内部:check_existing() → wait_for_uevent()
}
// network_device_handler.rs:44-58
let matcher = NetPciMatcher::new(&sysfs_rel_path, root_complex);
let pattern = format!(r"[./]+{}/[a-z0-9/]*net/[a-z0-9/]*", matcher.devpath);
if check_existing(re)? { return Ok(()); } // ← ② 扫 sysfs(时序无关)
let _uev = wait_for_uevent(sandbox, matcher).await?; // ← ③ 才真正等
| 级 | 机制 | 位置 | 对“时间差”的敏感性 |
|---|---|---|---|
| ① | uevent_map 缓存命中 |
uevent.rs:123-128 |
完全无关(事件早于 RPC 也能命中,且不会过期删除) |
| ② | check_existing() 扫 /sys/class/net |
network_device_handler.rs:19-36, 52-54 |
完全无关(直接读内核当前状态) |
| ③ | 注册 watcher 等新 uevent | uevent.rs:135-159 |
只有“事件还没发生”时才需要,默认 3s 超时 |
⚠️ 顺序是 ② 先于 ③:
check_existing是 sysfs 真值探测。网卡已经稳定存在时,一定命中 ②,根本不进 ③。
所以延迟发送 = 网卡已经稳定 = ② 必中 = 零等待。
而真正干活的 netlink.rs:82 update_interface() 只做 netlink 查询:
// netlink.rs:100-110
let link = match self.try_find_link(LinkFilter::Address(&iface.hwAddr)).await? {
Some(link) => link,
None => {
self.set_link_mac_by_name(&iface.name, &iface.hwAddr).await?; // 按名字改 MAC
self.find_link(LinkFilter::Address(&iface.hwAddr)).await?
}
};
它查的是 netlink 里的当前 link 表,与 uevent、与 tap 创建时间毫无关系。
二、逐场景判定
场景 A:只有 host tap,VMM 未 hotplug → ❌ 不会
这里有个关键分支,很多人会误判:
// sandbox.go:1276
inf.DevicePath = endpoints[0].PciPath().String()
- VMM 没 hotplug ⇒
PciPath为空 ⇒Types.PciPath.String()(types/pcipath.go:66-72)对空slots执行strings.Join([]string{}, "/")="" - 于是
rpc.rs:1267if !interface.devicePath.is_empty()为 false ⇒ 整个 uevent 等待被跳过 - 直奔
netlink.rs:82:by-MAC 找不到 →set_link_mac_by_name("eth0", mac)→ 内部find_link(LinkFilter::Name)失败 →
// netlink.rs:319-323
.ok_or_else(|| anyhow!("Link not found ({})", filter))
- Go 侧
kata_agent.go:627-643的 retry 只对"Link not found"重试:
if !strings.Contains(errMsg, "Link not found") {
return retry.Unrecoverable(nerr)
}
...
retry.Attempts(20), retry.Delay(20*time.Millisecond) // ≈ 400ms 后放弃
⇒ 失败,且 400ms 就返回,不会等 3s。这种情况下再发一次也一样,必须等 VMM 完成 hotplug。
场景 B:tap + hotplug 都完成,间隔很久 → ✅ 会(且零等待)
| 子情况 | 命中 | 等待时间 |
|---|---|---|
agent 的 watch_uevents 当时在跑且收到了 add |
① uevent_map(uevent.rs:84 写入,watch_for_uevent 从不删除) |
0 |
| 事件发生在 agent bind socket 之前(冷插 / agent 启动中) | ② check_existing() |
0 |
事件被 ENOBUFS 丢了 |
② check_existing() |
0 |
devicePath 为空(mmio VMM:Dragonball/Firecracker) |
跳过①②③,直接 by-name 改 MAC(netlink.rs:105-109) |
0 |
结论:延迟发送时,网卡早已在 sysfs 里稳定存在,② 必然命中。 这也是为什么 kata 的冷插(VM template)能工作——那场景下 uevent 早在 agent 启动前就发完了。
场景 C:间隔很久,且期间被 hot-unplug 过 → ⚠️ 取决于 remove 事件是否收到
// uevent.rs:98-101
async fn process_remove(&self, logger: &Logger, sandbox: &Arc<Mutex<Sandbox>>) {
let mut sb = sandbox.lock().await;
sb.uevent_map.remove(&self.devpath);
}
| 情况 | 后果 |
|---|---|
remove 事件正常收到 |
map 条目被清 ⇒ 后续等待靠 ②(sysfs 也没有)⇒ 3s 超时或 "Link not found" ⇒ 正确失败 |
remove 事件丢了(agent 没设 NETLINK_NO_ENOBUFS/没调大 SO_RCVBUF) |
map 残留陈旧条目 ⇒ 等待“秒过” ⇒ netlink 找不到 link ⇒ "Link not found" ⇒ 重试 400ms ⇒ 失败 |
这是“时间差”唯一真正危险的面,但根因是事件丢失,不是延迟本身。而且延迟本身帮不上忙:网卡已不存在,任何时序都救不回来。
场景 D:延迟期间这张网卡已经被配过(重复调用)→ ✅ 基本幂等
| 项目 | 行为 | 位置 |
|---|---|---|
| IP 重复添加 | add_addresses 用 .replace() ⇒ NLM_F_REPLACE,不会 EEXIST 失败 |
netlink.rs:661-669 |
| MAC | 按 MAC 找到就直接用,不改 | netlink.rs:100-104 |
| 名字 | link.name() != iface.name 才重命名 |
netlink.rs:148 |
| UP 状态 | 已 UP 会先 down 再 up ⇒ 短暂抖动 | netlink.rs:112-115 |
唯一副作用:已经 UP 的网卡会被 down/up 一次。如果这期间有连接,会闪断。这是“晚发/重发
UpdateInterface”的实际代价。
场景 E:guest 侧 MAC 或名字变了(VM template / 快照恢复)→ ✅ 有专门兜底
rpc.rs:1291-1331 在 devicePath 和 hwAddr 都非空时,先按 PCI 路径反查 netdev 名字并对齐 MAC:
// rpc.rs:1293-1311
match sandbox.rtnl.netdev_name_from_pci_path(&interface.devicePath) {
Ok(Some(netdev_name)) => { sandbox.rtnl.set_link_mac_by_name(&netdev_name, &interface.hwAddr) ... }
Ok(None) => info!("no netdev found for PCI path before by-MAC lookup"),
Err(err) => warn!("unable to resolve netdev from PCI path, fallback to by-MAC lookup"),
}
netdev_name_from_pci_path(netlink.rs:235-257)直接读 <pci_sysfs>/net/ 目录名,找不到返回 Ok(None)。
⇒ 只要“PCI 路径”或“接口名”有一个是准的,MAC 就能被改成目标值,后面 by-MAC 查找必中。
三、“晚发”仍然可能失败的清单(按概率排序)
| # | 原因 | 现象(agent 日志 / 错误串) |
|---|---|---|
| 1 | guest 里确实没网卡(VMM 未 hotplug / hotplug 失败) | devicePath 空:Link not found (name: eth0);非空:interface not available: Timeout after 3s waiting for uevent NetPciMatcher {...} |
| 2 | devicePath 与 guest 实际 PCI 拓扑不符(PCI 路径推导错) |
②正则不匹配 + ③前缀不匹配 ⇒ 3s 超时(除非①里有历史事件救场) |
| 3 | check_existing 的字符集限制踩坑 |
正则 [a-z0-9/]* 不含 - _ .;[./]+{devpath} 未 regex::escape(devpath 里 0000:01:01.0 的 . 是通配符) |
| 4 | 名字/MAC 都对不上(网卡被改名且 MAC 未设) | Link not found (address: xx:xx:...) |
| 5 | 边删边查:check_existing 中 fs::read_link(path)? 遇 ENOENT |
直接 Err(network_device_handler.rs:26),不走等待 |
| 6 | agent.hotplug_timeout 被调小(如 1s)且真的需要等待 |
Timeout after 1s ... |
四、时间轴对照(场景 B)
t0 host: createLink(tapN_kata) + vhost fds + TC redirect ← 阶段 A
t1 VMM: netdev_add/device_add ──────────────────────────────► 阶段 B
t2 guest: virtio_net probe → register_netdevice → uevent ← 阶段 C
t3 agent: recv uevent → uevent_map[".../virtio0/net/eth0"] ← 阶段 D(缓存,永不删)
⋮ (可能间隔任意长;期间 UpdateInterface 未发送也不影响)
tN runtime: AddInterface/RPC UpdateInterface ← 阶段 E
agent:
check_existing() ── 命中 sysfs(t2 之后一直为真)──► Ok(()) ← 零等待
rtnl.update_interface(): by-MAC 找到 eth0 → 配 IP/MTU/name/UP ← 阶段 F
关键洞察:t3 的缓存是为了“早于 RPC 的事件”,t2 之后的 sysfs 状态是为了“一切情况”。二者叠加后,RPC 的到达时间窗口几乎是无限的(除了 agent 重启——但 agent 是 guest init,不重启)。
五、和你上一个问题的呼应
这也解释了 uevent_map “命中后不删除”的设计取舍:
- 坏处:内存只增不减(
Vec/HashMap无 GC),且 remove 丢失时会残留陈旧条目导致“假通过”; - 好处:正是“先建 tap 很久后再 UpdateInterface”能零等待通过的原因 —— 对同一
devpath的后续所有请求都走 ①。
六、现场验证方法
# guest 内(debug shell)
readlink -f /sys/class/net/eth0
# → /sys/devices/pci0000:00/0000:00:02.0/0000:01:01.0/virtio0/net/eth0
ip -d link show eth0 # 看 MTU / state UP / MAC
# host 侧 agent 日志
journalctl -u kata-containers | grep -E "Waiting for NetPciMatcher|found .* in uevent map|on channel|Timeout after"
# 出现 "found ... in uevent map" → 走 ①(事件早于 RPC)
# 出现 "found ... on channel" → 走 ③(真的等了新事件)
# 只有 "Timeout after 3s" 且无前两条 → ①②③ 全没命中(网卡不存在 / devpath 错了)
一句话回答:只要 VMM 已经把 NIC 插进 guest,那么无论 tap 建好多久之后才发 UpdateInterface,agent 都会成功把 guest 网卡配置起来并拉起(check_existing 扫 sysfs 或 uevent_map 缓存必中其一,之后再走 netlink by-MAC 配置);只有当 guest 里压根没有这张网卡(hotplug 未完成)或 devicePath/MAC 对不上时才会失败。

浙公网安备 33010602011771号