Composer 2 的并行下载优化原理

解读

国内一线/二线互联网公司在社招/校招后端面试中,Composer 版本差异是“必问基础”。
面试官真正想确认的是:

  1. 你是否了解 Composer 1→2 性能瓶颈的“根因”——单线程下载导致 I/O 等待;
  2. 能否用“进程模型+IO 多路复用”把并行原理讲透,而不是背“变快了”三个字;
  3. 是否具备线上调优视角:镜像源、缓存、限速、回退策略。
    答不到“进程池+事件循环”层面,会被直接判定为“仅会用,不懂原理”。

知识点

  • Composer 1 的串行流程:
    解析 composer.json → 计算依赖 → 单线程 file_get_contents/curl 下载 → 阻塞等待 → 解压 → 下一个包。
  • Composer 2 并行核心:
    1. 多进程 + 非阻塞 I/O:主进程预先计算好“待下载包列表”,通过 symfony/process 拉起固定大小进程池(默认 6 个,CPU 核数 * 2 上限),每个子进程独立 curl 下载;
    2. 事件循环 + 管道通信:主进程使用 stream_select 监听所有子进程 stdout/stderr 的管道,一旦某子进程返回“下载完成”事件,立即分配下一个任务,实现“边下载边调度”;
    3. 共享缓存锁:~/.composer/cache/files 目录下用 flock 做文件级锁,防止多进程同时写入同一包;
    4. 国内镜像友好:如果 aliyun/huawei 镜像返回 302 到对象存储,Composer 2 会把重定向目标直接交给子进程,避免主进程二次解析;
    5. 失败重试与回退:单包 15 s 超时、整体 300 s 超时,失败后自动降级到“下一个镜像源”,保证高可用。
  • 量化收益:空缓存下 Laravel 项目首次 install,Composer 1 约 110 s,Composer 2 约 18 s(6 核笔记本,200 M 带宽,阿里云镜像),提速 5~7 倍。
  • 线上调优:
    – 设置 COMPOSER_PROCESS_TIMEOUT=600 解决大包超时;
    – 设置 COMPOSER_MAX_PARALLEL_HTTP=20 应对内网千兆镜像;
    – 私有包使用 satis/path-repo 避免外网拉取;
    – CI 镜像层预置 /root/.composer/cache,减少 Job 冷启动。

答案

Composer 2 把“下载”阶段从串行改为并行:

  1. 主进程先一次性算出完整依赖树,得到待下载包列表;
  2. 通过 symfony/process 启动固定进程池,每个子进程负责一个包的 curl 下载;
  3. 主进程用 stream_select 监听所有子进程管道,任何子进程完成即回收并分配新任务,实现非阻塞事件循环;
  4. 文件缓存层用 flock 保证多进程不会重复写入;
  5. 对国内镜像的 302 跳转、限速、超时都做了自适应重试与回退。
    最终效果是把 I/O 等待重叠,CPU 与网络带宽吃满,空缓存 install 提速 5 倍以上。

拓展思考

  1. 如果公司内网有 500 个私有包,如何把并行度提到 50 而不把 GitLab 打爆?
    → 自建 satis 元数据仓库,开启 lazy-load,让 Composer 只拉取 json 索引;
    → 把 dist.zip 提前推到对象存储,satis 里配置 redirect 下载地址,减轻 Git 压力;
    → 在 CI 中预置 Composer cache 镜像层,Job 启动时直接挂载,命中率 95%+。
  2. Composer 2 的并行模型能否照搬到 Yarn/npm?
    → 原理类似,但 npm 采用 pacote + cacache,全部走 JavaScript 单线程+libuv 线程池,进程模型不同;
    → 国内淘宝镜像对 npm 做了“包内文件级缓存”,而 Composer 是“整包缓存”,命中策略不一样。
  3. 未来 Composer 3 可能继续优化的方向:
    – 把“解压+类映射生成”也并行化(目前仍是主进程串行);
    – 支持 HTTP/3 多路复用,减少 TLS 握手;
    – 引入差分更新(zsync),只下载变更的 phar 块,进一步降低内网带宽。