2026年10月8日上午8点半左右,告警群里开始出现“沙箱任务初始化超时”的消息,紧跟着是第二波,第三波,十几条消息刷下来我才意识到不是某个任务偶尔抽风,而是承载 Codex 引擎的那批 Windows 沙箱集体罢工了。
Codex 是我内部负责的一个代码行为验证平台,简单说就是用户提交一段脚本或者可执行文件,系统会在一个干净的 Windows 环境里跑一遍,记录文件操作、进程树、注册表访问这些行为,最后输出一份结构化报告。为了保证分析过程不影响宿主机,执行环境落在 Windows 自带的沙箱里。Windows 沙箱本来是个挺省心的功能,轻量、用完即毁、无需额外授权,但它本质上还是一台轻量级虚拟机,底层依赖 Hyper-V,一旦系统层面有什么风吹草动,它比普通应用更容易先崩。
这篇文章我会把这次故障从发现、排查、定位到修复的完整过程梳理一遍,不绕弯子,直接讲我根据现场信息做的判断、踩过的坑,以及最后沉淀下来的一套 Windows 沙箱巡检与应急方案。如果你也跑 Windows 沙箱,或者你的业务也依赖这种“用完就丢”的隔离环境,这篇复盘应该能帮你提前避掉至少一半类似的问题。
1. 故障概述:时间线、现象与影响范围
先把事故发生当天的关键时间点列出来,方便后面讲排查逻辑时对得上号。
1.1 事件时间线
整个故障从异常出现到最终恢复,大约持续了 4 小时 20 分钟。
当天凌晨 1:07,Windows 更新通过自动更新通道安装了一批补丁。补丁安装完成之后,系统在静默状态下执行了重启。这批补丁不是我们的例行运维窗口,属于系统自动策略触发的行为,所以当时并没有任何人注意到版本变更。
早上 8:11,第一台任务节点上的沙箱启动失败,Codex 调度器把它标记为“初始化错误”。由于调度器没有设置快速失败阈值,后续任务继续被分配到了同一批宿主机上,空闲的沙箱实例开始逐个被占满,新的请求全部排队。
8:24,刚刚开始排队的任务客户端集体报出“等待沙箱分配超时”,这部分任务直接判定失败,反馈到业务侧就是“自动回归用例无法执行”。与此同时,宿主机上的 Windows 事件日志不断写入 Hyper-V-VMMS 错误,但普通业务日志里只留下了一句笼统的“沙箱初始化失败”,没有更多细节。
9:02,我介入时,现有任务池里已经有 342 个任务因为长时间等待被中断,后台积压任务数上升到一千出头。业务影响主要集中在三类场景:自动化测试用例执行、第三方代码行为采集作业,以及培训环境里的演示任务。
1.2 影响范围说明
这里需要说清楚一个容易误解的点:这次故障不是因为代码本身有问题,也不是 Codex 引擎的业务逻辑出了 bug,而是承载所有执行工作的 Windows 沙箱层在批量启动时集体失败。换句话说,但凡是那段时间抓取到“沙箱分配”这一步的任务,无一例外全部卡死。
故障期间我们做过快速验证,尝试手动创建一个沙箱实例,结果同样失败。这就把怀疑范围从 Codex 内部调度缩小到了 Windows 沙箱本身,后面的排查方向也就随之确定下来。
2. 我对 Windows 沙箱的定位逻辑与系统结构拆解
排查前,我先花两分钟把 Codex 为什么依赖 Windows 沙箱,以及沙箱本身在系统里是怎么运作的重新梳理了一遍。这不是浪费时间,后面的每一步判断都依赖这套底层的理解。
2.1 Codex 为什么最终选了 Windows 沙箱
早期 Codex 用过两种隔离方案:普通虚拟机镜像和容器。虚拟机的优点是隔离彻底,但启动要 40 秒以上,每次做快照、恢复、重置的环境准备周期太长,根本扛不住高频的分析任务。容器启动虽然快,可在 Windows 生态里对 GUI 程序、COM 组件和注册表行为的覆盖面不够理想,很多待分析的样本会需要真实桌面环境。
Windows 沙箱正好卡在一个中间位置。它用 Hyper-V 做虚拟机隔离,每次启动都从一个只读基础镜像动态创建可写副本,关闭时整个环境直接销毁,不留下状态残留。对 Codex 这种“跑一次、记录一下、然后丢弃”的短时任务非常合适。
2.2 Windows 沙箱的核心组件
内部实现上,Windows 沙箱并不是一个普通的常规虚拟机,它是一套整合了 Hyper-V 平台、虚拟化管理服务和一个轻量级桌面的组合体。几个处于故障关键路径上的组件:
Hyper-V 虚拟机管理服务(vmcompute):所有虚拟机的创建、启动、停止操作都由它统一调度。如果这个服务不在运行状态,沙箱基本上不可能正常起来。
Windows 沙箱基础镜像:系统首次启用功能时会在系统分区生成只读镜像文件,沙箱后续启动都基于它复制。这个镜像损坏或者被占满,就会出现启动失败。
临时可写磁盘:沙箱每次运行都需要一块临时虚拟磁盘保存运行时产生的文件。它存储在用户的沙箱工作目录下,通常在系统盘。这个目录如果磁盘空间不足,沙箱会在启动检查阶段直接失败。
Windows 沙箱配置管理器:负责读取 wsb 配置文件,把映射文件夹、启动命令、网络开关等参数传递给虚拟化层。
用一句话概括就是:沙箱启动过程类似“拆一间一次性样板房”。基础镜像相当于房屋图纸,临时虚拟磁盘相当于建材,vmcompute 服务相当于施工队。图纸有问题、建材不够、施工队罢工,房子都盖不起来。
2.3 本次故障涉及的几个关键路径
这次所有失败节点都集中在同一个阶段:沙箱启动后的“环境准备期”。具体来说,Codex 向 vmcompute 发出创建沙箱实例请求,vmcompute 开始加载基础镜像并尝试分配可写虚拟磁盘。这个环节有四个独立前置条件需要同时满足:
存在足够大小的系统盘剩余空间。
基础镜像和现有工作目录没有被其他进程锁定。
vmcompute 服务处于正常启动状态,且无其他异常占用。
Hyper-V 平台相关功能没有被系统更新或安全策略禁用。
这四个条件只要有一个不满足,沙箱就会抛出启动失败。排查的难点在于,表面报错未必直接指向真正的坏条件,往往需要逐层排除。
3. 排查实录:从虚假错误码到真正证据链
下面这一段是整个复盘里最值得记录的部分,因为它充分说明了“日志告诉你什么”和“真实发生了什么”完全是两码事。
3.1 第一轮:现象仍然模糊,错误码具有误导性
我第一次登录宿主机时,Codex 平台的日志里只记录了一行“Windows Sandbox 启动进程返回失败,错误代码 0x80370102”。看到这个代码的第一反应是去查常规解释,很多资料会直接把 0x80370102 解释为“虚拟化功能未启用”,常见于 WSL 或者老版本 Hyper-V 环境中。
但这台宿主机之前一直能正常创建沙箱,Hyper-V 功能和“虚拟机监控程序”设置都是正常的。如果在状态没变的情况下突然报出这个错误,就不能直接套常规解释,必须去确认系统层是否发生了某些变化。
紧接着我查了系统服务列表,发现 vmcompute 竟然处于阻塞状态。进一步查事件日志,Hyper-V-VMMS 记录了一条关键错误,内容大致是“虚拟机总线(VMBus)断开连接,沙箱主机进程未能及时响应”。
这个时候我基本可以确定,错误码只是表象,真正的核心问题是沙箱与 Hyper-V 之间的通信链路出问题了。
3.2 第二轮:通过事件日志和系统变更还原现场
我在排查过程中一直强调一条原则:先把系统的“历史变更”查清楚,再去琢磨怎么修复。如果不知道系统在哪里变了,即使修好了这次,下次大概率还会在同样的地方翻车。
这次事件日志里留下了两条极为关键的信息:
第一条是当天凌晨 1:07 系统完成了一次 Windows 更新,更新内容涉及虚拟化平台组件。
第二条是 8:11 前后,沙箱进程尝试启动时,事件日志同时出现了 VHD 文件访问冲突的记录,具体指向沙箱基础镜像文件被其他进程锁定。
顺着第二条信息去查是什么进程锁定了镜像,最后锁定了 Codex 自身未被释放的一个残留进程。因为凌晨更新完成后系统自动重启,部分 Codex 引擎的旧进程并没有被正确终止,而是带着对虚拟磁盘文件的句柄一直留在后台。这个句柄导致后续的沙箱启动请求在访问同一个镜像时直接冲突。
3.3 第三轮:并发调整和磁盘空间的双重放大
到这里我还没有停,因为还需要解释另一个现象:为什么故障集中在某一个时间点,而不是从凌晨 1 点就立刻爆发。
查了调度器配置之后发现,当天早上 7:50 有人把沙箱并发数从 4 调到了 8。这个调整本身在 Codex 层面很常规,但它直接放大了底层资源的压力。
系统盘当时的剩余空间约 2.4GB,而每个沙箱启动过程需要创建约 1.2GB 的临时可写磁盘。并发数从 4 变为 8 之后,理论上最大需要接近 10GB 空间,远远超过了系统盘剩余的 2.4GB。更巧的是,第一个沙箱启动时因为旧进程持有镜像文件句柄而失败,后续任务又不断重试,把残余空间也慢慢占满。最终呈现出的结果就是:所有新沙箱都无法创建,任务大面积排队。
所以真实原因链可归纳为三层:
直接引爆点是扩展后的并发数创建了大量临时磁盘请求,而系统盘空间不足支撑这些请求。
阻断启动的关键因素是残留进程锁住了基础镜像,导致即便有空间,沙箱也无法正常复制镜像。
隐藏的放大器是 Windows 更新触发的系统重启,让本应被清理的旧进程变成了“僵尸占用者”。
3.4 排查过程中用到的关键命令
这里列几条对定位类似问题有直接帮助的命令。每次排查时我会按顺序执行,效率比较高。
# 查看 Hyper-V 相关服务状态 Get-Service vmcompute, vmms, hvhost # 查看系统启用的虚拟化功能 Get-WindowsOptionalFeature -Online | Where-Object {$_.FeatureName -match 'Hyper|Containers|Sandbox'} # 查看沙箱基础镜像与工作目录位置及大小 Get-ChildItem C:\Users\Public\Documents\Sandbox -Recurse -ErrorAction SilentlyContinue Get-ChildItem C:\Windows\Sandbox -Recurse -ErrorAction SilentlyContinue # 查看是否有残留进程正在占用沙箱目录 Get-Process | Where-Object {$_.Path -match 'Sandbox|Codex'}这是一个“先查服务,再查功能,再查目录占用,最后查进程”的固定顺序。不少同行喜欢直接去翻 Windows 事件查看器,但事件查看器里信息量太大,不配合服务状态和文件占用情况,很容易把时间花在无关的日志里。
4. 根因确认与一键修复:如何一步步恢复服务
根因确认之后,修复本身并不复杂,真正复杂的是决定“修哪一步、按什么顺序修”。
4.1 根因链条整理
我在故障报告里把根因分成三层,方便后续做整改立项:
- 直接原因:沙箱临时虚拟磁盘创建失败,系统盘剩余空间不足以支持 8 个并发沙箱实例。
- 次要原因:凌晨 Windows 更新后的重启未清理 Codex 残留进程,残留进程锁定基础镜像,导致沙箱初始化在复制镜像阶段就无法继续。
- 管理原因:并发参数调整前没有评估宿主机的磁盘与内存余量,也没有为沙箱基础镜像和工作目录设置自动清理策略。
4.2 为什么按“先清理再扩容”的顺序执行
修复过程中我刻意没有一上来就调低并发数。因为即使把并发数调回去,残留进程锁镜像的问题依然存在,下一次只要再有任务高峰,故障还会复现。正确的做法是先释放掉被占用的资源,再调整配置,最后验证并发能力。
具体步骤是这样的:
第一步,找出锁住镜像进程并终止它们。这里要特别小心,不能直接用任务管理器一把梭杀进程,而是先确认进程是否真的属于 Codex 引擎的残留部分,避免误杀正在运行的其他在线任务。
Get-Process | Where-Object {$_.Path -match 'Codex'} | Select Id, ProcessName, Path在确认列表里没有任何正在处理业务的任务后,对选中的进程执行 Stop-Process。
第二步,清理沙箱工作目录里积累的历史临时文件。运行了半年没有清理的目录里,积压的旧虚拟磁盘总大小已经接近 11GB。一次性删掉没问题,因为这些历史文件对于已结束的任务已经没有保留价值。
第三步,重启 vmcompute 服务,确认真实状态。有时候服务虽然在运行列表里,但内部状态已经异常,重启一次能解决很多“隐藏故障”。
Restart-Service vmcompute -Force Get-Service vmcompute第四步,调整 Codex 并发数,从 8 暂时降为 4,先恢复基本可用。这一步在完全清理磁盘之前不要做,不然清理后剩余空间只能满足低并发,再之后你会发现系统很快就又被占满。
第五步,验证沙箱实例能手动创建,并且连续跑 20 个任务无失败后,再把并发数逐步提升到 6,观察稳定后再恢复到 8。
这个顺序看起来平淡无奇,但顺序如果反了,就很容易陷入“修好了一会又崩”的循环。我先见过很多次运维同事遇到这类问题,一上来就降并发,结果空间仍然不够,任务还是失败,白白浪费一上午。
4.3 修复后的回归验证
当天 12:41,所有沙箱任务恢复执行,积压队列在一小时内消化完毕。为确认修复不是“碰运气”,我额外做了一轮回归测试:连续提交 200 个短时任务,让它们全部跑满并发,同时监控系统盘剩余空间曲线,确认空间消耗与预期一致后,才将故障状态改回“已解决”。
这里特别提醒一点:修复完成后不要立刻关闭对日志的观察,至少要盯住系统盘剩余空间和 vmcompute 服务的运行时间。很多 Windows 沙箱问题在修复后初期会表现正常,但会在下一轮高并发或者下一次更新重启后再次冒出来。
5. 故障后整改:监控与日常巡检建议
复盘会议开完之后,我提了三个整改项,都是低成本但高收益的改动。
5.1 沙箱临时文件必须要有自动清理机制
Windows 沙箱本身不会主动清理历史工作目录,默认情况下这些临时虚拟磁盘会一直堆在系统盘里。像我们这种高频使用场景,两周不清理就能攒出 10GB 以上垃圾数据。
我后来补了一个计划任务,内容非常简单:每周日凌晨低峰时段,删除沙箱工作目录里超过 48 小时未修改的临时文件。同时设置一个告警阈值,当系统盘剩余空间低于 5GB 时,直接通过监控平台推送通知,不给它悄悄增长到危险区的机会。
5.2 并发数调整前先做资源预算
这是一个很容易被忽视的点。调整并发数会直接改变沙箱占用的磁盘空间,但多数团队的资源评估还是会习惯性地只盯着 CPU 和内存。Windows 沙箱特殊就特殊在磁盘占用是按倍数叠加的,一个沙箱 1.2GB,8 个就是接近 10GB,不考虑这个变量必然出事。
我把原来的“并发数调整流程”里加了一个前置检查项:新增并发前先计算所需磁盘空间、系统盘剩余空间、预计峰值占用,并将这些数据写进变更审批备注里。虽然听起来多了一步操作,但它能挡住很多本不该发生的故障。
5.3 更新后的重启检查必须纳入运维动作
这次故障里,Windows 更新自动重启是隐藏的背景板。更新本身未必直接导致故障,但它会改变进程生命周期,让一些平时会被自然回收的残留进程一直存活,进而影响到沙箱启动。
我建议所有有沙箱宿主机在收到更新通知后,尤其是更新涉及虚拟化、Hyper-V、Windows 沙箱组件时,第一时间检查三件事:系统是否已重启、vmcompute 服务是否正常启动、沙箱基础镜像文件是否能正常访问。这三件事用一条批处理脚本就能覆盖,但能防住大量类似故障。
另外,如果条件允许,Windows 更新导致的宿主机重启尽量安排在业务低峰时段,并提前做一次沙箱实例释放操作,避免更新期间仍有任务挂在沙箱环境中。
6. 常见 Windows 沙箱启动失败问题速查
复盘过程中我整理了几个高频错误,先列出来方便以后检索。
| 错误码/现象 | 常见含义 | 首要检查项 |
|---|---|---|
| 0x80370102 | 虚拟化平台或 Hyper-V 服务异常 | vmcompute 与 vmms 服务状态、基础镜像完整性 |
| 0x80070070 | 磁盘空间不足 | 系统盘剩余空间、沙箱工作目录大小 |
| 0x800f0954 | 更新后沙箱组件不可用 | Windows 更新是否完成、功能状态是否启用 |
| 0xc0000022 | 访问被拒绝 | 沙箱工作目录权限、基础镜像文件 ACL |
| 启动后立即退出 | 配置或环境初始化失败 | wsb 配置文件的 LogonCommand 路径是否存在、网络配置是否合理 |
表格看起来简单,但对应到实际排查的时候一定要记住:错误码只告诉你“在哪一步失败”,并不会告诉你“为什么失败”。同一个错误码可以由五种完全不同的原因触发。所以参考表格定位了大方向之后,还是要回到事件日志和进程句柄排查,把现场证据补齐,不要急着凭经验下定论。
7. 写在故障恢复后的几点心得
这次 Windows 沙箱故障如果写成一句话,就是“高并发叠加磁盘空间不足,配合残留进程锁镜导致的连锁故障”。但真正让我反思的并不是技术链条本身,而是这个故障背后的“慢变量”:一个系统跑了半年,沙箱工作目录一直没人清理,并发参数一直没人复核,监控阈值一直没人设置,直到某次普通的更新重启把所有慢变量一次性引爆。
我自己在后续维护里养成了一个习惯:每次遇到沙箱类故障,不管报错多花哨,先做三件事,看服务状态,看磁盘余量,看进程占用。这三步能解决掉至少七成的问题,剩下的再深入事件日志。
还有一个值得单独拿出来说的经验:Windows 沙箱的配置文件(wsb)里如果配置了 LogonCommand,一定要确认这个命令依赖的文件路径不会随系统版本变化而变化。我们经过这次故障后,把所有沙箱里的启动脚本统一放在了固定管理端,不再依赖沙箱内部路径,降低了很多不确定性。
如果你也在用 Windows 沙箱跑高频短任务,建议顺手检查一下沙箱工作目录的体积和系统盘剩余空间,并且认真考虑加一个每周清理任务。等到故障真正发生时再处理,成本通常比提前预防高出十倍不止。希望这篇复盘能帮你避开同样的问题。