HTTP/3 并不是简单地把 HTTP/2 的二进制帧从 TCP 搬到 UDP。它底层使用 QUIC,在传输层直接集成了 TLS 1.3,并且原生支持连接迁移和 0-RTT 恢复。Apache 作为反向代理时,如果只是打开 HTTP/3 监听而不重新审视缓存与亲和性策略,就可能出现缓存对象与客户端连接状态不匹配的问题。比如同一个客户端从 Wi-Fi 切到移动网络后,源 IP 改变,但 QUIC 连接 ID 仍然可以标识会话,这时基于源 IP 的亲和性就不再可靠。

要理解这个变化,需要把视线放在两个关键点上。第一,QUIC 连接不依赖四元组,连接 ID 是持久标识;第二,HTTP/3 头部压缩使用 QPACK,与 HTTP/2 的 HPACK 不同,某些头部可能不会出现在所有请求中。这两点会直接影响 Apache 的缓存键计算和会话粘滞策略。下面从配置和原理两个层面展开。
为什么 HTTP/3 会让 Apache 代理缓存变复杂
在传统 HTTP/1.1 或 HTTP/2 场景中,Apache 的 mod_cache 根据 URL 和少量头字段生成缓存键。客户端是否走 HTTP/2 不影响缓存键,因为请求方法、路径、Host 头和 Vary 指定的头字段已经足够区分资源表示。但 HTTP/3 出现后,客户端可能通过 0-RTT 发送早期数据,而 0-RTT 请求不会携带全量的对端认证信息,后端对这类请求的响应可能与正常请求不同。如果 Apache 把 0-RTT 响应写入缓存,后续别的客户端就会错误命中。
另一个容易忽视的细节是 Alt-Svc 头。Apache 在启用 HTTP/3 后,通常会返回 Alt-Svc 告知客户端可以使用 UDP 443。如果缓存层把 Alt-Svc 作为响应的一部分保存,对于不同虚拟主机或不同协议版本的响应,可能出现头部串用。更稳妥的做法是让 Alt-Svc 由边缘 Apache 生成,而不是由后端应用返回,或者确保缓存键包含协议版本信息。
此外,HTTP/3 的 QPACK 压缩对动态响应头的变化更敏感。如果后端返回的 Vary 头没有包含全部影响内容的字段,QUIC 连接复用可能会放大缓存污染范围。因此建议在高版本缓存策略中显式保留 Vary 头和 Age 头,避免被中间层移除。
配置 Apache 开启 HTTP/3 与代理缓存
Apache 从 2.4 系列开始提供实验性 HTTP/3 支持,主要通过 mod_http3 模块实现。启用前需要确认模块已经编译或动态加载。可以先执行 apachectl -M | grep http3 查看。如果存在,再调整监听配置。下面是一个典型的虚拟主机配置片段,同时监听 TCP 443 和 UDP 443,并启用 HTTP/3 与 HTTP/2。
Listen 443
<VirtualHost *:443>
ServerName proxy.ipipp.com
Protocols h2 h3 http/1.1
SSLEngine on
SSLCertificateFile "/etc/apache2/certs/proxy.pem"
SSLCertificateKeyFile "/etc/apache2/certs/proxy.key"
# 启用 QUIC 监听
H3Protocol on
H3MaxStreams 100
# 反向代理目标
ProxyPreserveHost On
ProxyPass "/" "http://backend_cluster/"
ProxyPassReverse "/" "http://backend_cluster/"
# 磁盘缓存
CacheEnable disk "/"
CacheRoot "/var/cache/apache2/mod_cache_disk"
CacheDefaultExpire 3600
CacheHeader on
CacheDetailHeader on
CacheIgnoreNoLastMod On
</VirtualHost>
这段配置里,Listen 443 只监听 TCP,需要额外的 QUIC 监听。Apache 的 HTTP/3 通常要求单独监听 UDP 443,可以用 Listen 443 quic 或让 mod_http3 自动处理。不同发行版语法有差异,需要参考对应文档。注意防火墙要同时放行 TCP 443 和 UDP 443,否则客户端只拿到 Alt-Svc 却无法建立 QUIC 连接。
缓存部分使用 mod_cache_disk,缓存键默认包含 URL 和 Host。如果后端会返回个性化内容,必须设置 CacheIgnoreHeaders Set-Cookie 或调整 Vary。启用 HTTP/3 后,建议打开 CacheHeader on,这样可以观察缓存命中状态,方便排查 0-RTT 响应被缓存的问题。
会话亲和性在 QUIC 下如何落地
会话亲和性指的是把同一个客户端的连续请求尽量路由到同一个后端节点。传统做法是基于源 IP 哈希或 Cookie。但 QUIC 连接迁移会改变源 IP,基于四元组的负载均衡也会失效。对于 Apache 反向代理来说,如果是单机多后端,可以使用 mod_proxy_balancer 的 stickysession 参数。
<Proxy "balancer://backend_cluster">
BalancerMember "http://10.0.0.11:8080" route=node1
BalancerMember "http://10.0.0.12:8080" route=node2
ProxySet stickysession=ROUTEID
ProxySet lbmethod=byrequests
</Proxy>
# 转发时注入路由 Cookie
ProxyPass "/" "balancer://backend_cluster/"
ProxyPassReverse "/" "balancer://backend_cluster/"
这段配置让 Apache 根据后端返回的 ROUTEID Cookie 粘滞请求。它不依赖源 IP,因此源 IP 改变不会破坏亲和性。但是如果结合缓存使用,需要确保缓存键也包含 Cookie 中的路由标识,否则两个节点返回的同一 URL 可能被交叉缓存。通常建议对需要亲和性的动态接口关闭缓存,或者把 ROUTEID 加入 Vary 头。
在边缘还有一层负载均衡时,更推荐使用 QUIC 连接 ID 的稳定字段做哈希。Apache 本身不直接做基于 QUIC 连接 ID 的转发,但可以在前置 L4 负载均衡器上完成。Apache 只需保持 HTTP/3 会话在后端连接上的映射。一个折中方案是让 L4 层把 QUIC 连接 ID 映射为固定内部 IP,或者启用代理协议传递原始连接信息,再由 Apache 根据代理协议中的连接 ID 做 affinity。
需要注意的是,QUIC 连接 ID 不是永远不变,客户端和服务器都可以通过 NEW_CONNECTION_ID 帧发布新 ID。因此用于亲和性时要在 L4 层维护 ID 映射表,而不是简单哈希一次。这个成本比 TCP 高,但换来了连接迁移下的稳定性。
验证与常见排错方法
配置完成后,先确认模块加载状态:apachectl -M | grep -E "http3|proxy|cache"。然后用支持 HTTP/3 的客户端发起请求,例如 curl --http3-only -v https://proxy.ipipp.com/。如果返回头包含 alt-svc: h3=":443"; ma=86400,说明 HTTP/3 已被协商。此时可以检查 X-Cache 头判断命中状态。
如果客户端始终回退到 TCP,先检查 UDP 443 是否被防火墙或云安全组拦截。再确认 Apache 是否监听了 UDP,可以通过 ss -lunp | grep 443 查看。日志中如果出现 AH02979 相关错误,通常是 TLS 证书或协议配置不完整,需要确认 SSLEngine on 和 Protocols 行在同一虚拟主机中。
缓存错乱排错可以临时关闭 0-RTT,将 H3EarlyData 设为 off,观察问题是否消失。如果确认是早期数据引起,应在后端响应中避免对早期请求返回可缓存内容,或使用 CacheIgnoreNoLastMod On 配合后端重新校验。整体策略是:边缘缓存只存储可复用的公共内容,个性化内容只做短时缓存或不缓存。
通过以上配置和排查,可以让 Apache 在启用 HTTP/3 后保持缓存一致性和会话亲和性。核心原则是区分传输层连接和业务会话,让缓存键基于业务标识而非传输层信息。
HTTP/3QUICApache反向代理修改时间:2026-10-06 17:59:49