线上CI流水打包失败排查思路案例

一次 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']

信息量很大:

  1. windows job 只负责编译 Controller 这一个 web 模块 ,编译命令是 npm cache clean --force & npm set registry ... & npm i --force & npm run build project=elec;
  2. 11:12:26 开编,11:18:02 失败,exit status 1,跑了约 5 分半;
  3. 详细的 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 的改进建议(防止再被同类问题坑)

  1. makecmd 里 & 改为 &&:npm i 失败立即中止,不会再出现"装了一半继续 build"产生的误导性报错;
  2. 去掉每次构建前的 npm cache clean --force:缓存命中能大幅减少全量下载,天然抗网络抖动;
  3. 镜像站侧:对代理缓存增加完整性校验(如下载后校验 tgz 完整性再入缓存),并提供"按包清缓存"的快速通道------这次的修复动作就是清缓存,如果常态化,此类故障的恢复时间可以从"跨组扯皮几天"缩短到几分钟。
相关推荐
卸任39 分钟前
Vibe coding工具使用心得
前端·ai编程
brien91 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance2 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M2 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写2 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李2 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易2 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马2 小时前
前沿视界
前端
kyriewen2 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易3 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员