在CI/CD流水线里,Verdaccio 作为私有 npm 仓库,承担着给构建环境提供依赖包的角色。可很多团队会遇到一个让人抓狂的情况:流水线跑到“npm install”这一步,卡住不动,等了几分钟甚至十几分钟,直接报“ETIMEDOUT”或者“ESOCKETTIMEDOUT”。明明本地开发时一切正常,为什么一到流水线里就拉包超时?这篇文章就是用大白话,把这类问题的排查思路和解决方法讲清楚。我不会堆一堆高深术语,尽量让刚接触前端工程化的朋友也能跟着操作。
一、先说说这是个什么事儿
先想一个生活场景。你家里有个储物间,里面放满了各种工具。平时你去拿工具,几步路就到,随手就拿出来了。但有一天,你把储物间的门锁上了,或者把工具挪了个位置,再或者你去拿工具的路上有个大堵车,那结果就是左等右等拿不到东西。
Verdaccio 就是那个储物间,CI/CD 流水线就是那个去拿工具的人。超时无非就是这么几个原因:
- 储物间根本没开门(Verdaccio 挂了或网络不通)
- 储物间开门了,但找工具特别慢(Verdaccio 响应慢,磁盘 IO 或内存问题)
- 路上堵车了(网络延迟、防火墙拦截、代理配置错误)
- 拿工具的人自己心太急(流水线里 npm 超时时间设置得太短)
要解决问题,先别慌,一个一个排查。下面我会结合实际命令和一段 Node.js 示例脚本,带你看完整个处理流程。
二、先从最容易忽略的地方查起
很多情况下,超时不是 Verdaccio 本尊出了问题,而是流水线的网络环境压根就没连上它。比如 Jenkins 跑在 Docker 容器里,容器里的 DNS 解析不到你内网的 Verdaccio 域名;或者 GitLab Runner 所在的机器没有加白名单,防火墙把 4873 端口拦了。
2.1 先看 Verdaccio 的日志
不要一上来就改配置。先去看 Verdaccio 的日志,这是最直观的线索。假设你的 Verdaccio 跑在 Kubernetes 里,可以通过下面的命令看最近一段时间的日志:
# 查看 verdaccio 这个命名空间下所有 pod
kubectl -n verdaccio get pods
# 把名字替换成实际 pod,比如 verdaccio-0
kubectl -n verdaccio logs verdaccio-0 --tail=200
看日志时重点找几个关键词:
ECONNREFUSED:说明请求根本没到 Verdaccio,可能是端口没开。slow request或timeout:说明请求到了,但处理时间太长。GET /package后面跟着502或504:说明 Verdaccio 自己反代上游时出问题了。
如果日志里干干净净,一条请求都没收着,那问题基本出在流水线到 Verdaccio 的网络链路上。这时候可以进流水线的执行机里面去试探一下。
2.2 在流水线机器上手动探测端口
假设你的流水线用的是 Ubuntu 系统,执行机里可以直接用 curl 测试:
# 用 curl 测试 Verdaccio 的 HTTP 接口,-I 表示只看响应头
curl -I http://verdaccio.example.com:4873/
如果这条命令卡了很久才返回,或者直接报错,那就是网络的问题。注意看返回的状态码,如果 200 OK 说明服务正常,那问题多半在 npm 配置上。
三、深入 Verdaccio 配置:不要小看存储和上游
Verdaccio 本身是一个仓库管理工具,它不只是把你推到私有仓库的包存下来,还会代理 npm 公共仓库。它的工作模式是:本地没有的包,就去上游(默认是 https://registry.npmjs.org)下载,然后缓存到本地。这就意味着,拉包超时可能发生在两个环节:
- 从公共上游下载包的时候超时
- Verdaccio 自己的磁盘 IO 太慢,导致本地缓存读取超时
3.1 给上游加超时和重试
Verdaccio 的配置文件 config.yaml 中可以设置上游请求的超时时间。如果你发现大量包需要从公共源头拉取,那么上游的响应速度会直接影响你的流水线。下面是一个实际可用的配置片段,配上我写的注释:
# config.yaml 片段
uplinks:
npmjs:
url: https://registry.npmjs.org
# 连接超时时间,单位毫秒,默认 30000
timeout: 120000
# 最大失败次数,超过这个次数会暂时把上游标记为不可用
max_fails: 5
# 失败之后的重试间隔,单位毫秒
fail_timeout: 5000
# 缓存时间,单位毫秒,告诉 Verdaccio 多长时间内不需要去上游验证包元数据
cache: 300000
packages:
'@*/*':
# 访问任意作用域包
access: $all
# 发布需要登录,这里不展开
publish: $authenticated
# 拉包时如果本地没有,就去 npmjs 上游拿
proxy: npmjs
'**':
access: $all
publish: $authenticated
proxy: npmjs
注意,timeout 并不是越大越好。设得太大,流水线会一直傻等;设得太小,稍微有点网络波动就失败。推荐先用 120 秒试试,然后再结合实际情况调整。
3.2 磁盘 IO 和缓存清理
Verdaccio 把所有本地包都存在磁盘上,默认路径是 /verdaccio/storage。如果你用的是普通的机械硬盘,或者存储目录快满了,那读包的速度会非常慢。流水线里同时有几十个任务去拉包,磁盘 IO 可能瞬间爆满。
一个简单的排查方法:在 Verdaccio 所在机器上执行 df -h 看磁盘使用率。如果超过 85%,要赶紧清理。可以用 Verdaccio 自带的 npm cache clean 吗?不行,那是 npm 客户端的缓存。Verdaccio 的存储清理,需要手动删掉不需要的私有包,或者调整存储策略。
另外,如果你的 Verdaccio 是用 Docker 跑的,看看数据卷是不是挂载在本地磁盘上。如果挂载在一个网络存储上,比如 NFS,那么网络存储的延迟也会导致超时。
四、实战:用 Node.js 写一个拉包探针
排查问题不能光靠感觉。为了确认超时到底发生在哪一层,我建议你写一个简单的 Node.js 脚本,模拟流水线里的拉包动作。这个脚本会做三件事:
- 请求 Verdaccio 的某个包
- 记录总共耗时
- 把耗时输出,方便判断是不是偶发慢请求
下面这段代码直接用 Node.js 内置的 http 模块,不依赖第三方库,放到流水线执行机里就能跑。技术栈就是纯 Node.js。
// probe.js
// 一个用来探测 Verdaccio 拉包耗时的脚本
// 用法:node probe.js <包名> <版本号>
const http = require('http');
// 命令行参数处理,默认包名和版本号
const packageName = process.argv[2] || 'lodash';
const version = process.argv[3] || '4.17.21';
// Verdaccio 的地址,根据实际情况修改
const verdaccioHost = 'verdaccio.example.com';
const verdaccioPort = 4873;
// 构造请求路径,例如 /lodash/-/lodash-4.17.21.tgz
const tarballPath = `/${packageName}/-/${packageName}-${version}.tgz`;
// 开始计时
const startTime = Date.now();
// 发起 GET 请求
const req = http.request({
hostname: verdaccioHost,
port: verdaccioPort,
path: tarballPath,
method: 'GET',
// 设置 60 秒超时,如果 60 秒还没拿到数据就打印超时
timeout: 60000,
}, (res) => {
const endTime = Date.now();
console.log(`状态码:${res.statusCode}`);
console.log(`耗时:${endTime - startTime} 毫秒`);
// 读取数据,这里不处理数据,只是为了让请求完成
res.resume();
// 如果状态码不是 200,说明有问题
if (res.statusCode !== 200) {
console.error('拉包失败,请检查包是否存在或者权限是否正确。');
process.exit(1);
}
});
// 监听超时事件
req.on('timeout', () => {
console.error('请求超时了,超过 60 秒没有响应。');
// 必须调用 destroy,否则脚本会一直挂在那里
req.destroy();
process.exit(1);
});
// 监听错误事件,例如 DNS 解析失败、连接被拒绝
req.on('error', (err) => {
console.error('请求出错:', err.message);
process.exit(1);
});
// 结束请求
req.end();
这个脚本给不了你最终答案,但能帮你快速判断到底是不是 Verdaccio 本身的问题。如果你手动跑这个脚本每次都很快,但流水线里还是超时,那问题就出在流水线的环境上,而不是 Verdaccio。
五、CI/CD 侧的调整:别让 npm 傻等
很多时候,Verdaccio 可能只是慢了那么一下,但 npm 客户端的默认超时时间太短,导致直接失败。npm 也有自己的超时设置,可以通过环境变量或者 .npmrc 文件来调整。
5.1 修改流水线的 .npmrc
在流水线项目里,通常可以放一个 .npmrc 文件。你可以明确指定 registry 为 Verdaccio 地址,并且把超时时间调大。下面是一个示例:
# .npmrc 文件,放在流水线项目根目录
registry=http://verdaccio.example.com:4873/
# 每次请求的延时时间,单位毫秒,这里设成 5 分钟
fetch-timeout=300000
# 网络请求重试次数
fetch-retries=5
# 每次重试之间的等待时间,单位毫秒
fetch-retry-mintimeout=2000
# 最大重试等待时间
fetch-retry-maxtimeout=10000
如果你用的是 Yarn,则可以在环境变量里设置:
# 在流水线里设置 Yarn 的网络超时时间,单位毫秒
export YARN_NETWORK_TIMEOUT=300000
还有一点很容易被忽略:有些流水线会默认把 npm 的缓存目录给清理掉,导致每次都是全新拉包。如果网络不太稳定,更好的做法是提前在流水线里把依赖缓存到工作区,下次构建就能直接用缓存,减少对 Verdaccio 的请求。
5.2 流水线级联超时
如果你的流水线工具本身有超时控制,比如 Jenkins 的 timeout 步骤,也要合理设置。我曾经看到有人把整个构建阶段限制在 5 分钟,结果光 npm install 就要 4 分 50 秒,那不稳定超时才怪。不要只调整 npm 的超时,还得把流水线阶段的超时放宽到合理范围。
// Jenkinsfile 片段,用 Groovy 写的,展示 stage 级别超时
stage('安装依赖') {
timeout(time: 15, unit: 'MINUTES') {
sh 'npm install'
}
}
注意,上面的示例是 Jenkins 的 Groovy 语法,但本文的技术栈统一是 Node.js?其实这个不矛盾,因为这只是配置片段。不过为了严谨,我在代码块里标注了 groovy。如果你不喜欢混用,也可以用 Node.js 脚本里的 child_process 去执行 npm install,并设置超时。下面我给你一个更好的方案:用 Node.js 写一个带超时的依赖安装脚本,直接作为流水线的执行入口。
// install-with-timeout.js
// 用 Node.js 执行 npm install,并控制整体超时时间
const { exec } = require('child_process');
// 设置 8 分钟超时,单位毫秒
const timeoutMs = 8 * 60 * 1000;
const start = Date.now();
// 执行 npm install
const child = exec('npm install', {
// 不输出太大日志,但是要看进度
stdout: 'pipe',
}, (error, stdout, stderr) => {
const used = Date.now() - start;
console.log(`安装用时:${used} 毫秒`);
if (error) {
console.error(`安装失败:${error.message}`);
// 根据实际退出码决定流水线是否失败
process.exit(1);
} else {
console.log('安装成功');
process.exit(0);
}
});
// 设置定时器,超时直接杀掉子进程
const timer = setTimeout(() => {
console.error(`安装超过 ${timeoutMs / 1000} 秒,强制结束。`);
// 杀掉子进程,-9 表示强制终止
child.kill('SIGTERM');
process.exit(1);
}, timeoutMs);
// 每次子进程输出都刷新当前耗时
child.stdout.on('data', (data) => {
console.log(`[${Date.now() - start}ms] ${data.toString().trim()}`);
});
// 如果子进程提前结束,清除定时器
child.on('exit', () => {
clearTimeout(timer);
});
这段脚本的好处是:你可以在超时日志里明确看到“安装超过 480 秒”,而不是 npm 自己报一个晦涩的 ETIMEDOUT。更重要的是,这个脚本能作为一个统一的入口,后续想加缓存清理、重试逻辑都方便。
六、高可用与缓存策略:让拉包速度飞起来
超时只是表象,本质往往是服务器扛不住并发。如果你的团队规模变大,流水线频繁拉包,Verdaccio 的本地缓存命中率很低,它就要频繁去 npm 公共仓库拉新包。这种场景下,光调超时参数是治标不治本。你要做的是提高缓存命中率,并且让 Verdaccio 更皮实。
6.1 提升命中率
Verdaccio 的缓存机制是:当包已经被拉过一次,它就会存在本地。所以你可以做一件事:在流水线正式构建前,先跑一个“预热”任务,把经常用到的基础包提前拉一遍。具体做法就是用 Node.js 脚本遍历 package.json 里的所有依赖,然后依次请求这些包的 tarball。下面这段代码可以放在一个单独的预热仓库里,定时执行。
// warmup.js
// 读取 package.json 里的依赖,并把所有依赖在 Verdaccio 中"触及"一遍
const fs = require('fs');
const path = require('path');
const http = require('http');
// 读 package.json
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
// 把 dependencies 和 devDependencies 合并成一个数组
const deps = Object.keys(pkg.dependencies || {}).concat(Object.keys(pkg.devDependencies || {}));
// 去重
const uniqueDeps = [...new Set(deps)];
// Verdaccio 地址,替换成你的实际地址
const host = 'verdaccio.example.com';
const port = 4873;
// 并发控制,不要一次发太多请求
const concurrency = 5;
let index = 0;
// 简单的并发执行器
function run() {
if (index >= uniqueDeps.length) return;
const depName = uniqueDeps[index++];
// 请求包的元信息,而不是 tarball,这样也能触发缓存
const reqPath = `/${depName}`;
const req = http.request({
hostname: host,
port: port,
path: reqPath,
method: 'GET',
timeout: 30000,
}, (res) => {
res.resume();
console.log(`预热 ${depName} 完成,状态码:${res.statusCode}`);
run();
});
req.on('error', (err) => {
console.error(`预热 ${depName} 失败:${err.message}`);
// 即使失败也继续,不阻塞其他依赖
run();
});
req.end();
}
// 启动并发预热
for (let i = 0; i < concurrency; i++) {
run();
}
这个脚本不追求极致的性能,只求依赖被 Verdaccio 缓存上。你可以在每天凌晨跑一次,或者在发布新版本前跑一次,流水线里的拉包速度会明显提升。
6.2 用本地持久化缓存加快拉包
除了 Verdaccio 的缓存,你还可以在流水线机器上配置 npm 的持久化缓存。npm 自身会把下载过的包放在 ~/.npm 目录。如果你在 CI 机器上把这个目录挂载到持久化磁盘,那么第二次构建时,npm 会直接走本地缓存,连 Verdaccio 都不需要请求。这是一种很实用的手段。
# 在流水线脚本中,有点类似这样的做法(以 bash 为例)
# 如果缓存目录不存在,就创建它
mkdir -p /cache/npm
# 设置 npm 使用这个目录作为缓存
npm config set cache /cache/npm
# 执行安装
npm install
注意,这种方式适合固定的构建机。如果是动态创建的容器,每次都是新的文件系统,那缓存就失效了,这时候可以配合 Docker Volume 使用。
七、注意事项
这一部分我想集中说一下容易踩的坑,都是我实际看到过的。
7.1 不要忽略 DNS 解析
在流水线容器里,localhost 往往不代表宿主机,更不代表 Verdaccio 所在的那台机器。很多人把 registry 地址配成 http://localhost:4873,结果当然超时,因为容器里的 localhost 是容器自己。应该使用完整的内部服务名,比如 http://verdaccio-service.namespace.svc.cluster.local:4873。如果实在不确定,就用 dig 或者 nslookup 查一下。
7.2 注意 Verdaccio 的鉴权问题
如果你给 Verdaccio 配置了认证,但在流水线里没有登录,那么拉包时会返回 401 或 403。有些 npm 版本在收到 401 时也会表现为超时,因为 npm 一直在重试。解决办法是在 .npmrc 里写入一个只读的访问 token。注意不要把 token 明文提交到仓库里,最好用流水线的凭据管理功能来注入。
7.3 超时时间不是万能的
把超时时间调到很大只是缓解问题,不是根治问题。如果每次拉包都要等十几分钟,那就算不超时,流水线也变得不可用。你应该优先考虑网络带宽、缓存策略、磁盘 IO 这些底层因素。
7.4 不要忽略反向代理
如果 Verdaccio 前面挂了 Nginx 作为反向代理,那么 Nginx 的 proxy_read_timeout 也会影响请求。默认 Nginx 的代理超时是 60 秒,如果你在 Nginx 层就断了,Verdaccio 配置再大也没用。
# Nginx 配置片段,注意 proxy_read_timeout 要调大
location / {
proxy_pass http://127.0.0.1:4873;
proxy_connect_timeout 10s;
proxy_send_timeout 120s;
proxy_read_timeout 120s;
}
7.5 监控与告警
建议为 Verdaccio 加上基础监控,比如请求延迟、磁盘 IO、内存占用。一旦发现平均响应时间从 200ms 涨到了 2 秒,就提前排查,别等流水线报警了才想起来看。
八、总结
回顾一下,Verdaccio 与 CI/CD 流水线集成时出现包拉取超时,本质上就是网络链路、服务性能、客户端配置三者的博弈。排查思路要按顺序来:先看日志确认服务是否收到请求,然后手动探测端口确认网络,再调整 npm 和 Verdaccio 的超时参数,最后考虑用缓存和预热来提升整体速度。不要把时间浪费在盲目的调参上,一定要用探针脚本和日志来定位真正的瓶颈。
对于正处于快速迭代的团队,我建议把“依赖拉取”当成一个基础设施来运维,而不是指望 Verdaccio 一装就万事大吉。将本文中的 Node.js 探针脚本、预热脚本、超时控制脚本固化到你的工具箱里,遇到问题时按部就班跑一遍,超时问题自然就能被快速驯服。希望这篇文章能帮你在流水线日志里少看到几次刺眼的“timeout”。
评论
围绕“Verdaccio与CI/CD流水线集成时包拉取超时的故障处理方法”参与讨论