运维与监控
Nginx 出问题时,能救命的只有三样东西:日志、状态接口、监控曲线。日志告诉你哪个请求慢、慢在哪一跳;stub_status 给你连接和请求的实时计数;Prometheus 把这些计数变成趋势和告警。这一篇把这三样配齐,示例环境 Ubuntu 22.04 + nginx 1.24。
日志格式定制
默认格式不够用
Nginx 自带的 combined 格式只有状态码和字节数,没有耗时。线上出现「接口慢」时,你没法从日志里判断是 Nginx 慢、网络慢还是后端慢。所以第一件事是自定义 log_format。
# 必须写在 http 层,写在 server 里会报 "log_format" directive is not allowed here
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'rt=$request_time uct=$upstream_connect_time '
'uht=$upstream_header_time urt=$upstream_response_time '
'upstream=$upstream_addr request_id=$request_id';
access_log /var/log/nginx/access.log main;
}
关键字段
| 变量 | 含义 | 排障价值 |
|---|---|---|
$request_time | 从读到客户端第一个字节到响应发完的总耗时(秒,毫秒精度) | 用户实际感受到的耗时,第一个该看的数 |
$upstream_response_time | Nginx 与上游建立连接后,等响应体的总耗时 | 与 $request_time 对比,判断慢在 Nginx 还是后端 |
$upstream_connect_time | 与上游建立 TCP 连接耗时 | 数值大说明网络或上游 backlog 有问题 |
$upstream_header_time | 上游返回第一个响应头的耗时 | 接近 $upstream_response_time 说明后端处理慢;远小于它说明是传输慢 |
$upstream_addr | 实际转发的上游地址 | 多实例时定位是哪台机器慢 |
$upstream_status | 上游返回的状态码 | 区分「Nginx 自己 502」还是「后端 502」 |
$request_id | 本次请求的随机 ID | 全链路日志串联的钥匙 |
判断逻辑很直接:
rt大、urt小 → 慢在客户端网络或响应体传输(大文件、弱网)。rt大、urt也大 → 慢在后端,继续看uht:uht接近urt是应用处理慢,差距大是响应体大/传输慢。rt大、urt为-→ 请求根本没转到上游,可能卡在 Nginx 自身(限流、磁盘 IO、try_files)。
开了 proxy_next_upstream 重试,或者一个请求打到多个上游,这些字段会输出成逗号分隔的多个值,比如 0.003, 0.512。做日志分析时要按逗号拆开,取最后一个才是最终生效的那次。
JSON 格式给日志平台
要接 ELK/Loki,直接输出 JSON 更省事(免去正则解析):
log_format json_log escape=json
'{'
'"time":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"method":"$request_method",'
'"uri":"$request_uri",'
'"status":$status,'
'"body_bytes":$body_bytes_sent,'
'"request_time":$request_time,'
'"upstream_time":"$upstream_response_time",'
'"upstream_addr":"$upstream_addr",'
'"request_id":"$request_id",'
'"user_agent":"$http_user_agent"'
'}';
access_log /var/log/nginx/access.log json_log;
注意 "status":$status 外面没有引号——状态码是数字,加了引号在 Kibana 里就得按字符串处理,连范围查询都做不了。
error_log 的级别
error_log /var/log/nginx/error.log warn;
级别从松到严:debug、info、notice、warn、error、crit、alert、emerg。生产用 warn,排障时临时调 info 并 reload,别长期开 debug,磁盘和 CPU 都受不了。
日志轮转
Nginx 一直持有日志文件的句柄,直接 mv 是没用的——进程还在往旧 inode 写。正确姿势是配合 USR1 信号让它重新打开文件。
/var/log/nginx/*.log {
daily
rotate 14
missingok
notifempty
compress
delaycompress
create 0640 www-data adm
sharedscripts
postrotate
[ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid)
endscript
}
要点:
postrotate里的kill -USR1是核心,等价于nginx -s reopen。sharedscripts保证一堆日志文件只触发一次信号。delaycompress让最新一份备份额外保留一天未压缩,方便zcat之外直接用grep查。rotate 14按天切保留两周,按磁盘容量和合规要求调。
验证轮转是否生效:
# 手动触发一次
sudo logrotate -f /etc/logrotate.d/nginx
# 确认新文件被创建、旧文件在,且 nginx 写的是新文件
ls -lh /var/log/nginx/
sudo lsof -p $(cat /var/run/nginx.pid) | grep access.log
容器里的 Nginx 日志一般走 stdout,由 Docker 的 json-file driver 接管,要在 daemon.json 里配 log-opts 的 max-size/max-file,或者用 journald/loki driver。在容器里装 logrotate 属于自找麻烦。
stub_status:最轻量的状态接口
stub_status 是 Nginx 自带的模块,输出几个纯文本计数,几乎零开销。先确认编译进去了:
nginx -V 2>&1 | tr ' ' '\n' | grep stub_status
# 输出 --with-http_stub_status_module 就说明有
没有的话:apt 装的官方包默认带;源码编译要加 --with-http_stub_status_module。
开启
server {
listen 127.0.0.1:8081; # 独立端口,只监听本地,外部一律访问不到
server_name _;
location = /stub_status {
stub_status;
access_log off;
allow 127.0.0.1;
allow 10.0.0.0/8;
deny all;
}
}
输出解读
curl http://127.0.0.1:8081/stub_status
Active connections: 291
server accepts handled requests
16630948 16630948 31070465
Reading: 6 Writing: 179 Waiting: 106
| 字段 | 含义 | 怎么看 |
|---|---|---|
Active connections | 当前活跃连接数(含 reading/writing/waiting) | 突然飙高说明流量涨了或后端变慢 |
accepts | 累计接受的连接数 | 单调递增,重启归零 |
handled | 累计成功处理的连接数 | 正常情况下等于 accepts |
requests | 累计处理的请求数 | 除以时间就是平均 QPS |
Reading | 正在读请求头的连接数 | 一般很小,长期偏高说明客户端发请求慢 |
Writing | 正在写响应的连接数 | 与后端处理中的请求大致对应 |
Waiting | 空闲的 keepalive 连接数 | 通常占大多数,正常 |
两个有用的推导:
accepts - handled不为 0:说明有连接被丢弃,通常是worker_connections不够或资源耗尽,日志里会有worker_connections are not enough。requests / accepts:平均每个连接承载多少请求。这个值接近 1,说明 keepalive 没生效(客户端或 Nginx 侧的超时配得太短),连接复用率低。
这个接口会泄露流量规模,而且没有任何认证。必须 allow/deny 限定来源,或者只监听 127.0.0.1。用 location = /stub_status 精确匹配,别写成前缀匹配,免得被别的路径捎带上。
接入 Prometheus
stub_status 只有当前值,没有历史。要曲线和告警,用官方出的 nginx-prometheus-exporter 把状态翻译成 Prometheus 指标。Prometheus 的抓取模型和数据模型见 核心概念与架构,这里只说 Nginx 这一段。
启动 exporter
docker run -d \
--name nginx-exporter \
--network host \
nginx/nginx-prometheus-exporter:1.1.0 \
--nginx.scrape-uri=http://127.0.0.1:8081/stub_status
--network host 让 exporter 直接访问宿主机的 127.0.0.1:8081,同时它自己也监听在宿主机的 9113 端口——用了 host 网络就不需要 -p。不用 host 网络时,把 scrape-uri 换成 host.docker.internal:8081(macOS)或宿主机内网 IP,并加 -p 9113:9113。
验证:
curl -s http://127.0.0.1:9113/metrics | grep '^nginx_'
主要指标:
| 指标 | 类型 | 含义 |
|---|---|---|
nginx_up | gauge | exporter 能否连上 stub_status,1 为正常 |
nginx_connections_active | gauge | 当前活跃连接 |
nginx_connections_accepted | counter | 累计接受连接 |
nginx_connections_handled | counter | 累计处理连接 |
nginx_connections_reading | gauge | 正在读头的连接 |
nginx_connections_writing | gauge | 正在写响应的连接 |
nginx_connections_waiting | gauge | keepalive 空闲连接 |
nginx_http_requests_total | counter | 累计请求数 |
Prometheus 抓取配置
scrape_configs:
- job_name: "nginx"
scrape_interval: 15s
static_configs:
- targets: ["10.0.0.11:9113"]
labels:
instance: "nginx-node1"
env: "prod"
reload 后在 Prometheus 的 Status → Targets 里确认是 UP,再跑一条 nginx_up 看数据。
stub_status 只给连接级计数,没有延迟分布、没有 URL 维度、没有状态码拆分。想要这些,得从 access log 里算(Loki + Promtail,或者日志转指标),或者用 OpenResty 的 lua-resty-prometheus 自己埋点。exporter 解决的是「Nginx 本身健康不健康」,不是「业务接口快不快」。
关键指标看什么
四个核心指标
| 关心的事 | 指标 / 查询 | 异常信号 |
|---|---|---|
| 流量大小 | sum(rate(nginx_http_requests_total[5m])) | 突增突降,配合发布记录看 |
| 连接压力 | nginx_connections_active | 持续接近 worker_processes × worker_connections |
| 连接丢弃 | rate(nginx_connections_accepted[5m]) - rate(nginx_connections_handled[5m]) | 大于 0 就是有连接被丢 |
| 存活 | nginx_up | 掉到 0,exporter 抓不到状态页 |
5xx 和耗时要靠日志。如果日志进了 Loki,可以直接在 Grafana 里出错误率和 P95;没有日志平台的话,最省事的办法是把错误率交给上游应用指标,Nginx 侧只保证「连接层健康」。
告警规则示例
groups:
- name: nginx
rules:
- alert: NginxDown
expr: nginx_up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Nginx exporter 无法获取状态,实例可能已挂"
- alert: NginxConnectionsHigh
expr: nginx_connections_active > 20000
for: 5m
labels:
severity: warning
annotations:
summary: "活跃连接数过高,检查 worker_connections 与后端耗时"
- alert: NginxDroppedConnections
expr: rate(nginx_connections_accepted[5m]) - rate(nginx_connections_handled[5m]) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "出现连接丢弃,检查 worker_connections / ulimit / somaxconn"
日志侧该盯的东西
- 5xx 比例:按
$status分组统计,499单独看(客户端主动断开,见第 6 篇)。 - P95 / P99 的
$request_time:平均值毫无意义,长尾才是用户投诉的来源。 $upstream_response_time与$request_time的差值:差值大说明 Nginx 到客户端这一段慢,可能带宽打满。- 单 IP 请求量 Top N:识别爬虫和攻击,配合
limit_req限流。
小结
- 日志格式的核心是加上
$request_time和$upstream_*_time,靠这几个数就能把「慢在哪一跳」定位清楚;接日志平台用 JSON 格式。 - 轮转的关键是
postrotate里发USR1,让 Nginx 重开文件句柄;容器场景交给 Docker 的日志驱动。 stub_status只监听本地、精确location、加allow/deny;重点看 Active、accepts 与 handled 的差值、requests。- Prometheus 接入用 nginx-prometheus-exporter 抓
stub_status,核心告警是nginx_up、连接水位和连接丢弃率。 - 指标看趋势,日志看细节:5xx 比例、P95 耗时、upstream 耗时差是三个必看项。
监控配好了,最后一篇处理「真出事的时候怎么办」:502/504 怎么分层排查、413/499 什么含义、性能瓶颈在哪,以及一份可以直接抄的最佳实践清单。