线上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 完整性再入缓存),并提供"按包清缓存"的快速通道------这次的修复动作就是清缓存,如果常态化,此类故障的恢复时间可以从"跨组扯皮几天"缩短到几分钟。
相关推荐
时间的情敌2 小时前
《回顾 2019 前端:React Hooks 问世,前端工程化爆发的一年》
前端·react.js·前端框架
燕落南枝2 小时前
CSS 类选择器详解:从入门到实战
前端·css
tryCbest3 小时前
Vue3——第 7 篇:组件基础——从注册到使用的完整流程
前端·javascript·vue.js
ai工具集合3 小时前
图标集用法,前端项目 SVG 图标 icon 完整接入实操
前端
门思科技3 小时前
ThinkLink 触发模型与告警规则实战:数据一小时才来一次,告警怎么做到不迟到、不刷屏、找对人
前端·后端
hunter_0763 小时前
从零读懂 SwiftUI 新拖放 API:用 Apple 的纸牌示例理解排序、多项拖拽与移动规则
前端
时间的情敌3 小时前
Electron 设置-webkit-app-region: drag;实现拖拽后, 无法响应鼠标划过事件及点击事件的解决方式
前端·vue.js
程序猿阿森3 小时前
深入解析 NLI:RAG 与大模型防幻觉的“最后一道防线”
前端·人工智能·算法
iCxhust5 小时前
模型:500kW飞轮 10kV输电线路 油田抽油机 同时考虑无功补偿 统计:全年节电量 结果56万度电
前端·css·css3·电力·飞轮