QUIC协议在传输层直接基于UDP实现,这让它绕开了传统TCP三次握手的开销,但也给反向代理带来了全新的挑战。Checkvist这类在线工具的流量如果需要经过Apache做代理缓存,HTTP/3的处理方式和HTTP/2完全不同。很多运维同学照搬老的mod_proxy配置,结果发现QUIC流量根本走不通,浏览器始终回退到HTTP/2,缓存命中率也上不去。这篇文章会把Apache代理HTTP/3流量的完整链路讲清楚,从协议原理到配置落地,一步步给出可用的方案。

一、QUIC为什么不能直接套用HTTP/2的代理配置
先从协议层面理解问题。传统的Apache反向代理,无论是mod_proxy还是mod_cache,工作前提都是后端连接基于TCP。代理收到客户端请求后,向源站发起一条新的TCP连接,再把响应转回去。这个过程对HTTP/1.1和HTTP/2都成立,因为它们都跑在TCP上。
QUIC彻底改变了这个模型。它把传输层和加密层合并到一起,TLS 1.3的握手直接嵌入QUIC帧里,连接标识符(Connection ID)的设计又允许连接在IP切换时不断重建。这意味着代理如果只是简单地转发UDP包,会破坏QUIC内部的加密握手,因为密钥协商和传输状态是绑定的。中一个典型的现象是:客户端发出Initial包,代理转发到后端,后端响应的握手包回来时代理无法关联到原始连接,握手直接超时。
所以Apache要做HTTP/3代理,实际上有两条路:一是做UDP层的反向代理,自己终结QUIC连接再向后端转发;二是只对HTTP/3做端口声明,实际业务流量仍走HTTP/2,靠Alt-Svc头让客户端自行选择。对Checkvist这种场景,第二种方案更务实,第一种目前Apache的支持还不成熟。
二、Apache的HTTP/3模块现状与编译安装
Apache官方的HTTP/3支持由mod_http3模块提供,它基于ngtcp2和nghttp3库实现。需要注意的是,这个模块目前仍处于实验阶段,2.4.x主线版本默认不带,需要单独编译。编译前先确认依赖:ngtcp2、nghttp3、OpenSSL 1.1.1以上版本,这些库的版本要求比较严格,版本不匹配会在握手阶段出各种诡异问题。
编译安装的大致流程如下:
# 安装依赖库 apt install build-essential libssl-dev cmake git clone https://github.com/ngtcp2/nghttp3.git cd nghttp3 && autoreconf -i && ./configure && make && make install git clone https://github.com/ngtcp2/ngtcp2.git cd ngtcp2 && autoreconf -i ./configure --enable-lib-only make && make install # 编译Apache的mod_http3 cd httpd/modules/http3 apxs -c -I/usr/local/include mod_http3.c h3.c apxs -i -a mod_http3.la
装好之后,在httpd.conf里加载模块并开启监听。注意HTTP/3监听的是UDP端口,必须显式写明proto参数:
LoadModule http3_module modules/mod_http3.so LoadModule ssl_module modules/mod_ssl.so LoadModule http2_module modules/mod_http2.so # TCP上继续提供HTTP/2服务 Listen 443 Protocols h2 h2c http/1.1 # UDP上提供HTTP/3 Protocols h3 h2 http/1.1
这里有个细节值得展开:Protocols指令的优先级是从左到右递减的,客户端会按照这个顺序协商。同一台虚拟主机上同时声明h3和h2是标准做法,因为HTTP/3的发现机制依赖HTTP/2或HTTP/1.1先建立连接,再通过Alt-Svc头告知客户端UDP 443可用。没有Alt-Svc头,浏览器根本不知道服务端支持QUIC。
三、反向代理与缓存策略的完整配置
接下来是代理部分。假设Checkvist的源站在内网,Apache作为前置网关做缓存加速。虚拟主机的核心配置如下:
<VirtualHost *:443>
ServerName checkvist.ipipp.com
Protocols h3 h2 http/1.1
SSLEngine on
SSLCertificateFile /etc/ssl/certs/server.crt
SSLCertificateKeyFile /etc/ssl/private/server.key
# 声明HTTP/3端点,ma=86400表示缓存该声明一天
Header always set Alt-Svc 'h3=":443"; ma=86400'
# 反向代理到内网源站
ProxyPreserveHost On
ProxyPass / http://192.168.0.10:8080/
ProxyPassReverse / http://192.168.0.10:8080/
# 缓存配置:静态资源缓存,API动态请求直通
CacheEnable disk /
CacheRoot /var/cache/apache2/proxy
CacheDirLevels 2
CacheDirLength 1
CacheDefaultExpire 3600
<LocationMatch "\.(js|css|png|woff2)$">
CacheEnable disk
Header set Cache-Control "public, max-age=86400"
</LocationMatch>
<Location /api/>
CacheDisable
ProxyPass http://192.168.0.10:8080/api/
</Location>
</VirtualHost>
这份配置里有几处需要特别解释。首先是Alt-Svc头,它是HTTP/2与HTTP/3之间的桥梁,浏览器看到这个头后,会在后续请求中尝试升级到QUIC。如果代理层用Header always set覆盖了这个头,要确保后端传来的Alt-Svc没有被重复设置,否则客户端可能拿到冲突的声明。
其次是缓存目录的权限问题。mod_disk_cache写入CacheRoot指定的目录时,运行Apache的系统用户必须有写权限,否则缓存静默失败,日志里只会看到零星的open failed记录。建议提前手动创建目录并chown。另外QUIC连接的0-RTT特性会让部分请求在重连时极快到达,缓存锁的配置要跟上:
# 防止缓存雪崩:并发请求同一失效URL时只回源一次 CacheLock on CacheLockPath /tmp/mod_cache-lock CacheLockMaxAge 5
最后是防火墙。HTTP/3走UDP 443,很多环境的安全组只放行了TCP 443,这是QUIC不通的最常见原因。验证命令:
ufw allow 443/udp # 用支持HTTP/3的curl验证 curl --http3-only -I https://checkvist.ipipp.com/
如果返回的响应头里有HTTP/3 200字样,说明QUIC链路已经打通。再配合nghttp3客户端工具或浏览器的开发者工具,在协议列看到h3即确认成功。
四、回退排查与性能调优建议
配置完成后如果浏览器始终显示h2而不是h3,按顺序排查。第一步确认UDP 443确实放行,可以在服务端用tcpdump抓包看有没有收到UDP流量:tcpdump -i any udp port 443。第二步检查证书,QUIC强制要求TLS 1.3,老证书或TLS 1.2-only的配置会导致握手失败。第三步看Alt-Svc头是否真的下发到了客户端,有些CDN或中间层会把这个头吞掉。
性能层面,QUIC的多路复用没有TCP层面的队头阻塞,但代理转HTTP/1.1到源站时瓶颈又会回到源站连接数上。建议源站连接池适当调大:
ProxyPass / http://192.168.0.10:8080/ min=5 max=100 acquire=3000
min和max控制到后端的连接数下限与上限,acquire是获取连接的超时毫秒数。对Checkvist这种交互频繁的应用,长连接复用能显著降低源站压力。至于缓存,动态API部分不建议激进缓存,只缓存静态资源和版本化URL,命中率反而更健康。
与Nginx相比,Apache的mod_http3成熟度确实落后一截,Nginx 1.25以上已原生支持QUIC且配置更简洁。但如果你的现有架构深度绑定Apache,模块化启用HTTP/3加缓存代理的组合依然是可行的过渡方案,等mod_http3转正后再考虑全面切换也不迟。