## 诊断 HTTP 请求延迟:从负载均衡器到 Linux 内核
生产环境中响应时间上升或突发 5xx 错误,需要即时且精确的问题定位。与其乱七八糟地检查各项服务,不如采用系统化的方法,将整个请求处理管道分为网络和应用两个部分。我们将介绍一种使用标准 Nginx 日志、Server-Timing 标头以及低级 TCP_INFO 指标快速定位瓶颈的方法。
起点:分析负载均衡器日志
典型的 Web 应用架构包括多个层:用户、CDN、负载均衡器 (SLB)、应用、连接池和数据库。每层都可能引入延迟,但诊断时负载均衡器始终应作为起点——它位于管道中心,能立即确定排查方向。
对于 Nginx,关键变量是 $request_time 和 $upstream_response_time。第一个记录从接收客户端首字节到发送响应尾字节的完整请求处理时间。第二个仅测量等待上游服务器响应的时间。如果尚未收集这些指标,请扩展日志格式:
log_format timing '$remote_addr - $request_uri '
'status=$status '
'rt=$request_time '
'uct=$upstream_connect_time '
'urt=$upstream_response_time';
access_log /var/log/nginx/access.log timing;
数据解读基于简单比较。如果 $upstream_response_time 接近 $request_time,瓶颈位于负载均衡器右侧:应用代码、连接池或数据库中。如果 $upstream_response_time 正常但 $request_time 异常高,则问题在左侧:网络基础设施、CDN 路由或客户端侧。请注意,通过 add_header 将 $request_time 注入响应标头不推荐用于精确测量,因为 Nginx 在完成响应体传输前就已生成标头。可靠数据仅在连接关闭后的访问日志中可用。
精确定位应用侧问题
当负载均衡器指向上游延迟时,需要深入应用内部操作。W3C Server-Timing 标准允许将单个请求处理阶段的精确计时发送到客户端或监控系统。标头示例如:Server-Timing: app;dur=120, db;dur=95, pool-wait;dur=18。这提供了透明度,而无需部署沉重的 APM 代理。
将标头集成到应用栈中只需少量修改。流行语言的实现示例:
Go (net/http):
start := time.Now()
rows, err := db.QueryContext(ctx, query)
dbDur := time.Since(start)
w.Header().Set("Server-Timing",
fmt.Sprintf("db;dur=%.2f", float64(dbDur.Microseconds())/1000))
Python (Django middleware):
class ServerTimingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
start = time.monotonic()
response = self.get_response(request)
dur = (time.monotonic() - start) * 1000
response["Server-Timing"] = f"app;dur={dur:.2f}"
return response
Node.js (Express):
app.use((req, res, next) => {
const start = process.hrtime.bigint();
res.on('finish', () => {
const ms = Number(process.hrtime.bigint() - start) / 1e6;
// already sent, but for logging:
console.log(`Server-Timing: app;dur=${ms.toFixed(2)}`);
});
// for header — time up to response:
const origEnd = res.end;
res.end = function(...args) {
const ms = Number(process.hrtime.bigint() - start) / 1e6;
res.setHeader('Server-Timing', `app;dur=${ms.toFixed(2)}`);
origEnd.apply(this, args);
};
next();
});
要通过 Server-Timing 转发 Nginx 自身的指标,请使用 map 指令,避免为静态文件或错误发送空值:
Nginx (upstream timing as Server-Timing):
# In http {} block of nginx.conf:
map $upstream_header_time $server_timing_upstream {
"-" "";
default "ngx-upstream;dur=$upstream_header_time;desc=\"NgxUpstream\"";
}
# In location {} block of your server:
add_header Server-Timing-Ngx "ngx-total;dur=$request_time;desc=\"NgxTotal\"" always;
add_header Server-Timing-Ngx $server_timing_upstream always;
注意使用 Server-Timing-Ngx 前缀和 $upstream_header_time 变量。浏览器期望毫秒值,而 Nginx 返回秒数。此外,$upstream_header_time 捕获从上游接收标头的时刻,准确反映响应生成时间——不像 $upstream_response_time,在某些缓冲条件下可能为零。
分析数据可快速分类事件:
- 主导的
db;dur指向 SQL 查询次优、缺失索引或表锁。 - 高的
pool-wait;dur表示 PgBouncer 或类似连接池器连接限制耗尽。 - 大的
app;dur伴随小的db;dur表示 CPU 绑定操作、阻塞 I/O 或运行时内存泄漏。 - 负载均衡器上的 502/504 错误伴随正常计时,通常表示应用进程崩溃或编排器超时。
网络部分:HTTP 阶段和内核指标
如果上游响应迅速但整体延迟增加,则问题出在网络路径中。每个 HTTP 请求经历六个顺序阶段:DNS、Connect(TCP 握手)、TLS 握手、Send、Wait(TTFB)和 Receive。特定阶段异常可立即缩小排查范围。DNS 时间过长指向解析器问题或复杂的 CNAME 链。高 Connect 和 TLS 值表示到 CDN 边缘节点的网络延迟、缺少 TLS 会话恢复或过时协议。如果 Connect 和 TLS 最小但 Wait 异常高,则延迟位于 WAF、CDN 工作者或源获取级别。
对于深度网络诊断,标准 HTTP 细分不够。需要通过 getsockopt(TCP_INFO) 获取 Linux 内核指标。内核跟踪每个 TCP 连接,提供精确数据:RTT、重传计数、拥塞窗口 (cwnd) 大小和 RTO。这些指标至关重要,因为当响应适合单个 TCP 段时,HTTP 计时无法区分网络延迟和服务器处理时间。从 Wait 阶段减去内核 RTT 即可得到真实请求处理时间。重传和压缩的 cwnd 直接指向数据包丢失,以及拥塞控制算法每请求额外添加数百毫秒。将应用标头与低级网络指标结合,即可实现完整可观测性,而无需依赖第三方供应商。
关键要点
- Nginx 中比较
$request_time和$upstream_response_time可立即分离网络和应用责任。 - Server-Timing 标头提供内部延迟(DB、连接池、运行时)的细粒度可见性,而无需沉重的 APM 系统。
- 分析 HTTP 请求阶段可精确识别 DNS、TLS 握手和 CDN 路由问题。
- TCP_INFO 内核指标提供应用级别无法检测的网络丢失和延迟客观视图。
- 系统化诊断方法可缩短 MTTR,消除事件期间混乱的服务重启。
— Editorial Team
暂无评论。