一个 request_id 串起全链路:Nginx 请求追踪实战,日志再也不打架

一个 request_id 串起全链路:Nginx 请求追踪实战,日志再也不打架

线上排查最头疼的场景是什么?用户说「我刚才点了一下就报错了」,你打开 Nginx access log,几千行几百毫秒内刷过去,根本不知道哪一条对应他这次操作。后端 PHP 日志里有一堆报错,前端接口日志里也有一堆,但它们之间没有任何关联字段,你只能靠时间戳猜。

解决办法只有一个词:请求追踪(Request Tracing)。核心思想是给每一个进来的请求分配一个全局唯一的 ID,Nginx 生成后通过响应头和上游头传给后端,让前端、Nginx、PHP、数据库慢查询日志都带上这个 ID。出了问题,拿一个 ID 就能把所有层的日志串起来。

Nginx 从 1.11.0 起内置了 $request_id 变量,开箱即用,不需要任何模块。这篇文章把它讲透。

一、$request_id 是什么,怎么来的

$request_id 是 Nginx 内置变量,值是 16 字节的随机十六进制字符串,比如 8f3a1c9b4d2e6f70a1b2c3d4e5f60718。它在你第一次引用这个变量时惰性生成,整个请求生命周期内保持不变。

要拿到固定长度的 UUID 样格式(带连字符),可以自己在日志里格式化,但绝大多数场景直接用那 32 位十六进制就够了。

有个细节要注意:$request_id 只在当前 Nginx 实例内唯一,不跨实例。如果你前面挂了 CDN 或者负载均衡,上游可能已经带了 X-Request-ID。最佳实践是「上游有就复用,没有才生成」,后面会讲怎么写。

二、让上游拿到这个 ID:proxy_set_header

光有变量没用,得把它塞进传给后端的请求头。在 location 或 server 段里加:

location / {
    proxy_set_header X-Request-ID $request_id;
    proxy_pass http://127.0.0.1:8080;
}

这样后端(Node、Java、Go、Python 应用)在 $_SERVER['HTTP_X_REQUEST_ID'] 或对应的请求头读取 API 里就能拿到它,然后写进自己的日志。如果是 PHP-FPM,除了 fastcgi_param 之外还可以直接用现成的:

fastcgi_param HTTP_X_REQUEST_ID $request_id;

三、复用上游传来的 ID(CDN/负载均衡场景)

正确做法是优先用客户端或上游系统已经生成的 ID,避免双份追踪。用 map 判断:

map $http_x_request_id $req_id {
    default   $http_x_request_id;
    ""        $request_id;
}

server {
    location / {
        proxy_set_header X-Request-ID $req_id;
        proxy_pass http://127.0.0.1:8080;
    }
}

逻辑:如果请求头 X-Request-ID 已存在(非空),就用它;否则用 Nginx 生成的。这样无论上游是 CDN、API 网关还是另一台 Nginx,链路 ID 都能贯穿到底。

四、把 ID 写进 access log

这是最重要的一步。修改 http 段的 log_format,把 $request_id 加进去:

log_format main '$remote_addr - $remote_user [$time_local] '
                '"$request" $status $body_bytes_sent '
                '"$http_referer" "$http_user_agent" '
                'rid=$request_id '
                'rt=$request_time urt=$upstream_response_time';

access_log /var/log/nginx/access.log main;

重启后,每条日志末尾都会带一个 rid=... 字段。此后任何一次排查,先拿到 ID,然后:

grep "rid=8f3a1c9b4d2e6f70a1b2c3d4e5f60718" /var/log/nginx/access.log

一秒定位到那一条请求的完整信息:状态码、耗时、上游耗时、UA。urt 这个字段还能帮你判断是后端慢还是网络慢。

五、返回给前端:暴露在响应头里

光记在服务器日志里不够 —— 用户反馈问题时你需要知道 ID。把 ID 加到响应头,前端 JS 出错时可以上报:

add_header X-Request-ID $req_id always;

always 参数保证即使返回 4xx/5xx 也会带上这个头(默认只在 2xx/3xx 加)。验证一下:

curl -sI https://你的域名/ | grep -i x-request-id

前端在发请求时可以读这个头,遇到错误弹窗里显示「错误编号 XXXX」,用户截图给你,你直接 grep 服务器日志,闭环了。

六、贯通后端:把 ID 传进应用日志

Nginx 侧做完,还要让应用日志也带上。以 PHP 为例,在入口文件(index.php)最前面加:

<?php
$rid = $_SERVER['HTTP_X_REQUEST_ID'] ?? '-';
define('REQUEST_ID', $rid);

// 让 error_log 自动带前缀
ini_set('log_errors', 1);
ini_set('error_log', '/var/log/php-fpm/www-error.log');

// 或者手动在每条日志前带上
function log_msg($msg) {
    error_log('[' . REQUEST_ID . '] ' . $msg);
}
?>

更有力的做法是接入 Monolog 之类的日志库,给每条日志加一个 processor:

$logger->pushProcessor(function ($record) {
    $record['extra']['request_id'] = $_SERVER['HTTP_X_REQUEST_ID'] ?? '-';
    return $record;
});

这样应用日志里每条都带 request_id,和 Nginx 日志一一对应。数据库层同理,MySQL 8.0 可以通过 performance_schema 或应用层的慢查询注释带上 ID:

SELECT /* rid:8f3a1c9b... */ * FROM posts WHERE id = 1;

慢查询日志里就会留下这个注释,慢 SQL 也能追溯到具体请求。

七、跨服务传递:让下游也带上

如果后端还要调用其他微服务,记得把 X-Request-ID 继续往下传,否则链路到第一个服务就断了。以 PHP 的 curl 为例:

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'X-Request-ID: ' . REQUEST_ID,
]);
// ...

Node.js 用 axios 的话是 headers: {'X-Request-ID': reqId}。原则很简单:收到什么 ID 就继续传什么 ID,永远不重新生成,这样整条调用链共享同一个追踪号。

八、一个完整的最小配置

把上面所有点拼起来,一份可直接抄的配置:

map $http_x_request_id $req_id {
    default   $http_x_request_id;
    ""        $request_id;
}

log_format traced '$remote_addr [$time_local] "$request" $status '
                  '$body_bytes_sent rt=$request_time urt=$upstream_response_time '
                  'rid=$req_id';

server {
    listen 443 ssl;
    server_name 你的域名;

    access_log /var/log/nginx/access.log traced;

    add_header X-Request-ID $req_id always;

    location / {
        proxy_set_header X-Request-ID $req_id;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_pass http://127.0.0.1:8080;
    }
}

nginx -t 测一下语法,然后 systemctl reload nginx 平滑重载。

九、验证链路是否打通

  1. 发一个请求并抓响应头:curl -sI https://你的域名/ | grep -i x-request-id,应返回 32 位十六进制;
  2. 拿这个 ID 去 Nginx 日志里 grep,应能找到对应行;
  3. 查应用日志(带 processor 的话),应能找到同 ID 的记录;
  4. 制造一个后端错误(比如访问一个不存在的路由),确认 500 响应头里 ID 依然存在(验证 always 生效)。

四步全过,说明从客户端到 Nginx 到应用日志的追踪链已经闭环。以后排查问题,先要一个 request_id,剩下的就是一条 grep 命令的事 —— 这才是「可观测」该有的样子,而不是在几千行日志里靠时间戳大海捞针。

Last modification:October 4th, 2026 at 08:26 pm

Leave a Comment