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:1267 if !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 对不上时才会失败。

posted @ 2026-10-23 23:26  rincloud  阅读(4)  评论(0)    收藏  举报