Nginx 的访问日志默认用的是 combined 格式,只记录了客户端 IP、时间、请求行、状态码、字节数这些基本信息。它够用来看"谁访问了什么、返回了没",但一旦要回答"这个接口为什么慢""是 Nginx 慢还是后端慢""哪类请求平均耗时最高"这类问题,默认格式就不够看了——因为它缺少耗时和上游(后端)响应维度的数据。
这篇文章教你自定义 log_format,把请求总耗时、上游连接/首字节/响应耗时、上游地址与状态码、客户端真实 IP、缓存命中情况等都记进日志。我们会先理清 Nginx 日志体系和常用变量,再给出一份可直接用的生产级日志格式,重点讲清那几个 $upstream_*_time 变量的含义与区别,然后讲如何按业务拆分日志、如何用 awk 做简单统计、如何配置 logrotate,最后说清日志量、缓冲与脱敏这些实战注意事项。
一句话概括核心价值:一份带耗时和上游信息的访问日志,是后端性能分析最低成本的抓手。它不依赖 APM,靠 Nginx 自己就能把慢请求、错误率、上游瓶颈都暴露出来。
一、Nginx 日志体系与配置位置
Nginx 有两类日志,职责完全不同:
| 日志 | 指令 | 内容 | 用途 |
|---|---|---|---|
| 访问日志 | access_log |
每个请求一条记录(可自定义格式) | 流量、耗时、状态码分析 |
| 错误日志 | error_log |
运行期错误、告警 | 排查连接、权限、上游故障 |
log_format 定义格式,access_log 指定写到哪、用哪个格式。两者都可以放在 http、server、location 层级,就近生效。
# 在 http {} 块里定义格式(只定义一次,全局复用)
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
}
二、常用变量速查
自定义格式前,先熟悉这些高频变量,配置时按需拼装:
| 变量 | 含义 |
|---|---|
$remote_addr |
直连的客户端 IP(有代理时可能是代理 IP) |
$http_x_forwarded_for |
经过代理时记录的真实客户端 IP 链 |
$time_local |
本地时间,如 12/Oct/2026:10:00:00 +0800 |
$request |
完整请求行,如 GET /a HTTP/1.1 |
$request_method |
请求方法(GET/POST…) |
$request_uri |
带参数的原始 URI |
$status |
响应状态码 |
$body_bytes_sent |
发给客户端的响应体字节数 |
$http_referer |
来源页 |
$http_user_agent |
客户端 UA |
$request_time |
从收到请求到发完响应的总耗时(秒,毫秒精度) |
$upstream_response_time |
Nginx 与上游往返耗时 |
$upstream_addr |
上游地址(多上游/重试时可能是多个) |
$upstream_status |
上游返回的状态码 |
三、自定义生产级日志格式
下面这份格式把耗时、上游、真实 IP 都记下来,适合作为性能分析的基线。
http {
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time '
'urt=$upstream_response_time '
'uaddr=$upstream_addr '
'ustatus=$upstream_status '
'uct=$upstream_connect_time '
'uht=$upstream_header_time '
'xff="$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
}
其中耗时相关的关键变量含义如下,务必分清:
| 变量 | 含义 | 单位 |
|---|---|---|
$request_time |
从读到请求第一个字节到发完响应的总时间 | 秒(ms 精度) |
$upstream_connect_time |
与上游建立 TCP 连接耗时 | 秒 |
$upstream_header_time |
从连上上游到收到响应头耗时(首字节) | 秒 |
$upstream_response_time |
与上游交互的总时间 | 秒 |
理解这几个值的关系,就能判断瓶颈在哪:
$request_time明显大于$upstream_response_time:慢在 Nginx 侧或客户端网络(比如客户端下载慢、TLS 握手)。$upstream_header_time远小于$upstream_response_time:上游"首字节很快但整体很慢",通常是后端边算边发、流式输出或响应体很大。$upstream_connect_time很大:Nginx 连上游就慢,可能是网络、上游负载高、连接队列满。$request_time与$upstream_response_time都很小:请求本身没问题。
经验值:静态资源(图片、CSS、JS)经 Nginx 直出的 $request_time 通常在毫秒级(几 ms 到几十 ms);普通动态接口在几十 ms 到几百 ms;超过 1 秒就值得关注,超过 3~5 秒用户会明显感知卡顿。
四、$upstream_* 变量的实战含义
这些变量在反向代理场景才有值;如果 location 只是 return 或直接出静态文件,它们会是 -。
# 一条典型日志(格式化为多行便于阅读)
remote_addr=10.0.0.9 request="POST /api/order HTTP/1.1" status=200
rt=0.352 # 总耗时 352ms
urt=0.340 # 上游耗时
uaddr=10.0.0.5:8080 # 后端地址
ustatus=200
uct=0.002 # 连上游仅 2ms
uht=0.120 # 首字节 120ms —— 后端计算慢
从这个例子可以看出:连上游很快(2ms),但首字节要 120ms,说明时间花在后端处理上,而不是网络或连接。
再看一个多上游重试的例子的取值规则:当一次请求因重试访问了多个上游时,$upstream_addr、$upstream_status、$upstream_response_time 会用逗号或冒号分隔记录每个上游的情况,例如:
uaddr=10.0.0.5:8080, 10.0.0.6:8080
ustatus=502, 200
urt=0.010, 0.220
这告诉你第一个上游 502 失败、自动重试到第二个才成功——排查稳定性时非常有用。
五、按业务拆分多个 access_log
大站把所有流量写一个文件,既不好分析也不好轮转。可以按站点、按业务、甚至按状态码拆分。
server {
listen 80;
server_name api.example.com;
# 访问日志:API 分开记
access_log /var/log/nginx/api.access.log main;
location /upload/ {
# 上传单独记一份,便于排查大文件问题
access_log /var/log/nginx/upload.access.log main;
proxy_pass http://127.0.0.1:8080;
}
}
# 也可以只记错误(针对高频健康检查场景,降低日志量)
map $status $loggable {
~^[23] 0; # 2xx/3xx 不记
default 1; # 其他都记
}
access_log /var/log/nginx/errors.access.log main if=$loggable;
if=$变量 是条件记录,值为 0 或空则跳过。用它可以把健康检查(如 /health 返回 200)的噪音过滤掉,显著减小日志体积。
六、用命令行做简单分析
不需要上 ELK,awk 加 sort/uniq 就能回答很多问题。以下假设日志格式中状态码字段在第 9 列(按前面 main 格式排列,实际以你的格式为准)。
# 只看 5xx 的请求
awk '$9 >= 500 {print}' /var/log/nginx/access.log | tail
# 统计各状态码出现次数
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn
# 找出耗时(rt=)超过 1 秒的请求(示例按 rt= 字段匹配)
grep -oE 'rt=[0-9.]+' /var/log/nginx/access.log \
| awk -F= '$2+0 > 1 {print}' | sort -t= -k2 -rn | head -20
# 列出访问量最高的 20 个 URL
awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# 实时看访问日志(配合上面带耗时的格式更直观)
tail -f /var/log/nginx/access.log
如果追求更强的分析能力,可以把日志导到结构化系统(如用 Filebeat 采集到 ES,或写入 ClickHouse),但对中小企业而言,上面的命令组合已经能定位大多数问题。
七、日志轮转:别让磁盘被写满
访问日志增长很快:一个中等流量的站点每天几十 MB 到几百 MB 是常态,高流量站点每天几 GB 也不稀奇(经验值,取决于 QPS 和记录字段数)。必须配置轮转。
大多数发行版的 Nginx 包自带 logrotate 配置:
# 查看并确认
cat /etc/logrotate.d/nginx
/var/log/nginx/*.log {
daily # 每天轮转,量大的可改 hourly 或用 size 触发
rotate 14 # 保留 14 份
missingok
notifempty
compress # 压缩旧日志
delaycompress
create 0640 nginx adm
sharedscripts
postrotate
# 让 nginx 重新打开日志文件
[ -f /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid)
endscript
}
kill -USR1 是让 Nginx 重新打开日志文件的关键信号,少了它,轮转后 Nginx 仍写老句柄,日志会"消失"在已改名的旧文件里。
八、性能、缓冲与脱敏注意事项
- 写入缓冲:高 QPS 下频繁刷盘会有开销,可开启缓冲。
access_log /var/log/nginx/access.log main buffer=64k flush=5s;
buffer=64k 表示攒够 64KB 再写,flush=5s 表示最多等 5 秒。注意开缓冲后进程崩溃可能丢一点未刷的日志,排查即时问题时可临时关掉。
- 日志量优化:过滤健康检查、关掉不必要的字段、按需拆分,都能显著降低 IO。
- 脱敏:URL 里的
token、手机号、邮箱等敏感参数不要原样记录,可用map改写或记录前处理,避免合规风险。 $request_time精度:以秒为单位、毫秒精度,统计时按浮点处理。- 改格式要重载:
nginx -t && nginx -s reload。已写入的旧日志格式不会变,注意新旧日志字段可能不一致,分析时按时间切分。
常见问题
Q:$request_time 和 $upstream_response_time 差很多,说明什么?
差大说明时间花在 Nginx 侧或客户端侧,而不是后端。常见原因:响应体大、客户端下载慢、TLS 握手开销、开启了 Gzip 压缩、或者 Nginx 在等客户端接收(客户端带宽差)。如果两者都很小,那就是正常快请求。
Q:为什么 $upstream_* 变量显示成 -?
说明这个 location 没有走反向代理(比如直接 return、try_files 出静态文件、或本地 fastcgi 之外的直出),没有上游概念,所以这些变量为空。
Q:日志里一堆 /health 健康检查请求,怎么过滤?
用条件记录:map $request_uri $loggable { ~^/health 0; default 1; },然后 access_log ... if=$loggable;。既能降低日志量,又不影响排查。
Q:改了 log_format 为什么没生效?
先 nginx -T | grep log_format 确认最终生效的定义,注意同名 log_format 在多层重复定义会以最内层为准;改完必须 nginx -s reload。另外旧日志是旧格式,别拿新格式去解析历史文件。
Q:日志轮转后 Nginx 还在写旧文件(日志没更新)怎么办?
发 kill -USR1 $(cat /run/nginx.pid) 让 Nginx 重新打开日志文件,检查 logrotate 的 postrotate 是否包含这条。用 lsof -p 能看到 Nginx 当前持有的日志句柄。
总结
默认 combined 格式缺少耗时和上游信息,做性能分析时几乎没有价值;自定义 log_format 加入 $request_time、$upstream_response_time、$upstream_addr、$upstream_status 等字段,成本极低、收益很高。
看懂耗时四件套是关键:$request_time 是总耗时,$upstream_connect_time 是连后端耗时,$upstream_header_time 是首字节耗时,$upstream_response_time 是与后端总耗时;两两比较就能判断瓶颈在客户端、网络还是后端。
日志实践上:按业务拆文件、用 if= 过滤健康检查噪音、用 awk 做轻量统计、配好 logrotate 并用 kill -USR1 重开句柄,同时对敏感参数脱敏。
高并发下可用 buffer=64k flush=5s 降低写盘开销,但要权衡崩溃时可能丢少量日志;任何格式变更都要 nginx -t && nginx -s reload 并在切换时间点区分新旧格式。
企业QQ咨询




