前端团队在CI流水线里跑pnpm install,最头疼的就是依赖下载慢、外网registry不稳定。直接把registry指向淘宝镜像虽然能缓解一部分问题,但每次构建都要重新拉取几十上百兆的tarball,带宽和时间都被白白消耗。更合理的做法是在内网架一层代理缓存:第一次请求走外网,之后的请求全部命中本地缓存。如果这层代理再支持HTTP/3的QUIC传输,弱网环境下客户端与服务器之间的连接建立和恢复速度还能进一步提升。本文就围绕Apache这套组合方案,把原理、配置和验证方法讲透。

一、为什么选择QUIC:先搞懂HTTP/3的底层逻辑
QUIC是Google设计、IETF标准化的传输层协议,运行在UDP之上。传统HTTPS连接需要TCP三次握手加TLS握手,一趟下来至少两三个往返;而QUIC把传输层握手和加密握手合并成一次,首包就能携带业务数据,首次连接通常只需一个RTT,恢复会话时甚至可以做到0-RTT。对于频繁创建短连接的包管理器场景,这个优势相当明显。
另一个关键特性是流的多路复用没有队头阻塞。HTTP/2虽然能在一条TCP连接上并行多个流,但TCP层一旦丢包,所有流都得停下来等重传。QUIC在传输层就实现了独立的流,某个流丢包只会阻塞它自己,其余流照常收发。在跨境拉取npm包这种丢包率不低的链路上,实际吞吐差距可以拉开数倍。
还要注意一点:pnpm本身对registry的协议没有强绑定,它最终走的是标准HTTP语义。也就是说,只要服务端正确响应Alt-SVC头通告HTTP/3可用,支持QUIC的客户端就能自动升级协议。这层透明性是我们能用Apache统一做入口的前提。
二、Apache的模块准备:mod_cache与HTTP/3支持
缓存部分依赖Apache自带的三个模块:mod_cache、mod_cache_disk和mod_proxy,一般发行版默认已经编入,直接启用即可。HTTP/3支持则麻烦一些,目前主流做法是通过mod_http3模块实现,它底层依赖ngtcp2和nghttp3这两个库。以Debian系为例,安装和启用模块的命令如下:
# 启用缓存与代理相关模块 a2enmod cache cache_disk proxy proxy_http proxy_http2 headers rewrite ssl a2enmod http3 # 若发行版未收录 mod_http3,需要自行编译 apt install -y libngtcp2-dev libnghttp3-dev apache2-dev git clone https://github.com/icing/mod_http3.git cd mod_http3 && apxs -i -a -c mod_http3.c
编译成功后,确认/etc/apache2/mods-available/http3.load存在并且已在mods-enabled下有软链接。接着创建缓存存储目录并赋予权限,注意CacheRoot指向的目录必须归www-data用户所有,否则Apache会静默写不进缓存:
mkdir -p /var/cache/apache2/pnpm chown -R www-data:www-data /var/cache/apache2/pnpm chmod 755 /var/cache/apache2/pnpm
三、核心配置:反向代理加磁盘缓存的完整实现
下面是虚拟主机的完整配置。思路是监听443端口走HTTP/2,同时通过Protocols指令声明h3与QUIC支持,代理目标指向上游registry.npmjs.org,磁盘缓存对tarball和元数据分别设置不同时长:
<VirtualHost *:443>
ServerName npm-proxy.ipipp.com
Protocols h3 h2 http/1.1
Listen 443
# QUIC 监听 UDP 443 端口
<IfModule mod_http3.c>
ProtocolsH3EarlyData on
</IfModule>
SSLEngine on
SSLCertificateFile /etc/ssl/certs/npm-proxy.crt
SSLCertificateKeyFile /etc/ssl/private/npm-proxy.key
# 启用磁盘缓存
CacheEnable disk /
CacheRoot /var/cache/apache2/pnpm
CacheDirLevels 2
CacheDirLength 1
CacheMaxFileSize 50000000
# tarball 基本不变,缓存30天;元数据缓存5分钟
<LocationMatch \.tgz$>
CacheDefaultExpire 2592000
</LocationMatch>
<LocationMatch /[^/]+$>
CacheDefaultExpire 300
</LocationMatch>
ProxyPreserveHost Off
ProxyPass / https://registry.npmjs.org/
ProxyPassReverse / https://registry.npmjs.org/
Header always set Alt-Svc 'h3=":443"; ma=86400'
</VirtualHost>几个细节值得展开说说。CacheDirLevels 2配合CacheDirLength 1会让缓存文件按哈希分两级目录存放,避免单目录文件数爆炸。上游npm registry对tarball返回的Cache-Control头本身就很长,Apache默认会尊重它,CacheDefaultExpire只在头缺失时兜底。另外ProxyPreserveHost Off是必须的,否则发往上游的Host头仍是你的代理域名,registry会返回404。
四、客户端配置与缓存命中率验证
服务端就绪后,pnpm侧的改动非常小。给项目或全局设置registry指向代理即可:
# 全局切换 registry pnpm config set registry https://npm-proxy.ipipp.com/ # 也可以只针对单个项目,写入项目下的 .npmrc echo "registry=https://npm-proxy.ipipp.com/" >> .npmrc # 验证下载 pnpm install lodash --reporter=ndjson | tail -2
验证缓存是否生效,最直观的办法是看响应头。第一次请求某tarball时响应头里有X-Cache: MISS或APCache: MISS,第二次请求应变为HIT。也可以用htcacheclean工具配合-a参数查看缓存占用,或者在Apache日志格式里加上%{X-Cache}o字段长期统计命中率。若想确认HTTP/3真的建立了,用curl显式指定协议测试:
# curl 需要 7.66 以上版本且编译时带 HTTP3 支持 curl -I --http3 https://npm-proxy.ipipp.com/lodash -v 2>&1 | grep -E "HTTP/3|quic"
命令输出中出现HTTP/3 200字样就说明QUIC握手成功。此外别忘了防火墙要放行UDP 443端口,这是QUIC落地时最容易踩的坑,很多配置完全正确却因为云安全组只开了TCP而一直回退到HTTP/2。
五、常见故障排查与优化建议
实际运行中常见三类问题。第一类是缓存始终MISS,多半是上游返回了Set-Cookie或Vary头导致Apache判定响应不可缓存,可以在配置中加CacheIgnoreHeaders Set-Cookie Vary绕过,但要清楚这对元数据的新鲜度有影响。第二类是pnpm安装时报证书错误,原因是自签证书未被信任,把CA证书导入系统信任库或在客户端配置strict-ssl=false(仅限内网测试)。第三类是QUIC不通,除了防火墙问题,还要检查内核UDP缓冲区,执行sysctl -w net.core.rmem_max=7500000调大缓冲能显著减少大包传输失败。
长期维护方面,建议把htcacheclean配成systemd定时任务,限制缓存目录总量在磁盘容量的百分之七十以内。如果团队规模扩大,可以在Apache前面再加一层,或者改用varnish做缓存层、Apache只负责QUIC终结,这样职责更清晰。对于元数据更新频繁的场景,缩短CacheDefaultExpire并配合pnpm的--prefer-offline参数,能兼顾新鲜度与速度。整体跑通后,团队二次构建的依赖下载时间通常能从几分钟压缩到十几秒,QUIC带来的快速握手则让首次拉取和移动办公场景的体验再上一个台阶。
Apache代理缓存HTTP/3 QUICpnpm私有仓库修改时间:2026-09-08 00:10:42