☰
smolvm-agent 持久化 Overlay 的 DNS 刷新与有界关闭信号:热备用机群可靠性修补深度解析
2026/10/9 1:34:00 网站建设 项目流程
  • 虚拟化
  • AI Agent
  • 人工智能
  • CLI

【免费下载链接】smolvm

An embeddable, portable, branchable virtual machine to safely run Agents locally.

项目地址:https://gitcode.com/gh_mirrors/sm/smolvm
点击查看免费下载

导读

本文围绕 smolvm 仓库中 crates/smolvm-agent/src/INTENT.md 记录的一次源码级可靠性修补展开:第一项是让持久化 Overlay 文件系统在复用(reuse)与重挂载(remount)两种路径上都按当前生效的网络后端刷新/etc/resolv.conf,确保热备用(warm-fleet)机器群在长期运行中 DNS 不被旧配置"钉死";第二项是为 SIGTERM/SIGINT 信号处理引入"有界关闭证据"——在退出前向既有 stderr 通道写入一条固定、有界、无分配的 JSON 事件。读完本文,你将掌握这两处修改的完整实现细节、三层 DNS 决策优先级、异步信号安全约束清单,以及如何用仓库内的编译式测试复现验证。

背景与动机:warm-fleet 可靠运行的两个痛点

smolvm 的 agent(crates/smolvm-agent)负责在宿主机上为每个 workload 准备持久化 Overlay 文件系统并启动虚拟机。在生产场景下,同一台机器上的 workload 会长期复用、反复挂载/重挂载同一份 overlay,形成"热备用机群"。INTENT.md 开篇即点明其目标:Sean 要求可靠的热备用机群运行,Root 授权了这些源码修正。

在此之前存在两类隐患:

  1. DNS 配置随网络后端漂移:overlay 首次创建时写入的resolv.conf反映当时的网络模式;当 agent 以 virtio-net 或 TSI(无 virtio 的 tap/serial 后端)复用同一持久化 overlay 时,旧文件不会自动更新,导致容器内 DNS 查询走错服务器。
  2. 关闭路径不可观测:SIGTERM/SIGINT 处理器只做sync()后退出,机群调度器无法区分"信号确实到达并已处理"与"信号丢失/阻塞",排障只能靠猜。

下文逐一拆解这两处修正的源码实现。

修补一:持久化 Overlay 复用/重挂载时的 Resolver 刷新

Overlay 生命周期的三种状态

核心入口是 storage.rs 中的OverlaySetup::execute_or_remount()。它按三种情形分派:

情形判定条件动作
已挂载且健康merged_path存在且是挂载点,mounted_overlay_is_healthy()通过复用:refresh_overlay_resolver(&merged_path)后重建 bundle 直接返回
已挂载但陈旧挂载点存在但健康检查失败(如 fork 克隆携带的陈旧挂载,每次查询返回 ESTALE)杀掉 keep-alive 容器、卸载死挂载,落到重挂载路径
仅上层存在upper_path存在(例如 VM 重启后目录还在但未挂载)重挂载:先refresh_overlay_resolver(&upper_path),清空并重建 work 目录,再挂载
全新创建上层目录都不存在走完整execute()流水线

前两种情形正是 INTENT.md 所述"复用/重挂载时跟随当前活跃网络 DNS 策略"的落地位置:每次复用或重挂载,都在挂载/复用完成前刷新 resolver,且不重写持久化 overlay 的其他内容(测试中专门断言etc/unrelated与etc/hosts原样保留)。

写入纪律:Merged 优先,绝不直写 Live Upper

INTENT.md 有一条硬性纪律:"Mounted overlays are written through merged, never the live upper directory"(已挂载的 overlay 必须通过 merged 写入,绝不能直写活跃的 upper 目录)。对照源码:

  • 复用路径调用refresh_overlay_resolver(&self.merged_path)——overlay 已挂载,写入merged/etc/resolv.conf会经 overlayfs 正常落到 upper 层,符合复制写(CoW)语义;
  • 重挂载路径调用refresh_overlay_resolver(&self.upper_path)——此刻 overlay尚未挂载,upper 还不是"活跃"的 overlay 组成部分,直接写它是安全的。

这一区分避免了绕过 overlayfs 直写 live upper 导致的可见性/原子性问题。刷新函数本身位于 storage.rs,实现极简:

fn refresh_overlay_resolver(root: &Path) -> Result<()> { let path = root.join("etc/resolv.conf"); std::fs::write(&path, overlay_resolv_conf_contents()).map_err(|error| { warn!(path = %path.display(), error = %error, "persistent overlay resolver refresh failed"); StorageError::new(format!("refresh persistent overlay resolver {}: {error}", path.display())) }) }

注意失败路径:写失败会返回Result错误,并以warn!打印受影响的具体路径——这正是 INTENT.md 要求的行为,保证排障时能立即定位是哪个 workload 的 overlay 刷新失败。

Resolv.conf 内容决策:三层优先级

