一次 CI 打包失败的完整排查:从"找不到模块"到"网络链路断连"
记录一次 weci 流水线打包失败的排查全过程。最终结论有两层反直觉:一是报了半天的
Cannot find module 'compressing'根本不是病因,只是并发症 ;二是我一度用对照实验把嫌疑锁定在"构建节点到镜像站的网络链路",最终证实的根因却是镜像站代理缓存里几个包的 tgz 副本损坏 ------清缓存后一次通过。这个过程让我深刻体会到:排查问题时最大的敌人不是错误本身,而是被表象报错和看似充分的中间结论带偏方向。
一、问题背景
我们的项目通过 Jenkins + weci 流水线做整体打包,流程分三层:
编译层:ZW_DH3.RD010000_WEBController_KA_linux (Linux 节点,编固件)
ZW_DH3.RD010000_WEBController_KA_windows (Windows 节点,编 web 前端 H5)
↓ 产物拷贝
打包层:ZW_DH3.RD010000_WEBController_KA_package (汇总打包出 SC_Dist 包)
某天打包 job #371 失败,我负责排查。手头只有一份打包 job 的控制台日志。
二、第一层排查:打包日志里的"假终点"
打开 package job 的日志,搜索 ERROR,最先看到的是:
11:35:10 ERROR [package.py line:98] [dist/windows] => [handle_dist] stage compile failed
11:35:27 ERROR [build.py line:563] [**TechBuild] dependent module build failed
日志里还有一段 STATUS 汇总:
"Main": "FAILED,2026-08-27 11:30:51,2026-08-27 11:34:58,,2026-08-27 11:35:10"
这里我做了第一个关键判断 :STATUS 里 Main 的时间字段是 FAILED,下载开始,下载结束,,打包时间------注意中间编译字段是空的 (,,)。再回看日志开头:
Started by upstream project "ZW_DH3.RD010000_WEBController_KA" build number 373
Copied 10 artifacts from "ZW_DH3.RD010000_WEBController_KA_linux" build number 373
Copied 8 artifacts from "ZW_DH3.RD010000_WEBController_KA_windows" build number 373
结论:这个 job 是 --stage package 纯打包 job,根本不编译 。它只是检查了上游编译阶段的产物状态。stage compile failed 说明上游编译挂了,真正的报错不在这份日志里。
另一个细节佐证了方向:linux job 拷回 10 个 artifacts,windows job 只拷回 8 个 ------windows 编译没出全产物。且后续流程中 dist/linux 分支正常走完,dist/windows 分支报错中止。
方法论小结 1:流水线失败时,先搞清楚"这个 job 在流水线里扮演什么角色"。错误日志出现在哪,不代表错误发生在哪。
三、第二层排查:追到 windows 编译 job
打开 ZW_DH3.RD010000_WEBController_KA_windows #373 的控制台日志,搜 ERROR:
11:12:26 INFO [builder.py line:122] [Controller__cfd607] native compile command <makecmd>:
npm cache clean --force & npm set registry http://npm.**.com/ & npm i --force & npm run build project=elec
11:12:26 INFO [builder.py line:191] [Controller__cfd607] nuw build start
11:18:02 ERROR [builder.py line:194] [Controller__cfd607] compile failed, exit status: 1
| error info: artifacts/ProcessLogs/Module_Controller__6a1c67a6__cfd607.log
11:18:19 ERROR [build.py line:598] [**TechBuild] dependent module handle failed: ['Module_Controller__6a1c67a6__cfd607']
信息量很大:
- windows job 只负责编译 Controller 这一个 web 模块 ,编译命令是
npm cache clean --force & npm set registry ... & npm i --force & npm run build project=elec; - 11:12:26 开编,11:18:02 失败,exit status 1,跑了约 5 分半;
- 详细的 npm 输出被单独写到
artifacts/ProcessLogs/Module_Controller__6a1c67a6__cfd607.log------这才是我要找的文件。
同时我注意到环境变量里 windows_path 解 base64 后是:
bat
set CI=false
set Path=C:\Program Files\node-v16.17.0-win-x64;%Path%
即节点用 Node 16.17.0 构建。这排除了我之前在另一个项目踩过的"Jenkins 节点 Node 10 不支持 ?. 语法"的坑------历史经验帮我快速排除了一个方向,但也提醒我不要路径依赖,这次的失败形态完全不同。
方法论小结 2 :CI 框架通常会把"框架日志"和"编译器输出"分离。看到
error info: xxx.log这种指引时,务必拿到那份文件,否则分析永远停在表面。
四、第三层排查:ProcessLogs 里的真凶与假象
拿到 Module_Controller__xxx.log 后,整个失败现场终于完整了。这份 306 行的日志可以分成四段:
第 1~2 行:执行的命令
npm cache clean --force & npm set registry http://npm.**.com/ & npm i --force & npm run build project=elec
第 3~266 行:npm i 正常工作(全是噪音)
两百多行 npm WARN:peer dependency 冲突被 --force 强制覆盖、老包 deprecated 提示......这些都不是错误,说明依赖树解析正常,正在下载阶段。
第 267~278 行:真正的失败点 ★
npm ERR! code ECONNRESET
npm ERR! syscall read
npm ERR! network request to http://npm.**.com/postcss/-/postcss-7.0.39.tgz failed, reason: read ECONNRESET
下载 postcss-7.0.39.tgz 时 TCP 连接被重置,npm i 中止------依赖只装了一半。
第 280~304 行:次生报错(最大的误导)
> node scripts/build.js
Error: Cannot find module 'compressing'
Require stack:
- ...\code_path\Controller__1d9a64\scripts\build.js (build.js 第 16 行)
看到这里我恍然大悟。makecmd 里的命令连接符是 &(cmd.exe 语义:不管前一条成败都继续执行) ,而不是 &&。所以 npm i 失败后,npm run build 照样启动,scripts/build.js 第 16 行 require('compressing') 时 node_modules 里根本没这个包,报 MODULE_NOT_FOUND。
"找不到 compressing"不是病因,是并发症。
更巧的是,我翻 git 历史发现这个项目有"前科":
bf4e221d 打包失败问题,找不到compressing,将npm.**.com改回npm
62c1d703 Revert "...找不到compressing..." 不需要修改镜像,可能网络不稳定
之前的人也踩过一模一样的坑,也被 compressing 这个表象误导去改了 registry,最后又 revert。历史考古验证了我的判断:这是反复出现的网络问题。
方法论小结 3 :报错信息指向的模块(compressing)往往只是"第一个撞到问题的无辜者"。要沿着因果链往上追:为什么模块会缺失?→ 因为 npm i 中断;为什么中断后还执行 build?→ 因为
&;为什么 npm i 中断?→ ECONNRESET(Connection Reset)。
五、第四层排查:区分"包的问题"和"链路的问题"
一个自然的疑问是:会不会是 postcss 这个包本身坏了?我对比了三次失败的日志:
| 时间 | 断在哪个包 | 错误类型 |
|---|---|---|
| 10:24 | postcss-7.0.39.tgz | read ECONNRESET |
| 10:58 | yargs-13.3.2.tgz | socket hang up |
| 11:26 | p-finally-1.0.0.tgz | read ECONNRESET |
每次断的包都不一样------如果是某一个包坏了,会每次死在同一个包上;断点随机漂移,说明不是单个包的问题。
接着我从办公机对 registry 做了对照测试:
bash
curl.exe -s -o NUL -w "%{http_code} %{size_download}B %{time_total}s\n" http://npm.**.com/yargs/-/yargs-13.3.2.tgz
# 连续 3 次:200 63582B 0.03s / 200 63582B 0.02s / 200 63582B 0.03s
registry 服务正常、秒级下载。至此可以画出初步的责任边界:
办公机 ──→ npm....com ✓ 正常(30ms)
构建节点 YC-CXJC-20-NEW ──→ npm.**.com ✗ 一小时内断 3 次
当时我把问题锁定在"构建节点到镜像站的链路"上。但最终被证实的根因更加微妙(见第六节):镜像站代理缓存里这几个包的 tgz 文件是坏的。办公机能下载成功,是因为镜像站是多层缓存架构,办公网请求命中的是健康的缓存节点/路径,而构建节点恰好命中了损坏的缓存副本------这也是为什么"办公机通、节点断"并不能完全排除镜像站。
方法论小结 4(修正版) :对照实验能缩小范围,但要警惕缓存类中间件的"分区健康"假象------同一个 URL,不同来源的请求可能命中不同缓存副本,一边通一边断是可能的。最终定论要以"清除缓存后问题消失"这类修复性验证为准。
方法论小结 4:用"对照实验"划责任边界。同一时刻办公机通、节点断,一下子就把"镜像站背锅"的可能性排除了,避免在错误的方向上浪费各方时间。
六、根因因果链总结
① 镜像站代理缓存中 postcss/yargs/p-finally 的 tgz 副本损坏,
构建节点下载到损坏处连接即被截断(ECONNRESET / socket hang up) ← 根本原因
↓
② npm i 下载中断,依赖只装了一半(compressing 等未安装) ← 直接原因
↓ (makecmd 用 & 连接,不检查前步成败)
③ npm run build 照常启动,build.js:16 require('compressing') ← 放大因素
↓
④ MODULE_NOT_FOUND → exit 1 → 构建标记 FAILED ← 表象
六(续)、最终解决:清理镜像站损坏缓存
排查后期各方曾陷入"踢皮球":镜像组说本地测试通、不是镜像问题;我没有构建节点权限;网络组又没人认领。僵持之下我做了两件事推进:
把证据钉死:三次失败时间、每次断的包不同、办公机对照正常的完整记录,一并抛给相关方。
最终镜像站负责人清理了 postcss、yargs、p-finally 这三个包的错误缓存 ,重新触发构建,一次通过。
回过头看,"每次断的包不同"这个特征有两种解释:一是链路随机中断(我当时的判断),二是恰好有几个包的缓存副本损坏,npm 每次并发下载时先撞到哪个坏包就断在哪------事后证明是后者。办公机 curl 同一 URL 能成功,则是镜像站多层缓存"分区健康"造成的干扰信号。
方法论小结 4(补充) :当故障表现为"下载类操作随机位置中断"时,除了怀疑网络链路,镜像/代理缓存损坏也要列入嫌疑清单------它的特征一模一样,而验证成本极低(让镜像方清一次缓存即可)。修复性验证(修好后问题消失)是终局判据。
七、给 CI 的改进建议(防止再被同类问题坑)
- makecmd 里
&改为&&:npm i 失败立即中止,不会再出现"装了一半继续 build"产生的误导性报错; - 去掉每次构建前的
npm cache clean --force:缓存命中能大幅减少全量下载,天然抗网络抖动; - 镜像站侧:对代理缓存增加完整性校验(如下载后校验 tgz 完整性再入缓存),并提供"按包清缓存"的快速通道------这次的修复动作就是清缓存,如果常态化,此类故障的恢复时间可以从"跨组扯皮几天"缩短到几分钟。