网站"能打开"却有人打不开:分层测量排障实战,curl 时间解剖、SSH 队列堆积与故障现场采集脚本

为什么"能访问"不等于"没问题"

我维护这台小服务器的第七个年头,踩过最多的坑不是"网站打不开",而是"网站能打开,但某些人打不开"。这类问题最折磨人:你自己刷新十遍都正常,客户却截图给你看一个 504;你在电脑上跑得好好的,手机连着 4G 就是转圈。你第一反应是"他网络有问题",第二反应是"再等等看",然后就拖成了一次真实的收入损失。

这篇文章不讲抽象理论,只讲一套我在真实事故里反复用过的排障方法。核心思路是:不要相信"我这里正常",要把"我这里"和"他那里"之间的每一段路径都拆开测量。掌握了这套方法,绝大多数"玄学故障"半小时内就能收敛到具体环节。

先分层,再动手:把一次访问切成五段

一个访客打开你的页面,数据要经过下面五段路。故障排查的第一步不是敲命令,而是在纸上写下这五段,然后一段一段排除:

  • 第一段:访客到本地网络出口(家里的路由器、公司的网关、手机基站)
  • 第二段:公网传输(运营商的骨干、中间可能有的 CDN 节点)
  • 第三段:你的服务器入口(防火墙、Nginx 监听、TLS 握手)
  • 第四段:服务器内部处理(PHP-FPM、数据库、磁盘 IO)
  • 第五段:响应回程(数据回到访客浏览器并渲染)

很多人一上来就重启 Nginx,是因为没有分层概念,只能靠"碰"。有了分层,你至少知道该去哪里找证据。

第一件工具:用 curl 的 -w 拿到精确的时间解剖

这是我最常用的诊断命令,比任何图形化工具都快。它会把一次请求拆成若干个时间点:

curl -o /dev/null -s -w '
DNS解析:      %{time_namelookup}s
TCP连接:      %{time_connect}s
TLS握手完成:  %{time_appconnect}s
首字节返回:   %{time_starttransfer}s
整个请求结束: %{time_total}s
HTTP状态码:   %{http_code}
' "https://www.example.com/?_=$(date +%s)"

怎么读这些数字,才是关键:

  • time_namelookup 大(超过 0.3 秒):问题在 DNS。换本地 DNS 服务器,或者检查域名的 NS 是否响应慢。
  • time_connect 减去 time_namelookup 大:TCP 三次握手慢,通常是网络路径拥堵或服务器带宽被打满。
  • time_appconnect 减去 time_connect 大:TLS 握手慢,多半是证书链文件太大、没开 OCSP Stapling,或者服务器 CPU 被占满导致加密计算慢。
  • time_starttransfer 减去 time_appconnect 大:这是最要命的一段 —— 服务器内部处理慢。请求已经进来了,但你的 PHP 或数据库还没算完。
  • time_total 减去 time_starttransfer 大:传输阶段慢,说明返回的内容太大、或回程网络差。

把这五个数字记下来,你就知道该往哪个方向挖了。我见过一个案例,全站"偶尔慢",最后发现是 time_appconnect 占比过高 —— 原因是服务器上跑了个吃 CPU 的爬虫任务,TLS 握手算不动。杀掉任务,问题消失。如果不分层测量,你可能会傻傻地去优化数据库。

第二件工具:从两端分别打点

curl 还有一个被严重低估的用法 —— 强制指定解析结果,绕开 DNS:

# 直接连源站 IP,绕开 CDN
curl -o /dev/null -s -w '状态:%{http_code} 首字节:%{time_starttransfer}s\n' \
  --resolve "www.example.com:443:1.2.3.4" \
  "https://www.example.com/?_=$(date +%s)"

# 对比:走正常 DNS(经过 CDN)
curl -o /dev/null -s -w '状态:%{http_code} 首字节:%{time_starttransfer}s\n' \
  "https://www.example.com/?_=$(date +%s)"

如果直连源站一切正常,走 CDN 就走不通,那问题百分之百在 CDN 那一层(回源配置、缓存规则、节点故障)。这个对比能帮你省掉大量瞎猜的时间。

反过来,如果源站本身也慢,就要进服务器看了。这时候我会同时在服务器上敲:

tail -f /var/log/nginx/access.log

然后让客户再访问一次。如果日志里压根没出现这条请求,说明请求根本没到服务器(问题在第一到第三段);如果出现了但耗时很长,说明卡在第四段;如果日志里状态码是 200 且耗时很短,而你那边还是很慢,说明卡在第五段(回程或浏览器渲染)。

第三件工具:用 ss 看连接队列有没有堆积

有些"慢"不是计算慢,而是请求在排队。Linux 内核里有两个关键队列:半连接队列(SYN queue)和全连接队列(accept queue)。队列满了,客户端就会感觉"连接很慢或直接超时",而服务器 CPU 和内存看起来都很闲。

# 看 TCP 连接状态汇总
ss -s

# 看监听套接字的队列堆积情况(Recv-Q / Send-Q)
ss -lnt

# 只看 443 端口
ss -lnt sport = :443