内容生成函数overlay_resolv_conf_contents()(storage.rs)按固定优先级决定写入内容:

fn overlay_resolv_conf_contents() -> String { if std::env::var(guest_env::DNS_FILTER).as_deref() == Ok("1") { return "nameserver 127.0.0.1\n".to_string(); // ① DNS 过滤 } if let Ok(dns_server) = std::env::var(guest_env::DNS) { if !dns_server.is_empty() { return format!("nameserver {}\n", dns_server); // ② 主机显式下发 } } "nameserver 8.8.8.8\nnameserver 1.1.1.1\n".to_string() // ③ 公共兜底 }
优先级触发条件写入内容语义
①SMOLVM_DNS_FILTER=1nameserver 127.0.0.1启用 guest 侧 DNS 过滤代理,本地代理拦截所有查询
②SMOLVM_NETWORK_DNS非空该地址virtio-net 下是网关地址;TSI 下是--dns指定的自定义 resolver,guest 直连
③主机未指定8.8.8.8+1.1.1.1仅当主机没有下发时兜底

"显式覆盖保持既有优先级"这一要求对应 guest_env.rs 中的常量定义:SMOLVM_NETWORK_BACKEND=virtio-net、SMOLVM_NETWORK_DNS、SMOLVM_DNS_FILTER均为 guest 环境变量;TSI 路径在 main.rs 的tsi_resolv_conf中同样遵循"显式--dns覆盖优先、仅修复残留的 127.0.0.1"逻辑,使两个后端对"guest 用哪个 nameserver"达成一致——这正是--dns在公共解析器被封锁的网络(1.1.1.1/8.8.8.8 不可达)上能生效的原因。

此外,首次创建 overlay 时(setup_upper_layer),agent 还会在 upper 层写入gai.conf(IPv4 优先,防止 IPv6 路径故障导致硬挂起)以及镜像缺失时的默认hosts——这些属于 overlay 初始化范围,刷新修正刻意不触碰它们,与"只修 resolver、不重写其他内容"的意图一致。

修补二:有界关闭信号证据(SIGTERM/SIGINT)

为什么强调"有界"

信号处理器运行在异步信号上下文,任何调用都必须异步信号安全。INTENT.md 给出了硬性约束清单:无分配(no allocation)、无锁(locks)、无凭据(credentials)、无重试循环(retry loop),且只使用既有的 async-signal-safewrite系统调用。任何违反都会引入未定义行为或死锁风险。

实现逐行解读

处理器位于 main.rs 的setup_signal_handlers()(仅 Linux 编译):

unsafe extern "C" fn handle_term_signal(sig: libc::c_int) { let receipt: &[u8] = match sig { libc::SIGTERM => b"{\"event\":\"smol_hotfork_agent_signal\",\"signal\":15}\n", libc::SIGINT => b"{\"event\":\"smol_hotfork_agent_signal\",\"signal\":2}\n", _ => b"{\"event\":\"smol_hotfork_agent_signal\",\"signal\":\"unexpected\"}\n", }; let _ = libc::write(libc::STDERR_FILENO, receipt.as_ptr().cast(), receipt.len()); libc::sync(); libc::_exit(0); }

关键设计点:

  1. 固定有界 JSON:三种信号各自对应一条编译期常量字节串,&[u8]直接指向静态数据,零分配、零格式化;
  2. 既有 stderr 通道:FD2 是既有的 krun-stderr / agent-console 通道,事件在sync()和退出之前发出,保证证据先行落盘;
  3. 顺序固定:write → sync → _exit。信号到达时,同步文件系统(sync()是异步信号安全函数)后再干净退出;
  4. 不安装 SIGCHLD 处理器:源码注释明确指出,安装它会在同步 exec 路径上与Child::wait()竞争,后台 exec 子进程改由 accept 循环中周期性调用的reap_background_children()回收。

观测性边界:缺失不等于没发生

INTENT.md 特别声明了证据的语义边界:"short/error writes may lose that evidence; absence is not proof no signal happened"(短写或错误写可能丢失该证据;证据缺失不构成"信号未发生"的证明)。也就是说,这是一条观测性诊断通道,而非事务性保证;关闭语义(shutdown semantics)保持不变,诊断是观测性的,不是 resolver 修复的前提,也不构成对 fork 修复的声称。调度器应把该事件当作辅助证据,而不是硬性契约。

验证:编译式测试套件

运行方式

两个测试均位于仓库根目录 tests,按 INTENT.md 给出命令从仓库根目录本地运行:

python3 -B -Werror -m unittest discover -s tests -p 'test_agent_persistent_resolver.py' python3 -B -Werror -m unittest discover -s tests -p 'test_agent_signal_receipt.py'

-B禁止写入__pycache__,-Werror把警告升级为错误,保证测试环境干净严格。

测试原理:编译真实源码片段

两个测试的共同特点是不做黑盒集成,而是从真实源码中精确提取目标函数,拼接固定的文件系统/syscall 夹具后交给rustc编译成可执行探针:

  • test_agent_persistent_resolver.py 从crates/smolvm-agent/src/storage.rs提取execute_or_remount、overlay_resolv_conf_contents、refresh_overlay_resolver(若存在),用rustc --edition 2021 -A unused编译;夹具用CASE_MODE=reuse|remount环境变量控制is_mountpoint的判定,模拟已挂载复用与重挂载两条路径;
  • test_agent_signal_receipt.py 从crates/smolvm-agent/src/main.rs提取handle_term_signal函数体,并用一个桩libc模块替换write/sync/_exit,从而在普通用户态验证信号处理器的精确执行顺序。

Resolver 测试的四组用例

测试方法场景断言
test_virtio_to_tsi无显式 DNS(默认兜底),reuse 与 remount 两种模式merged 层resolv.conf为8.8.8.8+1.1.1.1;etc/unrelated字节原样保留、etc/hosts不变
test_explicit_override设置SMOLVM_NETWORK_DNS=9.9.9.9写入nameserver 9.9.9.9,显式覆盖生效
test_filter_precedence9.9.9.9加SMOLVM_DNS_FILTER=1过滤优先,写入127.0.0.1
test_write_failure_loud将 resolv.conf 替换为目录(写必失败)进程非零退出,stderr 出现resolver warning,失败可闻

此外,storage.rs 内还内嵌同名单元测试,覆盖DNS_FILTER→127.0.0.1、virtio-net 下使用100.96.0.1、TSI 下使用自定义100.100.100.100、无配置时回落公共解析器四条内容决策路径。

信号测试的精确断言

test_agent_signal_receipt.py 的核心断言是 stderr 输出的逐行、逐字段顺序:

[{'event':'smol_hotfork_agent_signal','signal':sig}, {'step':'sync'}]

即对信号 15(SIGTERM)、2(SIGINT)与意外信号 99,处理器必须依次输出一条固定 JSON 事件和{"step":"sync"},sync步骤严格位于事件之后——这正是"先发证据、再同步退出"的次序约束的机器可验证版本。

修补前后对比

按 INTENT.md 记录:修补前,当前上游 commit013f877存在8 个 resolver 用例失败与 3 个信号用例失败;修补后全部通过。修复前后的失败/通过对比是这两组测试的核心价值——它们把"复用/重挂载要刷新 resolver"与"信号处理器有界输出"这两条行为契约固化为可回归的机器检查。

发布纪律与版本一致性

INTENT.md 末尾明确了本次发布的工程纪律,这些信息对下游构建者至关重要:

  • 精确前向应用:源码补丁是对已评审私有 delta 的精确前向应用(exact forward application);当前上游存储因包含无关的后续改动,其完整文件哈希与先前built659c源码不同,但被修改的函数逐字一致;
  • 不触碰任何既有产物:本次源码发布不改变任何既有二进制、provenance 戳或运行时闭包(runtime closure);
  • 不夸大范围:resolver 测试夹具允许未使用的 fixture 声明,因此这不是"完整 agent 零警告"的声明;既有私有 agent 构建仍保留两个libc::time_t弃用警告;
  • 独立验证仍待进行:完整当前上游 agent/runtime 构建与跨平台资质认证(cross-platform qualification)仍是独立事项,本文所述修复已通过上述专项测试,但不代表整仓构建状态。

已知限制与后续工作

  • 平台范围:信号处理器以#[cfg(target_os = "linux")]编译,非 Linux 平台为 no-op 桩(见 main.rs),本修补的验证集中在 Linux 路径;
  • 覆盖范围:resolver 刷新仅针对持久化 overlay 的etc/resolv.conf;TSI 路径的 in-guestresolv.conf修复(tsi_resolv_conf)遵循同一 DNS 决策逻辑,但属于另一条执行链;
  • 证据强度:关闭信号证据是观测性的,短写/错误写可能丢失;对证据缺失的语义解释(不构成"信号未发生"的证明)应在机群监控告警设计时显式纳入,避免误报;
  • 后续验证:完整的 agent/runtime 构建与跨平台认证仍在 仓库测试体系 之外单独推进,落地本修补时应以上游最新构建结果为准。

综上,这两处修正以最小、有界的源码改动,解决了热备用机群中最容易"静默变坏"的两类问题——持久化 overlay 的 DNS 漂移与关闭路径的不可观测性,并通过编译式测试把行为契约固化为回归保障,是理解 smolvm-agent 持久化与生命周期设计的高质量切片。

  • 虚拟化
  • AI Agent
  • 人工智能
  • CLI

【免费下载链接】smolvm

An embeddable, portable, branchable virtual machine to safely run Agents locally.

项目地址:https://gitcode.com/gh_mirrors/sm/smolvm
点击查看免费下载

相关推荐

上一篇:前端系统设计面试中如何展现面试官评分看重的信号(需求探索、架构、权衡)?
下一篇:ERNIE-4.5-300B-A47B-Base-Paddle语义搜索:向量数据库集成指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询