跳到主要内容

运维与监控

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_timeNginx 与上游建立连接后,等响应体的总耗时与 $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
Docker 场景别配 logrotate

容器里的 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 侧的超时配得太短),连接复用率低。
stub_status 不要暴露公网

这个接口会泄露流量规模,而且没有任何认证。必须 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_upgaugeexporter 能否连上 stub_status,1 为正常
nginx_connections_activegauge当前活跃连接
nginx_connections_acceptedcounter累计接受连接
nginx_connections_handledcounter累计处理连接
nginx_connections_readinggauge正在读头的连接
nginx_connections_writinggauge正在写响应的连接
nginx_connections_waitinggaugekeepalive 空闲连接
nginx_http_requests_totalcounter累计请求数

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 看数据。

exporter 的边界

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 什么含义、性能瓶颈在哪,以及一份可以直接抄的最佳实践清单。