服务器如何自动检测网站是否正常运行?HTTP状态码与服务巡检脚本

服务器活着,不等于网站能用。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、别给生产接口造成压力。