服务器活着,不等于网站能用。CPU 正常、进程在跑、端口在听,用户打开页面却可能是 502、504 或者干脆白屏。这种"看上去一切正常,实际服务已挂"的情况,是监控里最容易被漏掉的一类。真正的可用性判断,必须站到用户视角去发起一次真实请求,看拿到的是什么结果。
这篇文章讲怎么用 Shell 脚本自动检测网站是否正常:怎么取 HTTP 状态码、怎么看响应时间、怎么判断是"真故障"还是"偶发抖动"、怎么区分"应用挂了"和"依赖挂了"。脚本用 curl 实现,兼容各发行版,可以直接挂定时任务或接入监控系统。
文中提到的响应时间阈值、连续失败次数都是经验值,不同业务差异很大,请结合自己的基线调整。涉及证书忽略、重试策略的地方会说明风险,避免把"忽略证书校验"当成常规操作。
一、什么叫"网站正常运行"
先把判断标准说清楚,否则脚本写出来也是拍脑袋。一个网站的"可用"至少包含四层,缺一不可:
| 检查层次 | 判断内容 | 手段 | 说明 |
|---|---|---|---|
| 端口可达 | 服务器端口能连上 | nc、ss |
最底层,通了不代表服务好 |
| HTTP 状态码 | 返回 200/301/302 等预期码 | curl -o /dev/null -w |
最常用的可用性指标 |
| 响应内容 | 页面含关键字、大小合理 | curl + grep |
防"200 但返回错误页" |
| 响应时间 | 耗时在基线范围内 | curl -w %{time_total} |
防"能开但慢到不可用" |
很多人只做第二层。但只有状态码是不够的:有些应用出错时会返回 200 加一个"系统繁忙"的页面,状态码骗过了监控,用户却在骂娘。所以内容校验(检查页面里是否包含预期关键字)很关键,哪怕只检查一个字符串,也能挡住一大类"假 200"。
另外还要区分状态码的语义。"2xx/3xx"通常算正常,"4xx"要分情况(404 可能是资源没了,401/403 是权限问题,但这些通常是内容层面而非服务故障),"5xx"基本可以确认是服务端问题。脚本的判定逻辑要按这个来。
二、curl 取状态码的正确姿势
curl 是脚本里做 HTTP 探测的标准工具。要干净地拿到状态码和耗时,关键是配合 -w(write-out)和 -o /dev/null。
# 只取状态码
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/
# 取多个指标:状态码、总耗时、DNS解析、连接时间、重定向次数
curl -s -o /dev/null -w \
"code=%{http_code} total=%{time_total}s dns=%{time_namelookup}s connect=%{time_connect}s redirect=%{num_redirects}\n" \
https://example.com/
# 带超时,避免脚本卡死(连接超时 5s,总超时 10s)
curl -s -o /dev/null -m 10 --connect-timeout 5 -w "%{http_code}\n" https://example.com/
-m(max-time)和 --connect-timeout 一定要加。没有超时的 curl 遇到网络黑洞时会一直挂着——脚本卡住不返回,定时任务就会越积越多。
几个常用参数说明:
| 参数 | 含义 | 备注 |
|---|---|---|
-s |
静默,不输出进度 | 脚本里必加 |
-o /dev/null |
丢弃响应体 | 只要状态码时用 |
-w "%{http_code}" |
输出指定变量 | 可组合多个变量 |
-m 10 |
总超时 10 秒 | 防卡死 |
--connect-timeout 5 |
连接超时 | 区分连接阶段 |
-L |
跟随重定向 | 需要看最终页时用 |
-k |
忽略证书错误 | 仅临时排障,勿用于长期监控 |
-A |
指定 User-Agent | 防被 WAF 拦截 |
-H |
附加请求头 | 如 Host:、Authorization: |
关于 -k 要特别提醒:忽略证书校验等于放弃了 HTTPS 的核心保护,用户流量可能被中间人窃听。只在临时排障、确认是自己证书过期时用一次,绝不能写进长期运行的监控脚本。如果监控必须用 IP 直连,更推荐用 --resolve 把域名解析到指定 IP,同时保留证书校验:
# 用 IP 直连但保留域名校验(推荐)
curl -s -o /dev/null --resolve example.com:443:1.2.3.4 -w "%{http_code}\n" https://example.com/
三、完整网站巡检脚本
下面这份脚本支持多个站点、内容关键字校验、响应时间阈值,并对每个站点做"连续失败 N 次才告警"的处理,避免单次抖动误报。
#!/usr/bin/env bash
# 网站可用性巡检脚本
# 用法:bash web_check.sh
set -uo pipefail
# ---- 配置区(经验值,按需调整)----
FAIL_THRESHOLD=3 # 连续失败次数达到才告警
TIME_WARN=3.0 # 总耗时超过此秒数视为"慢"(经验值)
STATE_DIR="/var/lib/web_check"
mkdir -p "$STATE_DIR"
MAIL_TO="ops@example.com"
# 站点列表:名称|URL|期望状态码|期望包含的关键字
SITES=(
"主站|https://example.com/|200|示例"
"API|https://api.example.com/health|200|ok"
"后台|https://admin.example.com/login|200|登录"
)
ALERT_BODY=""
for item in "${SITES[@]}"; do
IFS='|' read -r name url want_code want_kw <<< "$item"
safe=$(echo "$name" | tr -c 'A-Za-z0-9' '_')
state_file="$STATE_DIR/$safe.fail"
# 发请求,同时取状态码、耗时、响应体(前若干字节)
tmp_body=$(mktemp)
code=$(curl -s -o "$tmp_body" -m 10 --connect-timeout 5 \
-A "Mozilla/5.0 (MonitorBot)" \
-w "%{http_code}|%{time_total}" "$url" 2>/dev/null || echo "000|0")
http_code=${code%%|*}
time_total=${code##*|}
ok=1
reason=""
if [ "$http_code" != "$want_code" ]; then
ok=0; reason="状态码=$http_code(期望$want_code)"
elif ! grep -q "$want_kw" "$tmp_body"; then
ok=0; reason="页面未包含关键字[$want_kw]"
fi
# 响应时间单独判断(慢但不算故障)
slow=""
awk -v t="$time_total" -v w="$TIME_WARN" 'BEGIN{exit !(t>w)}' && slow="(慢:${time_total}s)"
rm -f "$tmp_body"
if [ "$ok" -eq 1 ]; then
echo "✅ $name 正常 code=$http_code ${time_total}s $slow"
echo 0 > "$state_file"
else
cnt=$(cat "$state_file" 2>/dev/null || echo 0)
cnt=$((cnt + 1))
echo "$cnt" > "$state_file"
echo "❌ $name 异常 $reason ${time_total}s (连续第 $cnt 次)"
if [ "$cnt" -ge "$FAIL_THRESHOLD" ]; then
ALERT_BODY="${ALERT_BODY}${name} (${url}): ${reason},已连续失败 ${cnt} 次\n"
fi
fi
done
if [ -n "$ALERT_BODY" ]; then
printf "主机: %s\n时间: %s\n\n%b\n" "$(hostname -s)" "$(date '+%F %T')" "$ALERT_BODY" \
| mail -s "[站点告警] $(hostname -s) 有站点不可用" "$MAIL_TO"
fi
脚本的三个关键点:关键字校验能抓"200 但内容不对";连续失败计数把结果存到文件,达到阈值才告警,滤掉网络抖动;响应时间单独提示,慢但不挂的站点也要让人知道,这在容量评估时很有用。
四、区分应用故障和依赖故障
站点报 502/504 时,问题可能出在应用、也可能是它依赖的数据库、缓存、上游服务。脚本层面能做的是"分层探测",快速定位是第一层还是后面几层的问题。
#!/usr/bin/env bash
# 分层探测:定位 502/504 的来源
URL="https://example.com/"
DB_HOST="127.0.0.1"
DB_PORT=3306
REDIS_HOST="127.0.0.1"
REDIS_PORT=6379
code=$(curl -s -o /dev/null -m 10 -w "%{http_code}" "$URL")
echo "HTTP 状态: $code"
case "$code" in
502|503|504)
echo "-> 服务端错误,逐层排查依赖:"
# 1) 本机 Web 服务是否在听(以 80/443 为例)
ss -lntH 'sport = :443' | grep -q . && echo " [OK] 443 在监听" || echo " [!!] 443 未监听,Web 服务可能没起来"
# 2) 应用进程是否存在
pgrep -f 'nginx|apache|php-fpm|java' >/dev/null && echo " [OK] 应用进程存在" || echo " [!!] 应用进程缺失"
# 3) 数据库端口
nc -z -w3 "$DB_HOST" "$DB_PORT" && echo " [OK] 数据库 $DB_HOST:$DB_PORT 可达" || echo " [!!] 数据库不可达"
# 4) 缓存端口
nc -z -w3 "$REDIS_HOST" "$REDIS_PORT" && echo " [OK] 缓存 $REDIS_HOST:$REDIS_PORT 可达" || echo " [!!] 缓存不可达"
# 5) 最近错误日志
echo " 最近应用错误(示例路径):"
tail -n 5 /var/log/nginx/error.log 2>/dev/null
;;
000)
echo "-> 请求未得到响应:检查 DNS、网络连通性与防火墙"
;;
*)
echo "-> 其他状态码,按业务预期判断"
;;
esac
nc -z -w3 用来测端口连通性,-w3 是连接超时 3 秒。如果系统没装 nc:RHEL 系 dnf install -y nc,Debian 系 apt install -y netcat-openbsd。也可以用 bash 内置的 /dev/tcp 探测,不依赖额外软件:
# 用 bash 内置 TCP 探测(无需 nc)
timeout 3 bash -c '/dev/null && echo "可达" || echo "不可达"
五、接入定时任务与告警
站点巡检通常每 1-5 分钟一次。太稀会漏掉短时故障,太密会产生大量请求(注意别把自己站点打挂,也别触发 WAF 限流)。经验上单站点每分钟 1 次、多站点错开时间是合理的选择。
# 系统级定时任务,每分钟检查
sudo tee /etc/cron.d/web_check >/dev/null <<'EOF'
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
* * * * * root /usr/local/bin/web_check.sh >/dev/null 2>&1
EOF
sudo chmod 644 /etc/cron.d/web_check
# 或者用 systemd timer,带随机延迟避免同秒并发
# /etc/systemd/system/web_check.service
[Unit]
Description=Website availability check
[Service]
Type=oneshot
ExecStart=/usr/local/bin/web_check.sh
# /etc/systemd/system/web_check.timer
[Unit]
Description=Run website check every minute
[Timer]
OnBootSec=1min
OnUnitActiveSec=1min
AccuracySec=5s
[Install]
WantedBy=timers.target
如果要检测多个站点的整体可用性,更好的是用专业探针(Prometheus 的 Blackbox Exporter、Uptime 类工具),它们支持从多个地域探测、记录历史可用率、生成 SLA 报表。Shell 脚本更适合做"轻量补充"和"自定义校验"。
六、风险提示与常见误区
别频繁请求生产接口。如果监控请求会触发写操作或有副作用的逻辑(比如每次请求都写库),高频巡检本身就是压力。健康检查应该指向专门的无副作用端点(如 /health)。
别忽略证书有效期。检查 HTTPS 站点时顺便看一眼证书到期时间,快到期就告警,能避免"证书过期导致全站不可用"这种低级事故。
# 查看证书到期时间
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -dates
# 一句话算剩余天数
exp=$(echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
echo "剩余天数: $(( ($(date -d "$exp" +%s) - $(date +%s)) / 86400 ))"
告警要有去重和恢复通知。和磁盘告警一样,站点频繁"挂-恢复-挂"会产生大量邮件。建议对同一站点采用连续 N 次才告警、恢复时发一次通知的策略。
回滚方式:巡检脚本是只读的,停止它只需删除 /etc/cron.d/web_check 或禁用对应的 systemd timer;状态目录 /var/lib/web_check/ 可整体删除重置计数。
常见问题
Q1:状态码返回 000 是什么意思?
curl 的 %{http_code} 在没有收到任何 HTTP 响应时返回 000,常见原因包括:DNS 解析失败、连接被拒、连接超时、TLS 握手失败。它不代表某个具体 HTTP 错误,而是"请求根本没走到 HTTP 层",排查要从网络和证书入手。
Q2:为什么站点正常,脚本却报失败?
常见三种原因:一是脚本没带正常浏览器 User-Agent,被 WAF 拦截;二是站点需要登录或有防盗链,直接请求返回 403/跳转;三是关键字写错或页面改版后关键字消失。加 -A 指定 UA、必要时带 Cookie/请求头,或改用专门的健康检查端点,都能解决。
Q3:如何判断是"真挂了"还是"网络抖动"?
用连续失败计数。单次失败不告警,连续 N 次(经验值 3 次)失败再告警,能滤掉绝大多数瞬时抖动。也可以从多个源探测,如果只有一处失败,多半是探测点网络问题而非站点故障。
Q4:能忽略证书错误吗?
技术上可以(curl -k),但强烈不建议用于长期监控——它会让监控失去发现证书问题的能力,也违背 HTTPS 初衷。如果必须用 IP 直连,用 --resolve 指定 IP 同时保留证书校验,是更安全的做法。
Q5:只检查 HTTP 状态码够吗?
不够。一定要加内容关键字校验,防止"200 但返回错误页"的假阳性。响应时间和证书有效期也建议一并检查。对关键业务,最好从多个地域、用真实浏览器内核(无头浏览器)做端到端探测。
总结
- "服务器活着"不等于"网站可用",可用性要从用户视角发起真实请求来判断。
- 判断标准分四层:端口可达、HTTP 状态码、响应内容、响应时间,只做状态码会漏掉"假 200"。
- 用
curl -s -o /dev/null -m 10 -w "%{http_code}|%{time_total}"稳定取数,必须设置超时防止脚本卡死。 - 结合关键字校验、连续失败计数、响应时间提示,能显著降低误报并发现"慢但不挂"的隐患。
- 502/504 要分层排查 Web 进程、数据库、缓存;巡检频率以每分钟一次量级为宜,并注意别触发 WAF、别给生产接口造成压力。
企业QQ咨询




