服务器访问优化实战:把一次请求的 800ms 压到 80ms

举报
yd_244592142 发表于 2026/10/26 22:58:56 2026/10/26
【摘要】 一、从一个真实的慢开始某天下午,监控告警:API 网关 P99 延迟从 120ms 涨到 800ms,用户侧反馈"页面转圈"。一台机器没挂,CPU 只有 30%,数据库也没有慢查询。但就是慢。后来定位出来是三个"不起眼"的叠加:客户端每次请求都重新建 TLS 连接(握手 2-RTT);反向代理没开连接池,每个请求都向上游新建 TCP;响应体没压缩,一个 300KB 的 JSON 在弱网下传...

一、从一个真实的慢开始

某天下午,监控告警:API 网关 P99 延迟从 120ms 涨到 800ms,用户侧反馈"页面转圈"。

一台机器没挂,CPU 只有 30%,数据库也没有慢查询。但就是慢。

后来定位出来是三个"不起眼"的叠加:

  1. 客户端每次请求都重新建 TLS 连接(握手 2-RTT);
  2. 反向代理没开连接池,每个请求都向上游新建 TCP;
  3. 响应体没压缩,一个 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 监控与告警
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。