ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

yarn install 卡在 Building fresh packages 的解决办法

yarn install 卡在 Building fresh packages 的解决办法 先说我最近一次被这个场景支配的经历。项目刚从仓库 clone 下来照着 README 敲yarn install前两段Resolving packages、Fetching packages都跑得飞快等到了Linking dependencies结束、终端冒出Building fresh packages...之后进度条就像被冻住一样十分钟、二十分钟过去连个报错都不给就硬生生卡在 wait 状态。按 CtrlC 能中断但重新执行还是卡在同一个地方。一开始我以为是电脑性能不行在后台编译什么大型原生模块后来冷静下来查了一圈才发现真正的问题不在编译而在网络下载和请求排队上面。这个场景太典型了尤其容易出现在新电脑、CI 环境、或者项目刚从一台机器迁到另一台机器的时候。这篇文章把yarn install卡在Building fresh packages...一直 wait 的根因、完整排查链路、以及我实际验证过的解决办法全部拆开讲一遍。文章后面给的方案按优先级排列从临时止血到工程化防复发照着做基本都能收工。1. 先看清楚你卡在的 Building fresh packages 到底是什么阶段先说一个容易忽略的前提下面聊的现象基于 yarn 1.x。这依然是目前绝大多数项目用得最多的版本yarn install在 1.x 里的输出顺序很固定Resolving packages解析依赖树读取 lockfile。Fetching packages把依赖包下载到 yarn 的全局缓存目录。Linking dependencies把缓存里的包硬链接到当前项目的node_modules。Building fresh packages执行每个包自带的 install / postinstall 构建脚本。Done in xx.xxs完成。所以Building fresh packages在整个流程中偏后它做的是“让依赖在你这台机器上真正能跑起来”的最后一步。这一步里 yarn 会逐个执行那些带有构建脚本的包的install、postinstall脚本。最常见的触发对象包括node-sass需要下载对应平台的binding.node二进制文件或者本地用 node-gyp 编译。sharp、bcrypt、canvas、sqlite3典型的原生模块需要编译 C/C 代码。electron需要下载几十 MB 到上百 MB 的平台预编译包。puppeteer默认会下载对应版本的 Chromium 浏览器。这一阶段卡住时终端上通常没有红色报错没有 warning就是光标一直停在那。如果你开了--verbose可能还能看到某个包反复在尝试请求一个下载地址或者干脆就是“卡住不动”。这种“无报错等待”最有迷惑性很多人会误以为是构建脚本在慢速编译实际上绝大多数情况是网络下载层已经彻底堵死了。2. 那个 wait 到底在等什么下载二进制、postinstall 脚本与连接池排队搞清楚 wait 背后的对象比盲目调参数更重要。我拆成几个层面来说。2.1 为什么依赖已经 Fetch 完了还要“Building fresh packages”Fetching packages阶段拿到的是 npm 包本体也就是package.tgz里的 JS 代码和资源文件。但很多包的真正可执行文件并不在 npm 包里比如node-sass的预编译binding.node通常是在安装时才下载的puppeteer的 Chromium 更是好几条下载链路。这些下载动作被写进了包里的scripts.install或scripts.postinstallyarn 在Building fresh packages阶段执行这些脚本时才会触发后续的二进制下载。如果构建脚本跑的是本地编译比如调用node-gyp rebuild那确实可能出现长时间 CPU 占用。但大多数“一直 wait”的场景并不是本地编译而是脚本里的一个 HTTP 下载请求迟迟拿不到响应。2.2 卡住的核心下载请求在排队而不是在传输我遇到过很多次卡住的时候查看网速几乎没有流量进出。这说明请求根本没有进入下载状态而是在连接层等待。这里就要提到日志里经常出现的一段关键信息getconnectiontimeoutexception: wait millis 30007, active 6, maxactive 6如果你开启完整日志后看到了类似字样说明负责下载的那一层用的是连接池模型。maxactive 6表示连接池最多允许 6 个活跃连接当前已经有 6 个连接被占用新请求就必须排队等待wait millis 30007表示默认最长等待 30 秒超时之后请求失败重试。整个安装过程里如果同时有多个下载任务在排队而前面那几个连接又因为网络问题一直不释放后面的任务就会像滚雪球一样越积越多最终表现就是你看到的“Building fresh packages... 一直 wait”。可以这么理解连接池就像餐厅后厨只有 6 个炉子已经全部在烧菜新订单只能排队。前面几个炉子的菜迟迟出不来后面所有订单全堵在队列里而且每单默认只肯等 30 秒等不到就重新排。网络环境越差排队越严重看起来就像永远等不到头。2.3 “wait for pack installer”和进程等待是一回事yarn 在执行构建脚本时本身也会等待一个包安装完成再进入下一个包。日志里的wait for pack installer就是在等这个构建子进程结束。而子进程内的下载请求因为连接池排队无法完成父子两边就陷入了“互相等待”的状态yarn 等构建脚本构建脚本等下载连接。这也是为什么连 CtrlC 都偶尔不灵敏的原因之一真正确认问题得靠下面这套排查步骤。3. 排查链路还原从 verbose 日志到 curl 实测一步步定位真凶遇到这种问题最忌讳的是直接rm -rf node_modules然后重试。没有定位到“具体是哪个包、哪条下载链路”之前重试多少次都一样。我建议按下面这个顺序排查。3.1 第一步开启 verbose 并保留完整日志把默认安装命令替换成yarn install --verbose --network-timeout 600000 21 | tee yarn-install.log--verbose会把每个包正在执行的脚本详情打出来tee把日志写到文件里。重点看卡住之前最后一个输出对应的是哪个包。最常见的场景是[4/4] Building fresh packages... success Saved lockfile. info There appears to be trouble with your network connection. Retrying...看到Retrying基本就锁定是网络问题。如果条目多到刷屏先用grep -n Retrying\|\d%\|Building fresh yarn-install.log过滤出关键行再顺着找是哪个包在重试。3.2 第二步确认进程到底卡在 IO 还是 CPU另开一个终端窗口执行ps aux | grep -E node|yarn | grep -v grep lsof -i -P | grep node重点看两件事进程 CPU 占用是不是接近 0以及有没有处于SYN_SENT、ESTABLISHED状态的连接长期不变化。CPU 趋近 0、连接状态不变几乎可以断定是网络 IO 等待。如果 CPU 跑满且持续很久才需要怀疑本地 C 编译卡住例如内存不足导致编译进程被杀后反复重启。3.3 第三步从明显日志里找出真实的下载地址并 curl 实测如果能在日志里看到具体的 URL直接复制到终端里实测。比如curl -o /dev/null -w dns: %{time_namelookup}s, connect: %{time_connect}s, total: %{time_total}s, speed: %{speed_download} B/s\n https://xxx.xxx/binding.node这一步能快速区分三种情况现象判断curl 秒开、速度稳定yarn 的下载链路配置有问题比如 registry 或镜像变量没生效curl 慢或超时网络环境到该下载源不稳定需要换源或用代理变量curl 直接拒连下载源本身不可用版本太老或链接失效实测下来大多数卡住场景都落在第二行也就是“能通但非常不稳定”。3.4 第四步确认是下载卡住还是编译卡住这一步也很关键。找到疑似出问题的包手动执行它的安装脚本。比如怀疑node-sass就进到node_modules/node-sass目录下直接跑cd node_modules/node-sass node scripts/install.js如果它卡住的位置是打印完下载链接之后那就是下载问题如果直接看到 g 或 clang 编译输出后长时间无动静并且机器内存吃紧那才是编译资源问题。两种问题的解法完全不同前者靠网络层修复后者要加 swap、加内存或升级工具链。4. 从“止血”到“治本”四套方案按优先级落地定位到问题之后我习惯按“先跑通、再稳定、最后工程化”的顺序处理。下面每套方案都有明确的使用场景。4.1 先止血调大超时、降低并发让 install 能跑完如果只是偶尔一次卡住项目里没有太多原生依赖最快的办法是给 yarn 更宽裕的网络容忍度和更低的并发压力。yarn install --network-timeout 600000 --network-concurrency 4 --verbose对应的环境变量写法export YARN_NETWORK_TIMEOUT600000 export YARN_NETWORK_CONCURRENCY4 yarn install--network-timeout从默认的 30 秒调大到 600 秒给慢速网络更多时间--network-concurrency默认值比较高容易把连接池占满就像前面说的maxactive 6一样并发越多反而互相拖死。把并发降到 4~6每个下载任务能分到的连接更稳定整体成功率反而更高。这个方案我用下来对“偶尔抽风”的网络环境效果明显但治标不治本。如果每次都卡就进入下一步。4.2 治本一统一镜像源与二进制下载站点把 registry 切到更稳定的镜像源是当前工程团队里最常见的做法。设置方式分两种推荐写进项目根目录的.npmrc而不是全局配置这样团队所有成员能保持一致registryhttps://registry.npmmirror.com/如果想在命令行临时验证yarn config set registry https://registry.npmmirror.com/镜像源解决的是 npm 包本体的下载速度但Building fresh packages阶段的很多坑出在“包内部脚本下载二进制”这一环光换 registry 不够。需要针对具体包设置二进制下载站点的环境变量常见几个如下# node-sass export SASS_BINARY_SITEhttps://npmmirror.com/mirrors/node-sass/ # puppeteer注意新版变量名不同旧版是 PUPPETEER_DOWNLOAD_HOST export PUPPETEER_DOWNLOAD_BASE_URLhttps://npmmirror.com/mirrors/chromium-browser-snapshots/ # electron export ELECTRON_MIRRORhttps://npmmirror.com/mirrors/electron/ # sharp 使用的 libvips export SHARP_DIST_BASE_URLhttps://npmmirror.com/mirrors/sharp-libvips/设置完这些变量后重新执行安装命令时那些原本需要走外网下载的二进制都会改走镜像稳定性完全不一样。注意不同版本的包对环境变量名有差异比如 puppeteer 在版本升级后把下载地址变量从PUPPETEER_DOWNLOAD_HOST改成了PUPPETEER_DOWNLOAD_BASE_URL。设置前先查一下你项目里锁定的版本对应的变量名否则设置不生效。4.3 治本二针对构建脚本的精准处理如果网络问题搞定了但项目里某些原生模块编译永远失败可以考虑按需跳过构建。一个粗暴但有效的临时方案是yarn install --ignore-scripts这个命令会跳过所有依赖的 install/postinstall 脚本安装速度会肉眼可见地变快。但它有个副作用需要编译的原生模块不会生成二进制文件后面的代码运行时会报“找不到 binding”之类的错误。所以实际项目里我更建议按依赖分情况处理如果项目根本用不到某个带原生模块的传递依赖检查它是不是被打进了dependencies如果只是因为某个包把它作为 optional 依赖安装可以试试--ignore-optional或把它挪到optionalDependencies。如果确实需要这个原生模块就别完全跳过脚本而是单独为它构建。比如先yarn install --ignore-scripts再用yarn rebuild node-sass只重建需要的模块。另一种情况是项目本身很老依赖里锁定了旧版node-sass这类早已停止维护的包。这类包的预编译二进制常常和当前 Node 版本不兼容无论怎么换源都会尝试本地编译。最稳妥的办法是在工程上评估升级依赖短期实在不能升级就固定 Node 版本然后用对应版本的 node-gyp 工具链去编译。4.4 让后续 install 不再全量下载缓存与离线思路Building fresh packages阶段触发的二进制下载通常不会进入 yarn 缓存目录所以每次 install 都要重新下载这也是“每次都卡在同一处”的原因。针对这个现象有几种实用的缓存思路。第一确认缓存目录是否存在异常。执行yarn cache dir如果缓存里有损坏的半成品先执行yarn cache clean清掉再重试。这个操作不频繁做但当你发现中断后重试一直失败时可以试一次。第二利用--prefer-offline优先读取已有缓存yarn install --prefer-offline --frozen-lockfile第三在 CI 环境里把node_modules和 yarn 缓存目录作为构建缓存保存下来并且把缓存 key 设计成包含 lockfile 的 hash。这样只有依赖真正变化时才需要走完整安装流程大部分情况下直接命中缓存从根源上规避了重复下载的问题。5. 怎么确认真的成功以及防止它再次发生的习惯安装成功不是看到Done in xx.xxs就结束了还要确认那些原生模块是真能用的。5.1 验证成功的三个硬指标第一yarn install最终输出Done in xx.xxs且中间不再出现Retrying...。第二对应模块的二进制文件真实存在。比如node-sass在node_modules/node-sass/vendor目录下会有对应平台的binding.node文件electron目录下有完整的dist包。第三直接跑一段最小验证代码node -e require(node-sass); console.log(ok) node -e require(sharp); console.log(ok)能正常打出ok才算安装链路真正通了。5.2 团队层面的固定化配置把一个偶然复现的问题变成“以后再也不会发生”需要把临时的命令行参数变成项目里的固化配置。我现在的习惯是项目根目录始终放一个.npmrc写明registry二进制的镜像站点尽量通过 CI 环境变量统一注入或者写在安装脚本里。lockfile 必须提交到仓库任何人安装前先确认自己和 lockfile 里的版本一致避免某个包升级后引入新的下载逻辑。CI 里统一使用yarn install --frozen-lockfile --prefer-offline配合缓存策略保证每次构建环境一致。对需要特殊下载链路的依赖锁定精确版本例如puppeteer: 22.15.0而不是^22.0.0防止小版本变化导致二进制下载源变更。5.3 个人经验里最值钱的几个避坑习惯踩过太多次这个坑之后我总结出几条比任何参数都好用的习惯。安装原生依赖较多的大型项目之前先手动 curl 一下那些已知的二进制下载地址确认网络通畅再执行 install。这能提前暴露 80% 的问题。卡住之后不要急着删node_modules。先看日志多半能找到具体卡在哪个包的哪条下载链路。直接删目录重来等于把已经定位到的线索扔掉了。就算要重试也只删掉对应包的目录再执行yarn install成本低很多。最后如果你的项目还停留在 yarn 1.x并且这种网络问题遇到得越来越频繁可以考虑把安装命令从yarn install换成corepack管理下的 yarn 2 或直接评估 pnpm。新版工具对缓存、并发和离线模式的控制更强很多老问题在设计层面就规避掉了。当然这是后话先把眼前这关过了再说。
返回列表