判断口诀很简单:对于监听状态的套接字,Recv-Q 表示当前已建立但还没被应用取走的连接数,Send-Q 表示 backlog 上限。如果 Recv-Q 持续接近 Send-Q,说明你的应用(Nginx、PHP)处理不过来了,请求在排队。

我遇到过一次典型的队列溢出:一个促销页面突然来了几千个并发,Nginx 的 worker 连接数配置太小,导致 Recv-Q 一直贴着上限。表面现象是"部分用户打不开",实际是连接还没来得及被处理就被丢弃了。调大 backlog 和 worker 配置后立刻恢复。

服务器端要看的三样东西

当 curl 已经证明问题在第四段,我按固定顺序看三样:

# 1. 系统整体负载与 IO 等待
uptime
vmstat 1 5

# 2. 谁在吃 CPU / 内存
top -b -n 1 | head -20

# 3. PHP-FPM 进程池是否打满
ps -ef | grep php-fpm | wc -l
tail -100 /var/log/php-fpm/www-error.log

vmstat 里最重要的两个数是 r(等待运行的进程数)和 wa(IO 等待占比)。r 长期大于 CPU 核数说明 CPU 不够用;wa 高说明卡在磁盘 IO。这两个数字能把"服务器慢"快速归类。

如果是 PHP 站,还要看数据库慢查询。很多"网站慢"的根因其实是一条没加索引的 SQL:

# 打开慢查询日志(临时开启,排查完记得关)
mysql -uroot -p -e "
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL log_output = 'FILE';
SHOW VARIABLES LIKE 'slow_query_log_file';"

# 分析慢日志(需先安装 percona-toolkit)
pt-query-digest /var/log/mysql/slow.log | head -50

把排查固化成脚本:一次性收集现场

临时敲命令最大的问题是:等你敲完,故障已经自己恢复了,现场没了。所以我写了个一键采集脚本,出问题时先跑它,把现场存成文件再慢慢看:

#!/bin/bash
# server-snapshot.sh —— 故障现场一键采集
OUT="/tmp/snapshot_$(date +%Y%m%d_%H%M%S).txt"
{
  echo "===== 采集时间: $(date '+%F %T') ====="
  echo "----- 系统负载 -----";  uptime
  echo "----- 内存 -----";      free -m
  echo "----- 磁盘 -----";      df -h
  echo "----- 虚拟内存统计 -----"; vmstat 1 3
  echo "----- TCP 汇总 -----";  ss -s
  echo "----- 监听队列 -----";  ss -lnt
  echo "----- CPU TOP10 -----"
  ps -eo pid,comm,%cpu,%mem --sort=-%cpu | head -11
  echo "----- 内存 TOP10 -----"
  ps -eo pid,comm,%cpu,%mem --sort=-%mem | head -11
  echo "----- Nginx 错误日志尾部 -----"
  tail -50 /var/log/nginx/error.log 2>/dev/null
  echo "----- PHP-FPM 错误日志尾部 -----"
  tail -50 /var/log/php-fpm/www-error.log 2>/dev/null
} > "$OUT" 2>&1
echo "已保存到: $OUT"

这个脚本我建议做成一个别名放进 /root/.bashrc,出故障的第一秒就跑,比任何"回忆式排查"都可靠。

给个人站长的三条经验

这套方法用了几年,我最想留给同行的是下面三条经验,它们比任何命令都值钱:

一、永远先建立基线

没有基线就没有"异常"。你必须在网站健康的时候记录一份正常数据:正常时的首字节时间是多少?正常时的连接数是多少?正常时的内存占用是多少?我习惯每天凌晨用一条 curl 把 time_total 写进日志文件,一个月后你就有了自己网站的"体温曲线"。故障来时,一眼就能看出偏差。

# 每天记录一条基线(放进 crontab)
0 3 * * * curl -o /dev/null -s -w "%{time_total}\n" \
  "https://www.example.com/" >> /var/log/site_baseline.log

二、一次只改一个变量

排障的大忌是"一口气改五个地方然后重启"。如果问题好了,你不知道是哪一个改动起的作用;如果没好,你也不知道是哪个改动引入了新问题。正确的做法是:改一个、验证一个、记录一个。慢一点,但每一步都算数。

三、让日志说话,别让记忆说话

人脑在故障时的判断力是最差的。我见过太多"我确定是数据库问题",最后发现是磁盘满了。养成习惯:任何结论都要有一条日志或一个命令输出作为证据。没有证据的判断,就是猜测。

小结

"我这里能打开"这句话在故障排查里毫无价值。真正有价值的是:用 curl 把一次请求拆成五段时间,用第一段和第三段的对比判断问题是本地还是远端,用 ss 和 vmstat 判断是排队还是计算,最后用脚本把现场固定下来。这套流程不依赖任何昂贵工具,一台普通 VPS 就能跑,但它能把"玄学故障"变成一道道可以验证的工程题。这,就是一个草根站长和"重启大法选手"之间最真实的差距。

Last modification:September 24th, 2026 at 12:23 pm

Leave a Comment