服务器访问优化实战:把一次请求的 800ms 压到 80ms
一、从一个真实的慢开始
某天下午,监控告警:API 网关 P99 延迟从 120ms 涨到 800ms,用户侧反馈"页面转圈"。
一台机器没挂,CPU 只有 30%,数据库也没有慢查询。但就是慢。
后来定位出来是三个"不起眼"的叠加:
- 客户端每次请求都重新建 TLS 连接(握手 2-RTT);
- 反向代理没开连接池,每个请求都向上游新建 TCP;
- 响应体没压缩,一个 300KB 的 JSON 在弱网下传输就吃掉 400ms。
修复这三处之后,P99 回落到 80ms。整个过程没加一台机器,没改一行业务代码。
这就是服务器访问优化的魅力:它不性感,但回报极高。
二、先测量,再动手
在没有基线数据之前做优化,等于闭眼开车。
关键指标
| 指标 | 含义 | 健康参考 |
|---|---|---|
| TTFB | 首字节时间,服务端响应速度的核心 | < 200ms |
| P95 / P99 | 长尾延迟,比平均值重要得多 | P99 < 3 × P50 |
| 连接建立时间 | DNS + TCP + TLS | < 300ms |
| 传输时间 | 响应体从服务端到客户端 | 取决于包体与带宽 |
| QPS / 并发 | 吞吐能力 | 压测得出拐点 |
| 错误率 | 5xx、超时、连接重置 | < 0.1% |
永远盯 P99,不要盯平均值。 平均值会掩盖长尾——10% 的请求慢 10 倍,平均值只涨一点点,但这 10% 的用户已经在流失了。
一次请求的耗时拆解
用 curl 就能拿到全链路分项耗时:
curl -w '
DNS解析: %{time_namelookup}s
TCP建连: %{time_connect}s
TLS握手: %{time_appconnect}s
首字节: %{time_starttransfer}s
总耗时: %{time_total}s
' -o /dev/null -s https://api.example.com/v1/feed
拿到这段数据,你就知道时间到底花在哪一段,而不必靠猜。
三、全链路优化地图
一次 HTTPS 请求的生命周期,可以拆成六段。每一段都有对应的优化手段:
[DNS 解析] → [TCP 建连] → [TLS 握手] → [请求发送] → [服务端处理] → [响应传输]
30ms 40ms 80ms 5ms ? ?
1. DNS:别让用户等一次查询
- 减少 CNAME 链:每多一跳 CNAME,就多一次递归查询。尽量控制在 1 跳以内。
- 合理设置 TTL:太短导致频繁解析,太长导致故障切换慢。常规业务 300s~600s 是个平衡值。
- 前端 DNS 预解析:
<link rel="dns-prefetch" href="https://api.example.com">
<link rel="preconnect" href="https://api.example.com" crossorigin>
preconnect 更进一步,会连 TCP + TLS 一起做掉,对第三方域名收益明显。
- 服务端不要每次都做反向 DNS 解析:不少日志组件默认开启,是隐形的性能杀手。
2. TCP:复用,复用,还是复用
新建一条 TCP 连接的成本 = 三次握手(1-RTT)+ 慢启动。高频短请求下,这个开销会被无限放大。
Nginx 侧打开长连接与连接池:
http {
keepalive_timeout 65; # 长连接保持时间
keepalive_requests 10000; # 单连接最大请求数,默认 100 太保守
upstream backend {
server 10.0.0.1:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.2:8080 max_fails=3 fail_timeout=30s;
keepalive 128; # 每个 worker 与上游的空闲连接池
}
server {
location /api/ {
proxy_pass http://backend;
proxy_http_version 1.1; # ← 关键:上游长连接必须用 1.1
proxy_set_header Connection ""; # ← 关键:清掉 close
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
}
}
}
⚠️ 这是最容易被漏掉的一处:
upstream里写了keepalive,但proxy_http_version还是默认的 1.0、或者把Connection: close透传了上去——连接池等于没生效。
内核侧调参:
# /etc/sysctl.conf
net.core.somaxconn = 65535 # 全连接队列上限,默认 128 太小
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_reuse = 1 # 复用 TIME_WAIT socket
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_slow_start_after_idle = 0 # 避免空闲后重新慢启动
换拥塞控制算法为 BBR(对丢包链路的提速极其明显):
echo "net.core.default_qdisc=fq" >> /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf
sysctl -p
sysctl net.ipv4.tcp_congestion_control # 验证
3. TLS:把握手从 2-RTT 压到 0-RTT
- 强制 TLS 1.3:握手从 2-RTT 降到 1-RTT,配合会话复用可做到 0-RTT。
- 开启会话复用:
ssl_session_cache+ssl_session_tickets。 - OCSP Stapling:省掉客户端自己去 CA 查证书状态的一次往返。
- HSTS:后续访问直接跳过 HTTP→HTTPS 的 302 跳转。
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_protocols TLSv1.2 TLSv1.3; # 果断关掉 TLSv1/1.1
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:50m; # 约可缓存 20 万会话
ssl_session_timeout 1d;
ssl_session_tickets on;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 1.1.1.1 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
注意 0-RTT 的代价:0-RTT 数据在 TLS 1.3 下不具备完全的前向保密性,且存在重放风险。只应对幂等请求(GET 类只读)启用,涉及下单、支付的接口务必关闭。
4. 协议升级:HTTP/2 与 HTTP/3
| 特性 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 连接复用 | 串行 / 多连接 | 单连接多路复用 | 多路复用(无队头阻塞) |
| 头部压缩 | 无 | HPACK | QPACK |
| 传输层 | TCP | TCP | QUIC(UDP) |
| 弱网表现 | 差 | 一般 | 好(丢包不互相拖累) |
HTTP/3 在高丢包、高延迟的移动网络下收益最大。落地建议:边缘层(CDN / Nginx)支持 HTTP/3,回源仍用 HTTP/1.1 或 HTTP/2,改动最小。
listen 443 quic reuseport;
add_header Alt-Svc 'h3=":443"; ma=86400';
5. 缓存:最快的请求是没发生的请求
缓存有四个层次,逐层拦截:
浏览器缓存 → CDN 边缘 → 网关/反向代理缓存 → 应用缓存(Redis) → 数据库
静态资源:强缓存 + 内容哈希文件名
location ~* \.(js|css|png|jpg|webp|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
文件名带 hash(app.a3f9c2.js),内容变了文件名就变,因此可以放心设一年。
动态接口:用 stale-while-revalidate 消除回源抖动
Cache-Control: max-age=60, stale-while-revalidate=300
过期后先给旧数据(不阻塞用户),同时在后台异步刷新。用户永远不用等回源。
反向代理缓存(Nginx):
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:100m
max_size=10g inactive=60m use_temp_path=off;
location /api/feed {
proxy_cache api_cache;
proxy_cache_valid 200 302 10s;
proxy_cache_lock on; # 防止缓存击穿:同一 key 只放行一个回源
proxy_cache_use_stale error timeout updating http_500 http_502 http_503;
add_header X-Cache-Status $upstream_cache_status; # 便于排查命中率
}
proxy_cache_lock 这一行很关键——热点 key 过期的瞬间,没有它会有成百上千个请求同时打穿到后端。
6. 传输:把字节数砍下来
- Brotli 优先,gzip 兜底:Brotli 比 gzip 平均再小 15%~20%。
- 只压缩文本:图片、视频本身已是压缩格式,再压纯属浪费 CPU。
- 别压太小的包:小于 1KB 的响应压缩收益低于开销,还会引入额外延迟。
gzip on;
gzip_comp_level 5; # 6 以上压缩比提升很小,CPU 却翻倍
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript
application/xml+rss image/svg+xml;
gzip_vary on;
# 若已编译 brotli 模块
brotli on;
brotli_comp_level 5;
brotli_types text/plain text/css application/json application/javascript;
别忘了零拷贝:sendfile on + tcp_nopush on,静态文件直接从内核态发送,跳过用户态拷贝。
7. 服务端处理:真正的那几毫秒
前面所有优化都做完之后,剩下的就是业务耗时。这里的优化路径:
- 慢查询治理:给
WHERE/JOIN字段建索引,用EXPLAIN验证,消灭 N+1 查询。 - 连接池:数据库的连接创建成本远高于 Redis,务必池化并限制上限(不是越大越好)。
- 异步化:日志上报、消息通知、数据统计这类非关键路径,丢进消息队列异步处理。
- 批量接口:把 10 次单条查询合并成 1 次批量查询,网络往返直接省 9 次。
8. 别让慢请求拖垮整台机器——限流与超时
一个"优化"角度常被忽略:把坏流量挡在外面,好流量自然就快了。
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s;
location /api/ {
limit_req zone=api_limit burst=50 nodelay;
limit_conn perip 20;
}
同时,每个对外调用都必须设超时。一个没有超时的下游 HTTP 调用,会在故障时把你的线程池耗尽,最终整站不可用。宁可快速失败,也不要无限等待。
四、优化前后对比
以本文开头的案例为例:
| 优化项 | 优化前 | 优化后 |
|---|---|---|
| TLS 握手 | 每次 2-RTT(~80ms) | 会话复用 0-RTT(~0ms) |
| 上游连接 | 每请求新建 TCP | 连接池复用 |
| 响应体 | 300KB 未压缩 | Brotli 后 48KB |
| P99 延迟 | 800ms | 80ms |
| 单机 QPS | 1,200 | 4,500 |
机器数量没变,业务代码没变,成本没变。
五、上线后别忘了:持续监控
优化不是一次性动作,而是闭环:
埋点采集 → 指标可视化 → 阈值告警 → 定位瓶颈 → 优化 → 验证 → 回到采集
- 指标可视化:Prometheus + Grafana,重点看 TTFB、P99、缓存命中率、连接数。
- APM 链路追踪:SkyWalking / Jaeger,定位"到底哪一段慢"。
- 日志分析:定期跑一遍慢请求 Top 50,看看有没有新的坏味道。
# 从访问日志里揪出最慢的 20 个接口
awk '{print $NF, $7}' access.log | sort -rn | head -20
六、一份可以直接照着做的检查清单
- [ ] 全站 HTTPS,且强制 TLS 1.2+
- [ ] 已开启 TLS 会话复用与 OCSP Stapling
- [ ] 已开启 HTTP/2(有条件上 HTTP/3)
- [ ] Nginx 到上游开启
keepalive且proxy_http_version 1.1 - [ ]
keepalive_requests已调高(≥ 10000) - [ ] 已开启
sendfile/tcp_nopush - [ ] 文本响应已开启 gzip / brotli,且设置了
min_length - [ ] 静态资源带内容 hash + 一年强缓存
- [ ] 热点接口有缓存,且配置了
proxy_cache_lock防击穿 - [ ]
somaxconn/tcp_max_syn_backlog已调大 - [ ] 已启用 BBR 拥塞控制
- [ ] 关键接口配置了限流
- [ ] 所有对外调用都设置了超时
- [ ] 有 P99 监控与告警
- 点赞
- 收藏
- 关注作者
评论(0)