为什么个人站长也需要 Nginx stub_status
很多个人站长的监控方式是「网站打不开了才去看服务器」。这种被动模式在流量小时还能凑合,一旦被爬虫抓取、被 CC 攻击、或者某个 PHP 接口突然变慢,你只能看到结果——网站变慢或 502——却不知道瓶颈到底在哪一段。Nginx 官方其实内置了一个极轻量的状态页模块 ngx_http_stub_status_module,开启后只暴露几个数字,却能直接回答「现在是正常的空闲连接,还是连接被占满卡住了」这个最关键的问题。
它的好处是零依赖:不需要装 Prometheus、不需要装 Exporter、不占额外进程,只要改几行 nginx.conf 就有一个实时状态接口。本文讲的是怎么把它接出来、每个数字到底代表什么、以及怎么把这些数字变成「能看懂、能告警」的指标。
一、确认模块是否已编译进 Nginx
先别急着改配置,第一步是确认你的 Nginx 编译时带了 stub_status。绝大多数发行版自带的 nginx 包(CentOS 的 nginx、Ubuntu 的 nginx-full)都是默认带的,但如果你用过 OpenResty 或自己编译过,就要先查。
nginx -V 2>&1 | tr ' ' '\n' | grep stub_status如果输出里有 --with-http_stub_status_module,说明模块可用。如果什么都没有输出,说明当前二进制不带这个模块,你有两个选择:换成带模块的官方包,或者用 nginx -V 复制出完整的 configure 参数重新编译加 --with-http_stub_status_module。个人站长强烈建议走前者,重新编译 Nginx 的风险和收益不成正比。
顺带一个小技巧:nginx -V 的输出会跑到标准错误,所以必须加 2>&1 才能被管道接住,否则你会以为是空结果。
二、最小可用的 status 配置
stub_status 的本质就是一个 location,返回纯文本。最常见的写法是放在默认 server 里,只允许本机访问:
server {
listen 127.0.0.1:8080;
server_name _;
location = /nginx_status {
stub_status;
access_log off;
allow 127.0.0.1;
allow 10.0.0.0/8;
deny all;
}
}
为什么要单独 listen 在 127.0.0.1:8080,而不是挂在公网 443 的 server 里?因为 status 页面会暴露你的流量规模,即使加了 allow/deny,也属于「不必要的对外暴露」。挂在 localhost 上,外部扫描器根本看不到这个端口,同时你本机、跳板机、或者同机的采集脚本都能直接 curl 到。如果你有多台机器要采集,把 allow 换成内网网段即可。
改完配置先做语法检查再 reload,这是运维的基本纪律:
nginx -t && nginx -s reload
curl -s http://127.0.0.1:8080/nginx_status正常会返回类似这样的五行文本:
Active connections: 43
server accepts handled requests
1283746 1283746 3729184
Reading: 2 Writing: 6 Waiting: 35注意:stub_status 在较老的版本里需要写成 stub_status on;,新版两者都兼容。如果你的配置报了 unknown directive,就加上 on 再试。
三、每个数字到底在说什么
这几个数字看着简单,但真正看懂的人不多,而它们恰恰是最有价值的排障依据。
- Active connections:当前 Nginx 正在处理的活跃连接数。注意它的口径是「TCP 连接」,不是「请求数」,也不是「独立访客数」。浏览器会复用连接(keepalive),所以 43 个活跃连接可能对应几百个在线用户。
- accepts:Nginx 启动以来接受的连接总数。
- handled:成功处理的连接总数。正常情况下
accepts和handled应该相等。如果 handled 比 accepts 小,说明有连接被丢弃了——通常意味着撞到了 worker_connections 上限,或者是资源耗尽。这个差值是你最该盯的「异常信号」,很多文章都不提。 - requests:总的 HTTP 请求数。用 requests 除以 handled,得到的就是平均每个连接处理了几个请求,这个比值直接反映 keepalive 的效果。如果这个数长期接近 1,说明你的 keepalive 基本没生效,每次请求都在重新建连,白白浪费 TLS 握手和 TCP 开销。
- Reading:Nginx 正在读取请求头的连接数。这个数应该很小(通常个位数)。
- Writing:Nginx 正在向客户端回写响应的连接数。
- Waiting:已经建立、但当前空闲等待请求的连接数(keepalive 连接)。这个数在正常业务下往往占 Active 的大头。
它们的关系是:Active = Reading + Writing + Waiting。这个恒等式非常关键,是你验证数据是否合理的第一个手段,也是后面排查「连接泄漏」的基础。
四、从数字到判断:三个实战场景
光有数字不会判断,等于没监控。下面三个场景是我在真实站点上遇到过的,每一个都能被 stub_status 直接定位。
场景一:站点持续 502,Active 数飙到几千
如果 Active 连接数远高于你的正常水平,而 Reading/Writing 都很小、Waiting 特别大,说明大量连接建立后被挂住了没释放。常见原因是后端 PHP-FPM 的进程池被占满:Nginx 把请求转给 PHP,PHP 不返回,Nginx 就一直等,连接堆在 Waiting 上。此时去看 php-fpm 的 pm.max_children 和慢日志,往往能立刻找到那个拖慢的接口。
反过来,如果 Writing 很高而 Waiting 很低,说明问题出在「往外发数据慢」——通常是出口带宽打满,或者客户端是慢速移动网络,这时该看的是带宽而不是 PHP。
场景二:requests/handled 比值一直在 1.0 附近
这是 keepalive 配置缺失的典型信号。检查你的配置里有没有:
http {
keepalive_timeout 60s;
keepalive_requests 1000;
# 反向代理到后端时同样重要
}
upstream backend {
server 127.0.0.1:9000;
keepalive 32;
}尤其容易漏的是 upstream 里的 keepalive 32;,以及代理时把 Connection 头改成 close 的老写法。很多老教程里写着 proxy_set_header Connection "close";,这一行会强制每次请求新建到后端的连接,彻底废掉 upstream keepalive。改成 proxy_set_header Connection ""; 才是对的。
场景三:等待连接不多但响应整体缓慢
如果 Reading/Writing/Waiting 都很正常,Active 也不高,但用户就是觉得慢,那说明瓶颈不在 Nginx 连接层。结合 upstream 的响应头看 X-Cache 或者自己加的 X-Upstream-Time,就能把问题甩给后端。这正是 stub_status 的价值:它不是万能诊断器,但它能快速做「排除法」,让你三步之内知道该往哪个方向查,而不是盲目重启服务。
五、用脚本把状态页变成可告警的指标
状态页本身不会通知你,需要一层极简采集。下面这个 Bash 脚本不依赖任何监控系统,用 cron 每分钟跑一次,异常时输出告警(可以接钉钉/企业微信/邮件):
#!/bin/bash
# /usr/local/bin/nginx_status_check.sh
STATUS=$(curl -s --max-time 3 http://127.0.0.1:8080/nginx_status)
if [ -z "$STATUS" ]; then
echo "[告警] Nginx 状态页无响应,服务可能已挂"
exit 1
fi
ACTIVE=$(echo "$STATUS" | awk '/Active/{print $3}')
ACCEPT=$(echo "$STATUS" | awk 'NR==3{print $1}')
HANDLE=$(echo "$STATUS" | awk 'NR==3{print $2}')
WAIT=$(echo "$STATUS" | awk '/Waiting/{print $4}')
DROP=$((ACCEPT - HANDLE))
MSG="Active=$ACTIVE Waiting=$WAIT accept=$ACCEPT handled=$HANDLE drop=$DROP"
# 阈值按自己站点规模调整
if [ "$ACTIVE" -gt 500 ]; then
echo "[告警] 活跃连接过高 $MSG"
elif [ "$DROP" -gt 0 ]; then
echo "[告警] 存在被丢弃的连接 $MSG"
else
echo "[正常] $MSG"
fi再配合 crontab 每分钟执行,并让脚本在告警时通过 webhook 推送。这里有个细节:awk 'NR==3{print $1}' 取的是第三行,因为第二行是标题文字「server accepts handled requests」。如果你改了版本号导致行数变化,用 grep -A1 会更稳,但 awk 按行号定位在 stub_status 上一直很可靠。
六、长期趋势比瞬时值更有价值
单看某一分钟的数字意义有限,真正有价值的是趋势。最简单的做法是让脚本把每次采集的结果追加到日志:
echo "$(date '+%F %T') $ACTIVE $ACCEPT $HANDLE $WAIT" >> /var/log/nginx_status.log积累几天后,你可以用 awk 快速看每天的高峰连接数:
awk '{print $1, $3}' /var/log/nginx_status.log | sort -k2 -n | tail -20这样你就有了一条「自己站点的连接基线」。有了基线,告警阈值可以设得更准,而不是抄别人的 500 或 1000。同时你还能验证优化是否真的有效:比如改了 keepalive 配置后,观察 requests/handled 比值是否从 1.0 上升到 3~5,这比任何主观感受都可靠。
七、几个容易踩的坑
- 忘了 access_log off。 如果你用脚本每分钟 curl 一次状态页又不关日志,一年下来会白白多出几十万条日志,logrotate 都会被这些噪音填满。
- 把状态页暴露在公网。 见过有人图方便直接
location /nginx_status挂在 443 上,结果被人扫到并用来估算攻击收益。务必只监听内网或 localhost。 - 把 Active connections 当成在线人数。 它不是 UV,也不是同时在线数。用它做业务汇报会闹笑话。
- 忽略 handled 与 accepts 的差值。 这个差值不为零就是明确的容量告警,比 Active 数更早预警。
- 只看状态页就下结论。 stub_status 是「第一层筛选器」,它告诉你去查哪里,不告诉你根因。真正的根因往往在 PHP-FPM、MySQL 或出口带宽上。
八、进阶:与日志和系统指标交叉验证
当状态页提示连接堆积时,最快的交叉验证是三样东西一起看:ss -s 看系统层面的连接统计,ss -lnt 看 socket 队列有没有溢出(Send-Q 不为 0 就是 accept 队列满了),以及 Nginx error log 里的 worker_connections are not enough。这三者与 stub_status 的数字互相印证,基本可以在五分钟内锁定问题层次。
对于流量更大一点的站点,还可以把 stub_status 的数字通过 nginx-prometheus-exporter 暴露给 Prometheus,做长期可视化和精细告警。但对绝大多数个人站来说,上面那套 Bash + cron + 日志的方案已经足够:零依赖、可读、可告警、可回溯,正是个人站长最需要的性价比。
小结
stub_status 只有寥寥几个数字,却是整个 Nginx 监控体系里性价比最高的一环。它不需要额外组件,配置五行以内,定位的是最关键的「连接层是否健康」。掌握 Active = Reading + Writing + Waiting 这个恒等式、关注 accepts 与 handled 的差值、用 requests/handled 判断 keepalive 是否生效,这三条就足以让你在服务器变慢时,三分钟内知道该往哪个方向走。把它接好,再配上一个每分钟跑一次的轻量告警脚本,你的站点就从「被动救火」升级到「主动预警」了。
常见问题
Q:stub_status 和 GoAccess 有什么区别?
A:GoAccess 分析的是访问日志,是「事后统计」,能告诉你谁来了、看了什么;stub_status 是「实时连接状态」,告诉你此刻服务是否健康。两者互补,不能替代。
Q:状态页里数字一直不变怎么办?
A:先确认你 curl 到的是正确的 location,其次检查是否被前面的 location 规则拦截了,最后确认 stub_status 指令没被注释。Nginx 的 location 匹配优先级高于普通前缀匹配的是 = 精确匹配,所以用 location = /nginx_status 最不容易被抢走。
Q:每次 reload Nginx,计数器会清零吗?
A:accepts/handled/requests 是随 worker 进程存的,reload 会启动新 worker 并平滑退出旧 worker,计数器会随新 worker 从零开始。所以采集脚本不要假设这些数字单调递增,趋势分析时要注意 reload 造成的中断。