- 虚拟化
- AI Agent
- 人工智能
- CLI
【免费下载链接】smolvm
An embeddable, portable, branchable virtual machine to safely run Agents locally.
导读
本文围绕 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 授权了这些源码修正。
在此之前存在两类隐患:
- DNS 配置随网络后端漂移:overlay 首次创建时写入的
resolv.conf反映当时的网络模式;当 agent 以 virtio-net 或 TSI(无 virtio 的 tap/serial 后端)复用同一持久化 overlay 时,旧文件不会自动更新,导致容器内 DNS 查询走错服务器。 - 关闭路径不可观测: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=1 | nameserver 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); }关键设计点:
- 固定有界 JSON:三种信号各自对应一条编译期常量字节串,
&[u8]直接指向静态数据,零分配、零格式化; - 既有 stderr 通道:FD2 是既有的 krun-stderr / agent-console 通道,事件在
sync()和退出之前发出,保证证据先行落盘; - 顺序固定:
write → sync → _exit。信号到达时,同步文件系统(sync()是异步信号安全函数)后再干净退出; - 不安装 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_precedence | 9.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.
相关推荐
Celery 4.4 系列变更深度解析:新信号体系、结果后端扩展与关键可靠性修复
Celery 4.4 系列变更深度解析:新信号体系、结果后端扩展与关键可靠性修复 本文以 Celery 仓库中的 变更历史文档 https://link.git
任务调度后端消息队列EMQX `cluster.hocon` 配置持久化可靠性改进:原子写入、fsync 与容错备份机制深度解析
EMQX cluster.hocon 配置持久化可靠性改进:原子写入、fsync 与容错备份机制深度解析 本文聚焦 EMQX 中集群级配置持久化文件 clust
后端物联网消息队列通信DeepSeek Harness 有界可升级的信号关闭(Signal Shutdown)机制解析:从 OTel 挂起到进程级兜底的完整修复
DeepSeek Harness 有界可升级的信号关闭(Signal Shutdown)机制解析:从 OTel 挂起到进程级兜底的完整修复 导读 DeepSee
人工智能AI AgentAgent 框架DeepSeek
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考