Nexus 3 npm 仓库迁移踩坑手册
根因分析
npm-group = [npm-proxy, npm-hosted]- npm-proxy:远程代理仓库,缓存 npmjs 公共包,包版本齐全
- npm-hosted:本地托管仓库,所有私有发包上传于此
- 两仓库存在重叠包(同名),group 合并元数据时冲突
- Nexus 3 内部解析重叠包的
dist-tags/versions产生异常,抛 500
1.3 仓库架构
用户请求 → npm-group(统一入口) ├─ npm-proxy(远程缓存,~X万包) └─ npm-hosted(本地存储,~Y万包)2. 修复方案:四阶段迁移
| 阶段 | 操作 | 并发 | 说明 |
|---|---|---|---|
| 1 | 全量检查重叠包 | 12 线程 | 对 proxy/hosted 共有包逐包请求 group,标记 500 失败 |
| 2 | 计算缺失版本 | 串行 | 获取 proxy 全量版本,与 hosted 差集得出需补清单 |
| 3 | 多线程下载→上传 | 4-20 线程 | 从远程源下载 tarball,上传到 hosted |
| 4 | 验证 + 重建 | 12 线程 | 逐一复查失败包,确认 group 恢复正常 |
最终放弃割接方案,改为修改 Nexus 源码新增
hostedFirst版本级合并策略。
3. 断点续传机制
# 核心:基于结果文件的 Set 去重 processed = set() if os.path.exists(RESULT_FILE): for line in open(RESULT_FILE): r = json.loads(line) processed.add((r['name'], r['version'])) pending = [e for e in all_entries if (e['name'], e['version']) not in processed]- 结果文件
fix500_result.jsonl逐行追加,每行包含{name, version, action, source, size} - 重启时加载全部已处理 key → Set 去重 → 筛选待处理
- 支持任意次数中断重启,进度不丢
- 每个 worker 完成立即
flush写入,单条不丢失
4. 性能踩坑实录
4.1 坑一:下载源优先级
现象:早期下载源配置npmmirror → npmjs → proxy,单包耗时 3 分钟,速率 ~0.5/min
根因:npmmirror 为国内镜像 CDN,但走公司代理后限速至300 KB/s;而内部 Nexus proxy 仓库直连可达6.5 MB/s(22 倍差距)
npmmirror(走代理): 300 KB/s → 55MiB/186s proxy(内部直连): 6.5 MB/s → 55MiB/8.6s解决:源优先级改为proxy → npmmirror → npmjs
4.2 坑二:代理绕过
现象:试图让 npmmirror 不走代理直连,结果彻底超时
根因:公司网络所有出站流量必须走指定代理,直连 npmmirror CDN 的 TCP SYN 被直接丢弃
解决:回退成所有请求统一走代理,利用内部 Nexus proxy 仓库作为首选源
4.3 坑三:并发悖论
现象:20 线程反而比 4 线程慢
根因:代理出口带宽固定(~3 MB/s),每个线程分到的带宽 = 总带宽 / 线程数
| 线程数 | 单线程带宽 | 55MiB 文件耗时 | 完成间隔 |
|---|---|---|---|
| 2 | ~1.5 MB/s | ~35s | ~18s |
| 8 | ~375 KB/s | ~150s | ~19s |
| 20 | ~150 KB/s | ~370s | ~18s |
结论:单文件完成时间与线程数成反比,但完成间隔(吞吐量)恒定,由总带宽÷文件大小决定
实践策略:
- 大文件段(>10MB):2-4 线程,减少连接开销和内存
- 小文件段(<1MB):8-20 线程,充分利用 HTTP 并发
4.4 坑四:大包家族
迁移过程中发现两类巨型 npm 包严重拖慢进度:
| 包家族 | 版本数 | 总大小 | 最大单包 | 特点 |
|---|---|---|---|---|
| @iconify/json | 1,491 | ~57 GB | 84 MiB | 全图标库 JSON,每个版本独立打包全部图标 |
| @swc/core-* | 8,580 | ~20 GB | 49 MiB | Rust 编译的 JS 工具链,每个平台/架构各一个包 |
影响:这两族包仅占总数的 3%,却消耗了80% 的迁移时间(iconify 约 8 小时,swc 约 3 小时)
应对:大包段降低并发避免互抢,确保优先用内部 proxy 源下载
4.5 性能演进总览
| 阶段 | 配置 | 速率 | 瓶颈 |
|---|---|---|---|
| 初始 | 15 线程,npmjs 优先 | ~2/s | npmjs 国际线路限速 |
| 优化1 | 20 线程,npmmirror 优先 | ~7/s → 2/s | 大包出现后回归 |
| 回退 | 20 线程,npmmirror 直连 | ~0.5/min | 直连超时 + 全回退 |
| 修正 | 8 线程,走代理 | ~1/min | proxy 慢 + 大包 |
| 翻盘 | 2-8 线程,proxy 优先 | ~20/s | 小包段起飞 |
5. 磁盘空间优化
迁移完成后,npm-proxy与npm-hosted存在大量重叠冗余包。
清理逻辑:遍历 proxy 中所有包,在 hosted 中找到同名包则删除 hosted 元数据。Nexus 内部 blob store 通过 "Compact blob store" 定时任务回收释放空间。
实际回收:数百 GB
6. 操作手册
6.1 环境变量
export NEXUS_URL="http://<NEXUS_HOST>:8081/nexus/content" export NEXUS_USER="<USER>" export NEXUS_PASS='<PASS>'6.2 脚本参数
python3 nexus-fix-500.py --phase 3 --workers 8| 参数 | 默认 | 说明 |
|---|---|---|
--phase 1 | 阶段1 | 检查 group 500 包 |
--phase 2 | 阶段2 | 计算缺失版本 |
--phase 3 | 阶段3 | 多线程迁移(主力) |
--phase 4 | 阶段4 | 验证修复 |
--phase 5 | 阶段5 | 更新 group 配置 |
--workers N | 15 | 迁移并发数 |
--skip-phase1 | - | 跳过阶段1(用已有清单) |
--skip-phase2 | - | 跳过阶段2(用已有清单) |
6.3 典型用法
# 首次完整执行 python3 nexus-fix-500.py # 断点续传:只跑迁移阶段 python3 nexus-fix-500.py --phase 3 --workers 8 # 换机器续传(拷贝 migration_data/ 目录即可) scp -r migration_data/ new-host:/path/to/ python3 nexus-fix-500.py --phase 3 --workers 86.4 前置依赖
- Python 3.6+
requests,urllib3- Nexus REST API 可用
- 网络可达
<NEXUS_HOST>:8081
6.5 断点续传文件清单
| 文件 | 用途 | 大小参考 |
|---|---|---|
fix500_result.jsonl | 已处理记录(核心) | ~7 MiB |
fix500_versions.jsonl | 待处理版本清单 | ~38 MiB |
fix500_list.jsonl | 失败包清单 | ~300 KiB |
migration_data/ | 所有数据目录 | ~46 MiB |
