Apache HTTP Server响应变慢,如何优化连接复用与静态资源缓存
本文说明如何定位Apache连接、首字节、传输及重复下载等耗时,并通过KeepAlive、HTTP/2、静态资源缓存和文本压缩改善响应。还涵盖反向代理连接池、超时参数、耗时日志与验证方法,适合排查网站访问变慢问题的运维和开发人员。

Apache页面“能打开但越来越慢”时,优先判断两个高频问题:请求是否反复建立 TCP/TLS 连接,以及浏览器是否在重复下载本可复用的 CSS、JavaScript、字体和图片。对应的处理方向是合理启用 KeepAlive、确认多路复用是否生效,并为可版本化的静态资源设置长期缓存。
如果动态页面的首字节时间本身很长,仅调整连接与缓存并不能解决根因,还需要继续检查反向代理上游、应用和数据库。优化时应先区分“连接慢、等待响应慢、传输慢还是重复下载”,否则很容易把超时参数改小,却没有消除真正的瓶颈。
先确认慢在哪个阶段
浏览器显示的“耗时”包含多个阶段,Apache访问日志中的总处理时间也不等于网络延迟。可以先根据现象缩小范围。
| 现象 | 优先检查项 | 常见原因 |
|---|---|---|
| 每个资源都有明显连接耗时 | KeepAlive、HTTP/2、反向代理连接头 | 连接未复用、服务端主动关闭、前置代理改写 |
| 首个HTML等待很久,静态文件正常 | 上游响应、PHP或应用日志、数据库 | 应用处理慢,不是静态资源问题 |
| 首次访问正常,刷新仍下载大量资源 | Cache-Control、Expires、资源版本号 | 缓存头缺失或被覆盖 |
| TTFB正常,但大文本资源传输慢 | gzip/Brotli、资源体积 | 文本未压缩或内容过大 |
| 并发稍高就排队 | MPM、工作进程、KeepAliveTimeout | prefork进程被空闲长连接占用 |
| 偶发502或读取超时 | 上游连接池、后端空闲超时、ProxyTimeout | 复用到已被后端关闭的连接 |
可以通过 curl 分解单次请求时间:
curl -sS -o /dev/null \
-w 'connect=%{time_connect}s starttransfer=%{time_starttransfer}s total=%{time_total}s connections=%{num_connects}\n' \
https://www.example.com/assets/app.css
time_connect较高:先看网络建立连接和连接复用。time_starttransfer明显高于time_connect:重点看Apache排队或上游处理。time_total远高于time_starttransfer:重点看响应体大小、压缩和传输过程。- 单次请求无法证明连接复用是否正常,应在同一客户端会话中请求多个资源。
核对Apache版本、MPM和已加载模块
以下配置以Linux上的Apache HTTP Server 2.4为主要参考。不同发行版的服务名和配置路径不同,修改前不要直接假设主配置一定在 /etc/httpd 或 /etc/apache2。
Debian、Ubuntu通常使用 apache2ctl,RHEL、Rocky Linux、AlmaLinux等系统通常使用 apachectl 或 httpd。先执行环境核对:
apachectl -V 2>/dev/null || apache2ctl -V
apachectl -M 2>/dev/null || apache2ctl -M
重点查看:
HTTPD_ROOT和SERVER_CONFIG_FILE:确定主配置位置。Server MPM:确认使用 event、worker 还是 prefork。expires_module、headers_module、deflate_module:确认缓存和压缩模块是否加载。proxy_module、proxy_http_module:使用反向代理时需要检查。http2_module:需要HTTP/2时再核对,不应只根据浏览器协议显示猜测配置状态。
如果使用prefork MPM,长时间空闲的KeepAlive连接可能持续占用进程。event MPM对空闲连接的处理更高效,但不能在不了解应用兼容性的情况下直接切换。尤其是仍使用嵌入式mod_php或非线程安全模块时,应先评估迁移到PHP-FPM等独立应用进程的影响。
优化客户端与Apache之间的连接复用
Apache的KeepAlive允许一个TCP连接承载多个HTTP/1.1请求。对于HTTPS站点,这还能减少重复TCP和TLS握手。建议先检查当前配置,而不是简单地把超时时间调得很大。
下面是一组可作为起始点的示例,数值应根据页面资源数量、客户端网络情况、MPM类型和并发量调整:
KeepAlive On
MaxKeepAliveRequests 200
KeepAliveTimeout 2
三个参数分别解决不同问题:
KeepAlive On:允许HTTP持久连接。MaxKeepAliveRequests:限制单个连接可以处理的请求数。设置过低会让资源较多的页面频繁重新连接;不建议未评估就设置为无限。KeepAliveTimeout:等待同一客户端下一个请求的时间。过长会增加连接和工作资源占用,过短则可能降低高延迟客户端的复用率。
对于event MPM,可以承受的空闲连接模式通常优于prefork,但这不意味着KeepAliveTimeout越长越好。判断是否合适,应同时观察忙碌工作线程、连接状态以及实际复用情况。
可以在一个 curl 进程中请求两个同源资源:
curl --http1.1 -v \
-o /dev/null \
https://www.example.com/assets/app.css \
https://www.example.com/assets/app.js
如果连接可以复用,详细输出中通常会出现复用已有连接的提示;第二个请求的 num_connects 也应为0。若服务端响应中出现以下内容,应继续检查Apache、负载均衡器或应用是否主动关闭连接:
Connection: close
HTTP/2可以在一条连接上并发传输多个请求,减少HTTP/1.1多连接带来的开销,但它并不替代浏览器缓存。即使启用了HTTP/2,未设置缓存头的静态资源仍可能在每次刷新时重新请求。
为静态资源设置可控的浏览器缓存
静态资源缓存的关键不是“全部缓存越久越好”,而是区分资源是否具有稳定URL。
- 文件名包含内容哈希或版本号:适合长期缓存,例如
app.8f31c2a9.js。 - 文件名固定但内容会被覆盖:缓存时间应保守,否则发布后用户可能继续使用旧文件。
- HTML入口页:通常应允许重新验证,避免引用旧版本资源。
- 登录页、用户数据和个性化接口:不能直接套用公共静态缓存策略。
确认模块已加载后,可以在虚拟主机或适当的目录配置中设置资源过期时间:
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/css "access plus 7 days"
ExpiresByType application/javascript "access plus 7 days"
ExpiresByType text/javascript "access plus 7 days"
ExpiresByType image/png "access plus 30 days"
ExpiresByType image/jpeg "access plus 30 days"
ExpiresByType image/svg+xml "access plus 7 days"
ExpiresByType font/woff2 "access plus 30 days"
</IfModule>
这些时间只是策略示例,不是固定标准。如果应用发布时会直接覆盖同名文件,7天也可能过长。更稳妥的方式是让构建系统生成带内容哈希的文件名,然后对这类资源使用长期缓存:
<IfModule mod_headers.c>
<FilesMatch "\.[0-9a-fA-F]{8,}\.(?:css|js|png|jpg|jpeg|svg|woff2)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
<FilesMatch "\.(?:html?|json)$">
Header set Cache-Control "no-cache"
</FilesMatch>
</IfModule>
这里的 no-cache 并不是禁止保存,而是要求使用前向服务器重新验证。若内容完全不应存储,需要根据业务使用 no-store,但不能将其应用到所有静态文件。
长期缓存必须和资源版本化同时实施。若配置了 immutable,却仍在原URL上覆盖内容,客户端可能长期无法获得新版本。哈希匹配规则也要与实际构建产物一致,不能直接复制后不验证。
使用ETag和Last-Modified减少重复传输
当缓存过期或配置为重新验证时,浏览器可以发送条件请求。资源未变化时,Apache返回 304 Not Modified,无需再次传输完整响应体。
检查响应头:
curl -sS -I https://www.example.com/assets/app.css
重点关注:
Cache-Control
Expires
ETag
Last-Modified
单机环境通常可以使用Apache生成的ETag。多节点部署中,如果不同节点对同一文件生成了不同ETag,客户端会失去有效的条件缓存。可以在确认所有节点文件内容、修改时间和发布流程一致后,考虑避免使用inode参与ETag:
FileETag MTime Size
这仍不能替代一致的发布机制。如果各节点文件修改时间不同,即使内容相同,ETag也可能不一致。对于带内容哈希且长期缓存的资源,重点应放在URL不可变,而不是依赖频繁的304验证。
对文本资源启用压缩
连接复用减少握手开销,缓存减少重复请求,压缩则降低首次传输的数据量。CSS、JavaScript、HTML、JSON、XML和SVG通常适合压缩;JPEG、PNG、WebP、MP4、WOFF2等已经压缩的格式不应重复进行DEFLATE处理。
使用 mod_deflate 的示例:
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE text/javascript
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/json
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE image/svg+xml
</IfModule>
验证时需要让客户端声明支持压缩:
curl -sS -I --compressed https://www.example.com/assets/app.css
正常情况下应看到类似响应头:
Content-Encoding: gzip
Vary: Accept-Encoding
mod_deflate通常会处理必要的 Vary: Accept-Encoding。如果前置代理或应用也在压缩,应检查是否出现重复的 Content-Encoding 或 Vary 头。压缩主要改善传输时间,无法修复应用长时间没有生成响应的问题;压缩级别过高还可能增加CPU消耗,应结合资源大小和负载观察。
动态请求慢时检查上游连接复用
当Apache作为反向代理时,存在两层连接:
- 浏览器或客户端到Apache。
- Apache到后端应用服务器。
启用前端KeepAlive并不等于上游连接一定复用。Apache的HTTP代理工作进程通常会维护可复用的后端连接池,但后端主动发送 Connection: close、连接池被禁用、空闲超时不匹配或MPM模式不合适,都可能导致每次请求重新连接上游。
可以先检查现有 ProxyPass 配置是否包含 disablereuse=On,以及应用服务器的Keep-Alive和空闲连接超时。下面的配置只用于说明参数关系,不能不经核对直接作为生产值:
# 示例数值需要结合后端地址、处理时长和后端空闲超时调整
ProxyPass "/api/" "http://127.0.0.1:8080/" connectiontimeout=3 timeout=30 ttl=20 keepalive=On
ProxyPassReverse "/api/" "http://127.0.0.1:8080/"
需要特别区分这些参数:
connectiontimeout:Apache建立上游连接时等待多久。timeout:当前代理请求等待上游数据的时间。ttl:池中空闲连接允许保留的时间,通常应短于后端主动关闭空闲连接的时间。keepalive=On:启用操作系统TCP Keepalive探测,不是HTTP连接池复用的总开关。
如果后端会在空闲30秒后关闭连接,而Apache长时间保留该连接,后续请求可能取到已经失效的连接,表现为偶发代理错误或重新连接。此时应让Apache连接池的空闲生命周期与后端配置匹配,而不是单纯增大 ProxyTimeout。
不要为了消除超时报错就全面提高全局 Timeout。全局超时会影响多个处理阶段,可能让异常请求长期占用资源。应优先定位是连接上游超时、读取响应超时还是应用本身执行过久,再使用更精确的代理参数。
用请求耗时日志区分Apache与应用问题
默认访问日志不一定包含处理耗时。可以增加带 %D 的日志格式,记录从接收请求到完成响应所用的微秒数:
LogFormat "%a %l %u %t \"%r\" %>s %b %D \"%{Referer}i\" \"%{User-Agent}i\"" timed_combined
CustomLog logs/access_timed.log timed_combined
启用前应确认日志目录和轮转策略,避免新增日志长期占用磁盘。通过耗时日志可以进行以下判断:
- 静态文件也普遍耗时高:检查Apache工作线程、存储读取、连接排队和系统负载。
- 静态文件快,代理接口慢:继续检查应用日志和上游处理链路。
- 只有大文件总耗时高,但首字节正常:检查压缩、文件体积和客户端传输。
- 少量请求固定在超时阈值附近失败:检查代理超时和后端空闲连接策略。
- 304很多但请求数量仍高:缓存可重新验证,但资源缓存期限可能过短。
%D记录的是Apache看到的请求总耗时,不能单独证明时间消耗在数据库、应用代码还是上游网络。更准确的做法是将Apache日志时间与应用访问日志、慢请求日志和请求ID对应起来。
安全应用配置并逐项验证
修改前先备份实际变更的配置文件。以下路径必须替换为前面核对出的真实路径:
sudo cp /path/to/changed.conf \
/path/to/changed.conf.bak.$(date +%Y%m%d%H%M%S)
完成修改后先做语法检查:
sudo apachectl configtest 2>/dev/null || sudo apache2ctl configtest
只有返回 Syntax OK 后再平滑重载。Debian、Ubuntu通常使用:
sudo systemctl reload apache2
RHEL系发行版通常使用:
sudo systemctl reload httpd
如果语法检查提示指令未知,先检查对应模块是否加载,不要直接删除报错行后继续上线。模块启用方式与发行版和安装来源有关:Debian系可能通过 a2enmod 管理,RHEL系通常从 conf.modules.d 加载,源码安装则取决于编译参数和 LoadModule 配置。
重载后按以下顺序复核:
- 使用
curl -I检查静态资源的Cache-Control、Expires、ETag和Last-Modified。 - 使用
curl --compressed确认文本资源具有正确的Content-Encoding。 - 在同一客户端会话请求多个资源,确认连接可以复用。
- 通过浏览器开发者工具检查第二次加载是否来自内存缓存、磁盘缓存或304响应。
- 对比静态文件和动态接口的首字节时间,避免把应用慢请求误判为Apache连接问题。
- 观察Apache错误日志、代理错误以及502、503、504状态码是否增加。
- 检查缓存头是否被应用、前置代理或虚拟主机中的其他规则覆盖。
最容易遗漏的是“缓存策略已经生效,但资源URL没有版本化”,以及“前端连接复用了,上游连接却因超时不匹配反复重建”。这两项应在每次静态资源发布和代理参数调整后单独复核,并保留原配置,以便出现旧资源或偶发代理错误时快速回滚。