找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2242

积分

0

好友

298

主题
发表于 3 小时前 | 查看: 3| 回复: 0

概述

1.1 背景介绍

凌晨 3 点,手机响了。告警内容只有一行:“服务 X 不可用,持续 5 分钟”。你打开笔记本,开始排查。DNS 正常吗?负载均衡后端健康吗?网关有没有报错?应用进程还活着吗?数据库连接池满了没有?每一层都可能是断点,每一层都需要不同的排查手段。

服务不可用是生产环境最严重的故障类型之一。从告警触发到服务恢复,整个过程涉及的组件可能横跨 DNS、负载均衡器、API 网关、应用服务、数据库甚至底层基础设施。问题的复杂性在于,你看到的现象(比如 HTTP 502)可能只是最上层的表现,根因藏在链路的任何一个环节。

这篇文章把“服务不可用”这个宽泛的问题拆成一条可执行的排查链路。从告警触发开始,快速确认影响范围,沿着 DNS → LB → 网关 → 应用 → DB 的方向逐层排查,找到根因后止血、修复、验证。配合 Prometheus 3.x + Grafana 11.x 的监控体系,建立从发现到恢复的完整闭环。

1.2 基础背景 / 核心语义

术语 含义 排障关注点
MTTD Mean Time To Detect,平均发现时间 告警规则灵敏度和覆盖率
MTTR Mean Time To Recover,平均恢复时间 排障效率和修复速度
止血 用最快手段恢复服务,不一定修根因 重启、回滚、切流量、扩容
根因 Root Cause,故障的底层真正原因 区别于表面现象
爆炸半径 Blast Radius,故障影响的范围 影响哪些服务、用户、地域
SLO Service Level Objective,服务级别目标 可用性目标,如 99.9%
Error Budget 在 SLO 范围内允许的故障时间 月度/季度允许宕机时长
Runbook 标准操作手册 预定义的排障步骤和修复动作

1.3 适用场景

  • 收到“服务不可用”告警后的标准化排查流程
  • 全链路服务中断的根因定位
  • 部分降级(慢响应、间歇性错误)的排查
  • 故障复盘中的排查链路回溯
  • 新运维人员的排障培训教材

1.4 环境要求

组件 版本 说明
Prometheus 3.x 指标采集和告警引擎
Grafana 11.x 可视化和告警通知
Alertmanager 0.28.x 告警路由和抑制
Node Exporter 1.9.x 主机指标采集
Blackbox Exporter 0.25.x 外部探测(HTTP/TCP/DNS)
操作系统 Ubuntu 24.04 / Rocky 9.5 主机环境
Nginx 1.27.x 网关/反向代理层
PostgreSQL / MySQL 16.x / 8.4.x 数据库层

1.5 排障坐标系

告警触发 → 影响确认 → 链路定位 → 根因分析 → 止血 → 修复 → 验证
                         │
           ┌─────────────┼─────────────┐
           ▼             ▼             ▼
     DNS/LB/网关     应用/中间件      DB/存储
     ┌──────┐       ┌──────┐       ┌──────┐
     │解析  │       │进程  │       │连接池│
     │证书  │       │OOM   │       │慢查询│
     │后端池│       │线程池│       │锁等待│
     │路由  │       │依赖  │       │主从  │
     └──────┘       └──────┘       └──────┘

核心原则:先止血后找根因。服务恢复是第一优先级,根因分析可以在服务恢复后进行。但止血前必须保存现场证据。

详细步骤

2.1 先把观测面补齐

2.1.1 告警确认和影响评估

收到告警后的前 60 秒应该完成以下动作:

# 1. 确认告警真实性(排除误报)
# 从外部探测服务可达性
curl -s -o /dev/null -w "HTTP: %{http_code}, DNS: %{time_namelookup}s, Connect: %{time_connect}s, Total: %{time_total}s\n" \
    https://api.example.com/health

# 2. 多地域探测(排除单点网络问题)
for region in "直连" "通过CDN" "备用DNS"; do
echo -n "${region}: "
    curl -s -o /dev/null -w "%{http_code} %{time_total}s" --max-time 10 https://api.example.com/health
echo ""
done

# 3. 快速确认影响范围
# 是全站不可用还是部分接口?
curl -s -o /dev/null -w "%{http_code}" https://api.example.com/health
curl -s -o /dev/null -w "%{http_code}" https://api.example.com/api/users
curl -s -o /dev/null -w "%{http_code}" https://www.example.com/

2.1.2 快速采集全局状态

# 查看所有关键服务的进程状态
for svc in nginx app-service postgresql redis; do
echo -n "${svc}: "
    systemctl is-active ${svc} 2>/dev/null || echo "not found"
done

# 查看系统资源概况
echo "=== CPU ==="
uptime
echo "=== Memory ==="
free -h
echo "=== Disk ==="
df -h / /var /data 2>/dev/null
echo "=== Network ==="
ss -s
echo "=== Load ==="
cat /proc/loadavg

2.1.3 Prometheus 查询关键指标

# 通过 Prometheus API 查询最近 5 分钟的请求成功率
curl -s "http://prometheus:9090/api/v1/query?query=sum(rate(http_requests_total{code=~\"2..\"}[5m]))/sum(rate(http_requests_total[5m]))"

# 查询错误率
curl -s "http://prometheus:9090/api/v1/query?query=sum(rate(http_requests_total{code=~\"5..\"}[5m]))/sum(rate(http_requests_total[5m]))"

# 查询 P95 延迟
curl -s "http://prometheus:9090/api/v1/query?query=histogram_quantile(0.95,sum(rate(http_request_duration_seconds_bucket[5m]))by(le))"

2.2 第一轮判断

根据告警类型和探测结果快速分流:

现象 初步判断 下一步
curl 返回 DNS 解析失败 DNS 故障 检查 DNS 解析链路
curl 返回连接超时 LB 或网络故障 检查 LB 后端健康和网络路径
curl 返回 502/503 网关正常但后端不可用 检查应用层
curl 返回 500 应用内部错误 检查应用日志
curl 返回 200 但数据异常 应用逻辑或数据层问题 检查数据库和缓存
部分接口 5xx,其他正常 特定依赖故障 检查故障接口的依赖链
所有地域都不可用 源站故障 直接排查源站
仅特定地域不可用 CDN/DNS/网络故障 检查该地域的网络链路

2.2.1 DNS 层排查

# 检查 DNS 解析
dig api.example.com +short
dig api.example.com @8.8.8.8 +short

# 检查 DNS 解析时间
dig api.example.com | grep "Query time"

# 检查是否有 NXDOMAIN 或 SERVFAIL
dig api.example.com +noall +answer +comments

# 检查本地 DNS 缓存和 /etc/resolv.conf
cat /etc/resolv.conf
resolvectl status 2>/dev/null || systemd-resolve --status 2>/dev/null

# 检查 DNS TTL(判断是否缓存了旧记录)
dig api.example.com +noall +answer | awk '{print "TTL:", $2}'

2.2.2 负载均衡层排查

# 检查 LB(以 Nginx 为例)后端健康状态
curl -s http://lb-host/stub_status

# 查看 LB 的 error_log
tail -50 /var/log/nginx/error.log

# 检查 upstream 节点连通性
for backend in 10.0.1.10:8080 10.0.1.11:8080 10.0.1.12:8080; do
echo -n "${backend}: "
    curl -s -o /dev/null -w "%{http_code} %{time_total}s" --max-time 5 http://${backend}/health
echo ""
done

2.3 第二轮下钻

2.3.1 应用层排查

# 检查应用进程
systemctl status app-service
ps aux | grep app-service

# 检查应用端口监听
ss -tlnp | grep 8080

# 查看应用日志最近 50 行
journalctl -u app-service -n 50 --no-pager

# 检查应用线程/连接状态
# Java 应用
jstack $(pgrep -f app-service) | grep -c "BLOCKED"
jstat -gc $(pgrep -f app-service)

# Python 应用
ls /proc/$(pgrep -f app-service)/fd | wc -l

# Go 应用(通过 pprof)
curl -s http://localhost:6060/debug/pprof/goroutine?debug=1 | head -20

# 检查 OOM 记录
dmesg | grep -i "oom\|killed" | tail -10
journalctl -k | grep -i "oom\|killed" | tail -10

2.3.2 数据库层排查

# PostgreSQL
sudo -u postgres psql -c "SELECT count(*) as total_connections, state FROM pg_stat_activity GROUP BY state;"
sudo -u postgres psql -c "SELECT pid, now() - pg_stat_activity.query_start AS duration, query, state FROM pg_stat_activity WHERE (now() - pg_stat_activity.query_start) > interval '30 seconds' AND state != 'idle' ORDER BY duration DESC LIMIT 10;"
sudo -u postgres psql -c "SELECT blocked_locks.pid AS blocked_pid, blocking_locks.pid AS blocking_pid, blocked_activity.query AS blocked_query FROM pg_catalog.pg_locks blocked_locks JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid JOIN pg_catalog.pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype AND blocking_locks.relation = blocked_locks.relation JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid WHERE NOT blocked_locks.granted LIMIT 10;"

# MySQL
mysql -e "SHOW PROCESSLIST;" | head -20
mysql -e "SELECT * FROM information_schema.INNODB_TRX WHERE trx_state = 'LOCK WAIT';"
mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_connected';"
mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_running';"

2.3.3 中间件和依赖排查

# Redis
redis-cli ping
redis-cli info memory | grep used_memory_human
redis-cli info clients | grep connected_clients
redis-cli slowlog get 10

# RabbitMQ / Kafka
# 检查消息队列积压
rabbitmqctl list_queues name messages_ready messages_unacknowledged | sort -rnk2 | head -10

# 检查外部 API 依赖
curl -s -o /dev/null -w "%{http_code} %{time_total}s" --max-time 10 https://external-api.example.com/status

2.4 根因矩阵

现象 可能根因 关键证据 止血手段 优先级
DNS 解析失败 DNS 记录被删或过期 dig 返回 NXDOMAIN 手动添加 DNS 记录 P0
DNS 解析慢 (>1s) DNS 服务器故障 dig Query time 异常 切换 DNS 服务器 P1
LB 返回 502 全部后端 所有后端服务挂掉 no live upstreams 重启后端服务 P0
LB 返回 502 部分后端 个别后端节点故障 Connection refused (特定 IP) 摘除故障节点 P1
应用 OOMKilled 内存泄漏或 limits 太低 dmesg OOM killer 日志 重启 + 增加内存 P0
应用线程池耗尽 下游依赖超时导致线程阻塞 jstack BLOCKED 线程多 重启 + 加超时 P0
数据库连接池满 慢查询占用连接不释放 pg_stat_activity 大量 active 杀慢查询 + 重启应用 P0
数据库锁等待 长事务锁表 pg_locks 显示锁等待 杀阻塞 PID P0
Redis 连接数爆满 连接泄漏 connected_clients 异常高 重启 Redis 客户端 P1
磁盘空间满 日志或数据增长 df 显示 100% 清理空间 P0

2.5 处理与验证

2.5.1 止血决策树

服务不可用
  │
  ├── 最近有变更?
  │     ├── 是 → 回滚变更(代码/配置/基础设施)
  │     └── 否 → 继续排查
  │
  ├── 能确定故障组件?
  │     ├── 是 → 重启/切流/扩容该组件
  │     └── 否 → 逐层排查 DNS→LB→网关→应用→DB
  │
  └── 止血后
        ├── 保存现场(日志、指标、核心转储)
        ├── 确认服务恢复
        └── 安排根因分析

2.5.2 常见止血操作

# 回滚最近部署
# 如果使用 Kubernetes
kubectl rollout undo deployment/app-service -n production

# 如果使用 systemd 管理的二进制
systemctl stop app-service
cp /opt/app/app-service.bak /opt/app/app-service
systemctl start app-service

# 重启服务
systemctl restart app-service

# 扩容(Kubernetes)
kubectl scale deployment/app-service -n production --replicas=6

# 摘除故障 LB 节点
# 在 Nginx upstream 中将故障节点标记为 down
# server 10.0.1.10:8080 down;
nginx -t && nginx -s reload

# 杀掉数据库慢查询
sudo -u postgres psql -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = 'active' AND query_start < now() - interval '5 minutes' AND query NOT LIKE '%pg_stat%';"

# 清理磁盘空间
journalctl --vacuum-size=500M
find /var/log -name "*.gz" -mtime +7 -delete

2.5.3 验证恢复

# 端到端验证
curl -s -o /dev/null -w "HTTP: %{http_code}, Total: %{time_total}s\n" https://api.example.com/health

# 持续观察 5 分钟
for i in $(seq 1 30); do
    CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 https://api.example.com/health)
echo "$(date '+%H:%M:%S') - HTTP ${CODE}"
    sleep 10
done

# 检查错误率是否归零
curl -s "http://prometheus:9090/api/v1/query?query=sum(rate(http_requests_total{code=~\"5..\"}[1m]))" | python3 -m json.tool

示例代码和配置

3.1 配置样例

Blackbox Exporter 配置,用于外部探测服务可用性:

# 文件:/etc/blackbox_exporter/config.yml
# 说明:定义 HTTP、TCP、DNS 探测模块
modules:
  http_2xx:
    prober: http
    timeout: 10s
    http:
      valid_http_versions: ["HTTP/1.1", "HTTP/2.0"]
      valid_status_codes: [200]
      method: GET
      follow_redirects: true
      preferred_ip_protocol: "ip4"
      tls_config:
        insecure_skip_verify: false

  http_post_2xx:
    prober: http
    timeout: 10s
    http:
      method: POST
      headers:
        Content-Type: application/json
      body: '{"check":"health"}'

  tcp_connect:
    prober: tcp
    timeout: 5s

  dns_resolve:
    prober: dns
    timeout: 5s
    dns:
      query_name: "api.example.com"
      query_type: "A"
      valid_rcodes:
      - NOERROR

Prometheus 中的 Blackbox 探测任务配置:

# 文件:prometheus.yml(scrape_configs 部分)
scrape_configs:
  - job_name: 'blackbox-http'
    metrics_path: /probe
    params:
      module: [http_2xx]
    static_configs:
      - targets:
        - https://api.example.com/health
        - https://www.example.com/
        - https://admin.example.com/login
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: blackbox-exporter:9115

  - job_name: 'blackbox-tcp'
    metrics_path: /probe
    params:
      module: [tcp_connect]
    static_configs:
      - targets:
        - 10.0.1.10:8080
        - 10.0.1.11:8080
        - 10.0.1.12:8080
        - 10.0.1.20:5432
        - 10.0.1.21:6379
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: blackbox-exporter:9115

3.2 脚本一:快速采集脚本

#!/bin/bash
# 文件名:service-outage-collect.sh
# 作用:服务不可用时一键采集全链路诊断信息,保存现场证据
# 适用场景:收到服务不可用告警后的第一时间取证
# 使用方式:bash service-outage-collect.sh <service-name> [output-dir]
# 输入参数:
#   $1 - 服务名称(必填,用于组织输出目录和采集相关日志)
#   $2 - 输出目录(可选,默认 /tmp/outage-diag)
# 输出结果:在输出目录下生成包含系统状态、服务日志、网络状态等的诊断目录
# 风险提示:仅读取操作;在高负载时采集可能有少量性能开销

SERVICE="${1:?用法: $0 <service-name> [output-dir]}"
OUTPUT_DIR="${2:-/tmp/outage-diag}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
DIAG_DIR="${OUTPUT_DIR}/${SERVICE}_${TIMESTAMP}"

mkdir -p "${DIAG_DIR}"

echo "[INFO] ===== 系统基础信息 ====="
{
echo "--- uptime ---"
    uptime
echo "--- free ---"
    free -h
echo "--- df ---"
    df -h
echo "--- loadavg ---"
    cat /proc/loadavg
echo "--- uname ---"
    uname -a
} > "${DIAG_DIR}/system_info.txt" 2>&1

echo "[INFO] ===== 进程信息 ====="
ps aux --sort=-%mem | head -30 > "${DIAG_DIR}/top_processes.txt" 2>&1
top -bn1 | head -30 > "${DIAG_DIR}/top_snapshot.txt" 2>&1

echo "[INFO] ===== 服务状态 ====="
systemctl status "${SERVICE}" > "${DIAG_DIR}/service_status.txt" 2>&1
journalctl -u "${SERVICE}" -n 200 --no-pager > "${DIAG_DIR}/service_journal.txt" 2>&1

echo "[INFO] ===== 网络状态 ====="
ss -tlnp > "${DIAG_DIR}/listen_ports.txt" 2>&1
ss -s > "${DIAG_DIR}/socket_summary.txt" 2>&1
ss -tn state established | head -50 > "${DIAG_DIR}/established_connections.txt" 2>&1
ss -tn state time-wait | wc -l > "${DIAG_DIR}/timewait_count.txt" 2>&1

echo "[INFO] ===== DNS 检查 ====="
{
echo "--- /etc/resolv.conf ---"
    cat /etc/resolv.conf
echo "--- dig api.example.com ---"
    dig api.example.com +short 2>/dev/null
} > "${DIAG_DIR}/dns_check.txt" 2>&1

echo "[INFO] ===== 磁盘 IO ====="
iostat -x 1 3 > "${DIAG_DIR}/iostat.txt" 2>&1

echo "[INFO] ===== OOM 检查 ====="
dmesg | grep -i "oom\|killed" | tail -20 > "${DIAG_DIR}/oom_check.txt" 2>&1

echo "[INFO] ===== Nginx(如存在)====="
if command -v nginx &>/dev/null; then
    nginx -t > "${DIAG_DIR}/nginx_config_test.txt" 2>&1
    tail -100 /var/log/nginx/error.log > "${DIAG_DIR}/nginx_error.txt" 2>&1
    tail -500 /var/log/nginx/access.log | awk '{print $9}' | sort | uniq -c | sort -rn > "${DIAG_DIR}/nginx_status_dist.txt" 2>&1
fi

echo "[INFO] ===== PostgreSQL(如存在)====="
if command -v psql &>/dev/null; then
    sudo -u postgres psql -c "SELECT state, count(*) FROM pg_stat_activity GROUP BY state;" > "${DIAG_DIR}/pg_connections.txt" 2>&1
    sudo -u postgres psql -c "SELECT pid, now()-query_start AS duration, left(query,100) FROM pg_stat_activity WHERE state='active' AND query_start < now()-interval '10 seconds' ORDER BY duration DESC LIMIT 10;" > "${DIAG_DIR}/pg_slow_queries.txt" 2>&1
fi

echo "[DONE] 诊断数据已保存到: ${DIAG_DIR}"
ls -la "${DIAG_DIR}"

3.3 脚本二:日志归桶/诊断脚本

#!/bin/bash
# 文件名:outage-timeline-builder.sh
# 作用:从多个日志源提取故障时间段内的关键事件,构建故障时间线
# 适用场景:故障复盘时梳理事件顺序、多组件故障的关联分析
# 使用方式:bash outage-timeline-builder.sh <start-time> <end-time>
# 输入参数:
#   $1 - 故障开始时间,格式 "YYYY-MM-DD HH:MM:SS"(必填)
#   $2 - 故障结束时间,格式 "YYYY-MM-DD HH:MM:SS"(必填)
# 输出结果:按时间排序的故障事件时间线
# 风险提示:仅读取操作;大日志文件可能耗时较长

START_TIME="${1:?用法: $0 \"YYYY-MM-DD HH:MM:SS\" \"YYYY-MM-DD HH:MM:SS\"}"
END_TIME="${2:?缺少结束时间参数}"

TIMELINE_FILE="/tmp/outage_timeline_$(date +%Y%m%d_%H%M%S).txt"

echo "==========================================" | tee "${TIMELINE_FILE}"
echo " 故障时间线构建报告" | tee -a "${TIMELINE_FILE}"
echo " 时间范围: ${START_TIME} ~ ${END_TIME}" | tee -a "${TIMELINE_FILE}"
echo " 生成时间: $(date '+%Y-%m-%d %H:%M:%S')" | tee -a "${TIMELINE_FILE}"
echo "==========================================" | tee -a "${TIMELINE_FILE}"

echo "" | tee -a "${TIMELINE_FILE}"
echo "--- 系统日志(内核/OOM/硬件错误)---" | tee -a "${TIMELINE_FILE}"
journalctl --since "${START_TIME}" --until "${END_TIME}" -k --no-pager 2>/dev/null | \
    grep -iE "oom|killed|error|fail|panic|segfault|hardware" | \
    head -30 | tee -a "${TIMELINE_FILE}"

echo "" | tee -a "${TIMELINE_FILE}"
echo "--- systemd 服务状态变更 ---" | tee -a "${TIMELINE_FILE}"
journalctl --since "${START_TIME}" --until "${END_TIME}" --no-pager 2>/dev/null | \
    grep -iE "Started|Stopped|Failed|Reloaded|Main process exited" | \
    head -30 | tee -a "${TIMELINE_FILE}"

echo "" | tee -a "${TIMELINE_FILE}"
echo "--- Nginx error_log ---" | tee -a "${TIMELINE_FILE}"
if [ -f /var/log/nginx/error.log ]; then
    awk -v start="${START_TIME}" -v end="${END_TIME}" '$0 >= start && $0 <= end' \
        /var/log/nginx/error.log 2>/dev/null | \
        grep -E "\[(error|crit|alert|emerg)\]" | \
        tail -20 | tee -a "${TIMELINE_FILE}"
fi

echo "" | tee -a "${TIMELINE_FILE}"
echo "--- Nginx 5xx 请求统计(按分钟)---" | tee -a "${TIMELINE_FILE}"
if [ -f /var/log/nginx/access.log ]; then
    awk '$9 ~ /^5/' /var/log/nginx/access.log 2>/dev/null | \
        awk -F'[' '{print $2}' | awk -F: '{print $1":"$2":"$3}' | \
        sort | uniq -c | tail -30 | tee -a "${TIMELINE_FILE}"
fi

echo "" | tee -a "${TIMELINE_FILE}"
echo "--- 应用服务日志 ---" | tee -a "${TIMELINE_FILE}"
journalctl -u "app-*" --since "${START_TIME}" --until "${END_TIME}" --no-pager 2>/dev/null | \
    grep -iE "error|exception|fatal|timeout|refused|unavailable" | \
    head -30 | tee -a "${TIMELINE_FILE}"

echo "" | tee -a "${TIMELINE_FILE}"
echo "--- PostgreSQL 日志 ---" | tee -a "${TIMELINE_FILE}"
if [ -f /var/log/postgresql/postgresql-16-main.log ]; then
    awk -v start="${START_TIME}" -v end="${END_TIME}" '$0 >= start && $0 <= end' \
        /var/log/postgresql/postgresql-16-main.log 2>/dev/null | \
        grep -iE "error|fatal|panic|deadlock|timeout" | \
        tail -20 | tee -a "${TIMELINE_FILE}"
fi

echo "" | tee -a "${TIMELINE_FILE}"
echo "==========================================" | tee -a "${TIMELINE_FILE}"
echo " 时间线已保存到: ${TIMELINE_FILE}" | tee -a "${TIMELINE_FILE}"
echo "==========================================" | tee -a "${TIMELINE_FILE}"

3.4 脚本三:验证脚本

#!/bin/bash
# 文件名:service-recovery-verify.sh
# 作用:故障修复后系统化验证全链路各组件恢复状态
# 适用场景:止血/修复操作后的系统化健康确认
# 使用方式:bash service-recovery-verify.sh [config-file]
# 输入参数:$1 - 验证配置文件路径(可选,默认使用内置检查项)
# 输出结果:逐项输出验证结果(PASS/FAIL/WARN),最终给出总体判定
# 风险提示:会向各服务发送探测请求;数据库查询仅为只读查询

PASS=0
FAIL=0
WARN=0

check_result() {
local desc="$1"
local exit_code="$2"
local detail="$3"
if [ "${exit_code}" = "0" ]; then
echo "[PASS] ${desc}"
        [ -n "${detail}" ] && echo "       ${detail}"
        PASS=$((PASS+1))
else
echo "[FAIL] ${desc}"
        [ -n "${detail}" ] && echo "       ${detail}"
        FAIL=$((FAIL+1))
fi
}

echo "=========================================="
echo " 服务恢复验证报告"
echo " 时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "=========================================="

echo ""
echo "=== 1. DNS 验证 ==="
DNS_RESULT=$(dig +short api.example.com 2>/dev/null)
if [ -n "${DNS_RESULT}" ]; then
    check_result "DNS 解析 api.example.com" "0" "解析结果: ${DNS_RESULT}"
else
    check_result "DNS 解析 api.example.com" "1" "解析失败"
fi

echo ""
echo "=== 2. 网络连通性 ==="
for target in "api.example.com:443" "10.0.1.10:8080" "10.0.1.20:5432" "10.0.1.21:6379"; do
    HOST=$(echo "${target}" | cut -d: -f1)
    PORT=$(echo "${target}" | cut -d: -f2)
    timeout 5 bash -c "echo > /dev/tcp/${HOST}/${PORT}" 2>/dev/null
    check_result "TCP 连通 ${target}" "$?"
done

echo ""
echo "=== 3. 服务进程 ==="
for svc in nginx app-service postgresql redis-server; do
if systemctl is-active --quiet "${svc}" 2>/dev/null; then
        check_result "服务 ${svc} 运行中" "0"
elif systemctl list-units --type=service 2>/dev/null | grep -q "${svc}"; then
        check_result "服务 ${svc} 运行中" "1" "服务存在但未运行"
fi
done

echo ""
echo "=== 4. HTTP 端到端 ==="
for url in "https://api.example.com/health" "https://api.example.com/api/users"; do
    HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "${url}" 2>/dev/null)
if [ "${HTTP_CODE}" = "200" ]; then
        check_result "HTTP ${url}" "0" "状态码: ${HTTP_CODE}"
else
        check_result "HTTP ${url}" "1" "状态码: ${HTTP_CODE}"
fi
done

echo ""
echo "=== 5. 数据库连接 ==="
if command -v psql &>/dev/null; then
    PG_CONN=$(sudo -u postgres psql -c "SELECT count(*) FROM pg_stat_activity WHERE state='active';" -t 2>/dev/null | tr -d ' ')
if [ -n "${PG_CONN}" ]; then
        check_result "PostgreSQL 连接正常" "0" "活跃连接数: ${PG_CONN}"
else
        check_result "PostgreSQL 连接正常" "1"
fi
fi

if command -v redis-cli &>/dev/null; then
    REDIS_PONG=$(redis-cli ping 2>/dev/null)
if [ "${REDIS_PONG}" = "PONG" ]; then
        check_result "Redis 连接正常" "0"
else
        check_result "Redis 连接正常" "1"
fi
fi

echo ""
echo "=== 6. 系统资源 ==="
MEM_USAGE=$(free | awk '/Mem/{printf "%.0f", $3/$2*100}')
DISK_USAGE=$(df / | awk 'NR==2{print $5}' | tr -d '%')
LOAD=$(cat /proc/loadavg | awk '{print $1}')
CPU_COUNT=$(nproc)

if [ "${MEM_USAGE}" -lt 90 ]; then
    check_result "内存使用率" "0" "${MEM_USAGE}%"
else
    check_result "内存使用率" "1" "${MEM_USAGE}% (过高)"
fi

if [ "${DISK_USAGE}" -lt 90 ]; then
    check_result "磁盘使用率" "0" "${DISK_USAGE}%"
else
    check_result "磁盘使用率" "1" "${DISK_USAGE}% (过高)"
fi

LOAD_INT=$(echo "${LOAD}" | cut -d. -f1)
if [ "${LOAD_INT}" -lt "$((CPU_COUNT * 2))" ]; then
    check_result "系统负载" "0" "Load: ${LOAD}, CPU: ${CPU_COUNT}"
else
    check_result "系统负载" "1" "Load: ${LOAD}, CPU: ${CPU_COUNT} (负载过高)"
fi

echo ""
echo "=========================================="
echo " 验证汇总: PASS=${PASS}  FAIL=${FAIL}  WARN=${WARN}"
if [ "${FAIL}" -eq 0 ]; then
echo " 结论: 服务已恢复"
else
echo " 结论: 仍有 ${FAIL} 项未恢复,需要继续处理"
fi
echo "=========================================="

3.5 脚本四:回滚/批量探测脚本

#!/bin/bash
# 文件名:full-stack-probe.sh
# 作用:对全链路各层(DNS/LB/应用/DB/缓存)进行批量健康探测
# 适用场景:日常巡检、故障排查时快速定位断点层级、变更后验证
# 使用方式:bash full-stack-probe.sh [probe-config]
# 输入参数:$1 - 探测配置文件(可选,默认使用内置目标列表)
# 输出结果:每个层级每个目标的探测结果(UP/DOWN/SLOW)
# 风险提示:会向各目标发送探测请求;DNS 查询和 HTTP 请求产生少量网络流量

echo "=========================================="
echo " 全链路健康探测"
echo " 时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "=========================================="

# DNS 层
echo ""
echo "=== DNS 层 ==="
DNS_TARGETS=("api.example.com" "www.example.com" "admin.example.com")
for domain in "${DNS_TARGETS[@]}"; do
    START=$(date +%s%N)
    RESULT=$(dig +short "${domain}" 2>/dev/null | head -1)
    END=$(date +%s%N)
    ELAPSED=$(( (END - START) / 1000000 ))

if [ -n "${RESULT}" ] && [ "${ELAPSED}" -lt 1000 ]; then
printf "  %-30s [UP]   %sms  → %s\n" "${domain}" "${ELAPSED}" "${RESULT}"
elif [ -n "${RESULT}" ]; then
printf "  %-30s [SLOW] %sms  → %s\n" "${domain}" "${ELAPSED}" "${RESULT}"
else
printf "  %-30s [DOWN] 解析失败\n" "${domain}"
fi
done

# LB / 网关层
echo ""
echo "=== LB / 网关层 ==="
LB_TARGETS=("https://api.example.com/nginx-health" "https://www.example.com/")
for url in "${LB_TARGETS[@]}"; do
    RESULT=$(curl -s -o /dev/null -w "%{http_code}|%{time_total}" --max-time 10 "${url}" 2>/dev/null)
    CODE=$(echo "${RESULT}" | cut -d'|' -f1)
    TIME=$(echo "${RESULT}" | cut -d'|' -f2)

if [ "${CODE}" = "200" ]; then
printf "  %-45s [UP]   HTTP=%s  %ss\n" "${url}" "${CODE}" "${TIME}"
elif [ "${CODE}" = "000" ]; then
printf "  %-45s [DOWN] 连接失败\n" "${url}"
else
printf "  %-45s [WARN] HTTP=%s  %ss\n" "${url}" "${CODE}" "${TIME}"
fi
done

# 应用层
echo ""
echo "=== 应用层 ==="
APP_TARGETS=("10.0.1.10:8080" "10.0.1.11:8080" "10.0.1.12:8080")
for target in "${APP_TARGETS[@]}"; do
    RESULT=$(curl -s -o /dev/null -w "%{http_code}|%{time_total}" --max-time 5 "http://${target}/health" 2>/dev/null)
    CODE=$(echo "${RESULT}" | cut -d'|' -f1)
    TIME=$(echo "${RESULT}" | cut -d'|' -f2)

if [ "${CODE}" = "200" ]; then
printf "  %-25s [UP]   HTTP=%s  %ss\n" "${target}" "${CODE}" "${TIME}"
else
printf "  %-25s [DOWN] HTTP=%s\n" "${target}" "${CODE}"
fi
done

# 数据库层
echo ""
echo "=== 数据库层 ==="
DB_TARGETS=("10.0.1.20:5432" "10.0.1.21:6379")
for target in "${DB_TARGETS[@]}"; do
    HOST=$(echo "${target}" | cut -d: -f1)
    PORT=$(echo "${target}" | cut -d: -f2)
    timeout 3 bash -c "echo > /dev/tcp/${HOST}/${PORT}" 2>/dev/null
if [ $? -eq 0 ]; then
printf "  %-25s [UP]   TCP 端口可达\n" "${target}"
else
printf "  %-25s [DOWN] TCP 连接失败\n" "${target}"
fi
done

echo ""
echo "=========================================="
echo " 探测完成"
echo "=========================================="

实际应用案例

案例一:数据库连接池耗尽导致全站 502

现场现象:  工作日上午 10 点,所有 API 接口返回 502,前端页面显示“服务暂时不可用”。告警系统同时收到 Nginx 502 告警和应用健康检查失败告警。

第一轮判断:

curl -s -o /dev/null -w "%{http_code}" https://api.example.com/health
# 502

检查 Nginx error_log:

upstream prematurely closed connection while reading response header from upstream

Nginx 本身正常,后端连接异常。直接测试后端:

curl -s -o /dev/null -w "%{http_code}" http://10.0.1.10:8080/health
# 503

后端返回 503,说明应用层有问题。

第二轮下钻:

journalctl -u app-service -n 20 --no-pager

应用日志:

ERROR: Unable to acquire connection from pool - pool exhausted, no available connections
ERROR: Connection pool timeout after 30000ms

连接池耗尽。检查数据库:

sudo -u postgres psql -c "SELECT state, count(*) FROM pg_stat_activity GROUP BY state ORDER BY count DESC;"

输出:

  state  | count
---------+-------
 active  |   95
 idle    |    5
         |    5

最大连接数 100,活跃连接 95 个。

sudo -u postgres psql -c "SELECT pid, now()-query_start AS duration, left(query,80) FROM pg_stat_activity WHERE state='active' ORDER BY duration DESC LIMIT 5;"

输出显示有 5 个查询跑了超过 10 分钟,全是同一个报表查询。

关键证据:  报表查询触发了全表扫描,长时间持有数据库连接不释放,导致连接池被耗尽,新请求无法获取连接。

根因:  运营人员在后台触发了大数据量报表导出,缺少查询超时配置,慢查询占满了连接池。

修复动作:

# 止血:杀掉长时间运行的查询
sudo -u postgres psql -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state='active' AND query_start < now()-interval '5 minutes' AND query NOT LIKE '%pg_stat%';"

# 重启应用释放连接池
systemctl restart app-service

修复后验证:

curl -s -o /dev/null -w "%{http_code}" https://api.example.com/health
# 200

sudo -u postgres psql -c "SELECT state, count(*) FROM pg_stat_activity GROUP BY state;"
# active 连接数恢复到正常水平(5-10个)

防再发建议:  数据库配置 statement_timeout = '60s';应用连接池配置最大等待时间和连接泄漏检测;报表查询走只读副本。

案例二:DNS TTL 过期后解析指向旧 IP

现场现象:  域名迁移后,部分用户反馈访问超时,另一部分用户正常。

第一轮判断:

# 从不同 DNS 服务器查询
dig api.example.com @8.8.8.8 +short
# 10.0.2.100(新 IP,正确)

dig api.example.com @1.1.1.1 +short
# 10.0.1.50(旧 IP,错误)

不同 DNS 解析结果不一致,说明 DNS 记录更新未完全传播。

第二轮下钻:

# 查看 TTL
dig api.example.com @8.8.8.8 +noall +answer
# api.example.com.  30  IN  A  10.0.2.100

dig api.example.com @1.1.1.1 +noall +answer
# api.example.com.  3540  IN  A  10.0.1.50

1.1.1.1 的缓存 TTL 还有 3540 秒(约 59 分钟),说明该 DNS 服务器缓存了旧记录,还没过期。

# 确认旧 IP 是否还可达
curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 http://10.0.1.50:8080/health
# 000(连接失败,旧服务器已下线)

关键证据:  DNS 变更前 TTL 设置为 3600 秒(1 小时),变更后部分 DNS 缓存服务器仍返回旧 IP,而旧服务器已下线。

根因:  DNS 迁移前没有提前降低 TTL,导致变更后 DNS 缓存过期时间过长,解析到已下线的旧 IP。

修复动作:

# 短期:在旧 IP 上临时启动转发,把流量重定向到新 IP
# 或联系 CDN/DNS 服务商清除缓存

# 长期:等待所有 DNS 缓存自然过期(最长 1 小时)

修复后验证:

# 持续监控不同 DNS 的解析结果
for dns in 8.8.8.8 1.1.1.1 119.29.29.29 223.5.5.5; do
echo -n "DNS ${dns}: "
    dig +short api.example.com @${dns}
done
# 等待所有 DNS 返回新 IP

防再发建议:  DNS 迁移标准流程:先将 TTL 降低到 60 秒,等待原 TTL 过期后再修改记录,迁移完成后恢复 TTL。旧服务器保持运行至少一个 TTL 周期。

案例三:应用 OOM 被反复杀死

现场现象:  Java 微服务在高峰期反复重启,systemd 显示 Main process exited, code=killed, status=9/KILL。

第一轮判断:

systemctl status app-service
# Active: activating (auto-restart)
# Main process exited, code=killed, status=9/KILL

dmesg | grep -i oom | tail -5

输出:

[T+0.000] app-service invoked oom-killer: gfp_mask=0xcc0(GFP_KERNEL), order=0
[T+0.001] oom_kill_process.cold+0xb/0x10
[T+0.001] Out of memory: Killed process 12345 (java) total-vm:4194304kB, anon-rss:3145728kB

OOM Killer 杀掉了 Java 进程。

第二轮下钻:

# 检查进程内存限制
systemctl show app-service | grep -i memory
# MemoryMax=2G

# 检查 JVM 参数
ps aux | grep java | grep -oP '\-Xmx\S+'
# -Xmx1536m

# Java 堆 1536MB,加上 Metaspace、线程栈、JNI,总内存占用超过了 systemd 的 2G 限制
# 查看 GC 日志(如果配置了)
tail -50 /var/log/app/gc.log
# 频繁 Full GC,堆内存持续增长不回收

使用 jmap 取堆转储(注意生产环境可能造成短暂停顿):

jmap -dump:format=b,file=/tmp/heap_dump.hprof $(pgrep -f app-service)

关键证据:  systemd MemoryMax=2G,JVM -Xmx=1536m,但堆外内存(Direct ByteBuffer + Metaspace + 线程栈)加上堆内存总计超过 2G。同时 GC 日志显示存在内存泄漏,Full GC 无法回收足够内存。

根因:  应用存在内存泄漏(缓存对象未设置过期),叠加 systemd MemoryMax 设置没有预留足够的堆外空间。

修复动作:

# 止血:增加内存限制
systemctl edit app-service
# [Service]
# MemoryMax=3G

systemctl daemon-reload
systemctl restart app-service

# 同时调整 JVM 参数
# 在 app-service 启动配置中设置:
# JAVA_OPTS="-Xms512m -Xmx1536m -XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=256m"

修复后验证:

# 持续观察内存使用
watch -n 30 'ps -p $(pgrep -f app-service) -o pid,rss,vsz --no-headers'

# 确认无 OOM 事件
dmesg | grep -i oom | tail -5
# 无新增 OOM 日志

# 确认服务稳定运行
systemctl status app-service
# Active: active (running) since ...

防再发建议:  JVM 应用必须设置 -XX:MaxMetaspaceSize 和 -XX:MaxDirectMemorySize 限制堆外内存;systemd MemoryMax 设置为 JVM 最大堆 + 堆外上限 + 500M 余量;接入内存指标监控,提前告警。

案例四:配置变更引发网关路由失效

现场现象:  下午 4 点,运维人员修改 Nginx 配置后执行 reload,随后部分 API 返回 404,部分返回 200。

第一轮判断:

# 确认哪些路径受影响
curl -s -o /dev/null -w "%{http_code}" https://api.example.com/api/users    # 200
curl -s -o /dev/null -w "%{http_code}" https://api.example.com/api/orders   # 404
curl -s -o /dev/null -w "%{http_code}" https://api.example.com/api/v2/users # 404

/api/users 正常,/api/orders 和 /api/v2/ 开头的路径全部 404。

第二轮下钻:

# 查看最近的配置变更
diff /etc/nginx/conf.d/api.conf.bak /etc/nginx/conf.d/api.conf

输出:

-    location /api/ {
+    location = /api/users {
         proxy_pass http://api_backend;
     }
+
+    location /api/v2/ {
+        proxy_pass http://api_v2_backend;
+    }

变更者将 /api/ 前缀匹配改成了 = /api/users 精确匹配,只命中 /api/users 这一个路径。其他 /api/ 下的路径没有 location 匹配,落到了默认 location 或被其他 location 捕获返回 404。

同时 /api/v2/ 的 upstream api_v2_backend 没有在配置中定义。

nginx -T | grep api_v2_backend
# 无输出

关键证据:  location 从前缀匹配改为精确匹配,导致大部分路径无法命中;新增的 upstream 名称未定义(nginx -t 实际上会报错,但当时没检查输出)。

根因:  配置变更缺乏审核,nginx -t 的错误输出被忽略,强制 reload 导致部分 worker 加载了不完整配置。

修复动作:

# 回滚配置
cp /etc/nginx/conf.d/api.conf.bak /etc/nginx/conf.d/api.conf
nginx -t
# syntax is ok, test is successful
nginx -s reload

修复后验证:

curl -s -o /dev/null -w "%{http_code}" https://api.example.com/api/orders   # 200
curl -s -o /dev/null -w "%{http_code}" https://api.example.com/api/v2/users # 200
# 所有路径恢复正常

tail -20 /var/log/nginx/error.log
# 无新增错误

防再发建议:  Nginx 配置变更纳入 Git 版本管理,通过 CI 流水线自动执行 nginx -t 验证;变更前备份,变更后自动跑关键路径测试;重大变更需要双人审核。

最佳实践和注意事项

5.1 最佳实践

  1. 建立标准 Runbook:  针对每种高频故障类型(502、OOM、磁盘满、连接池满)编写标准操作手册,值班人员可以直接按步骤执行,不需要每次从零开始分析。
  2. 外部探测优先于内部指标:  内部指标可能在故障时采集不到。用 Blackbox Exporter 从外部探测 HTTP/TCP/DNS 可用性,这是最接近用户视角的监控。
  3. 告警必须包含上下文:  告警消息中要包含受影响的服务名、节点、当前指标值和阈值,而不是只说“服务异常”。运维人员看到告警就要能判断严重程度和排查方向。
  4. 止血和根因分析分开:  先恢复服务,再分析根因。但止血前一定要保存现场:日志、指标截图、核心转储。否则重启后证据丢失。
  5. 超时配置全链路对齐:  CDN 超时 > Nginx proxy_read_timeout > 应用请求超时 > 数据库 statement_timeout。上游超时必须大于下游,否则会出现上游断连但下游还在处理的情况。
  6. 数据库连接池合理配置:  应用连接池最大连接数 * 应用实例数 < 数据库 max_connections。预留 10-20% 给管理连接和监控。
  7. 变更管理是故障预防的核心:  绝大多数生产故障由变更引发。所有变更(代码、配置、基础设施)必须有审核、有回滚方案、有验证步骤。
  8. 故障复盘要形成改进项:  每次故障都要做复盘,产出 timeline、root cause、改进措施、责任人和截止时间。复盘不是追责,是改进流程。

5.2 注意事项

  1. 不要在故障期间做不可逆操作:  除非完全确认根因,否则不要做数据删除、配置覆盖等无法回退的操作。
  2. 重启不是万能解药:  重启可以止血,但如果根因没解决(比如内存泄漏),重启后问题会复现。重启前保存现场证据。
  3. 注意告警风暴:  一个组件故障可能触发关联组件的大量告警。要配置 Alertmanager 的 group_by 和 inhibit_rules,避免告警淹没真正的根因信息。
  4. 并发排查要沟通:  多人同时排查同一个故障时,必须有人协调。两个人同时重启不同组件可能互相干扰。
  5. 生产环境禁止未经验证的操作:  不确定效果的命令先在 staging 验证。特别是数据库操作,一条 UPDATE 或 DELETE 不加 WHERE 可能造成灾难。

5.3 常见错误清单

错误操作 后果 正确做法
故障时直接重启不保现场 日志和内存状态丢失,无法复盘 先采集诊断信息再重启
同时变更多个组件 无法确定哪个变更导致问题 每次只变更一个组件
nginx reload 不检查 -t 输出 配置语法错误导致 reload 失败或部分失败 先 nginx -t 确认再 reload
数据库手动杀所有连接 正在执行的事务被强制中断 只杀明确的慢查询/长事务
不区分止血和修复 花大量时间分析根因,服务持续不可用 先止血恢复服务,再分析根因
告警阈值设置过低 频繁误报导致告警疲劳 根据基线数据设置合理阈值
回滚方案缺失 变更出问题时无法快速恢复 每次变更前准备回滚方案
DNS 变更不降低 TTL 解析切换时间过长 变更前 24 小时将 TTL 降至 60 秒
systemd MemoryMax 不预留堆外空间 Java 应用频繁 OOMKilled 预留 JVM 堆外需求 + 500M 余量

故障排查和监控

6.1 关键指标

# 服务可用性(Blackbox Exporter)
curl -s "http://prometheus:9090/api/v1/query?query=probe_success{job='blackbox-http'}"

# 请求成功率
curl -s "http://prometheus:9090/api/v1/query?query=sum(rate(http_requests_total{code=~'2..'}[5m]))/sum(rate(http_requests_total[5m]))"

# P95 延迟
curl -s "http://prometheus:9090/api/v1/query?query=histogram_quantile(0.95,sum(rate(http_request_duration_seconds_bucket[5m]))by(le))"

# 节点资源
curl -s "http://prometheus:9090/api/v1/query?query=1-avg(rate(node_cpu_seconds_total{mode='idle'}[5m]))by(instance)"
curl -s "http://prometheus:9090/api/v1/query?query=1-node_memory_MemAvailable_bytes/node_memory_MemTotal_bytes"

# 数据库连接数
curl -s "http://prometheus:9090/api/v1/query?query=pg_stat_activity_count{state='active'}"

6.2 指标说明

指标 PromQL 正常范围 告警阈值 说明
服务可用性 probe_success 1 0 持续 1 分钟 外部探测结果,1=可用 0=不可用
请求成功率 rate(http_requests_total{code=~"2.."}[5m]) / rate(http_requests_total[5m]) > 99.9% < 99% 持续 2 分钟 核心 SLI 指标
P95 延迟 histogram_quantile(0.95, ...) < 500ms > 2s 持续 5 分钟 用户体感延迟
错误率 rate(http_requests_total{code=~"5.."}[5m]) / rate(http_requests_total[5m]) < 0.1% > 1% 持续 2 分钟 5xx 错误比例
节点 CPU 1 - rate(node_cpu_seconds_total{mode="idle"}[5m]) < 70% > 85% 持续 5 分钟 单节点 CPU 使用率
节点内存 1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes < 80% > 90% 持续 5 分钟 可能触发 OOM
数据库活跃连接 pg_stat_activity_count{state="active"} < max_connections * 0.7 > max_connections * 0.8 接近上限会导致连接拒绝
DNS 解析延迟 probe_dns_lookup_time_seconds < 100ms > 1s DNS 解析慢影响所有请求

6.3 告警规则

# Prometheus 告警规则:全链路服务可用性
# 文件:service-availability-alerts.yaml
groups:
  - name: service.availability
    rules:
      - alert: ServiceDown
        expr: probe_success{job="blackbox-http"} == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "服务不可用: {{ $labels.instance }}"
          description: "Blackbox 探测失败持续 1 分钟"

      - alert: HighErrorRate
        expr: |
          sum(rate(http_requests_total{code=~"5.."}[5m])) by (service)
          /
          sum(rate(http_requests_total[5m])) by (service) > 0.01
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "{{ $labels.service }} 错误率超过 1%"
          description: "当前错误率: {{ $value | humanizePercentage }}"

      - alert: HighLatencyP95
        expr: |
          histogram_quantile(0.95,
            sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)
          ) > 2
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.service }} P95 延迟超过 2 秒"

      - alert: DatabaseConnectionPoolExhausted
        expr: pg_stat_activity_count{state="active"} > 80
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "数据库活跃连接数超过 80"
          description: "可能导致连接池耗尽,当前: {{ $value }}"

      - alert: NodeHighMemoryUsage
        expr: 1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes > 0.9
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "节点 {{ $labels.instance }} 内存使用率超过 90%"

      - alert: DiskSpaceLow
        expr: 1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"} > 0.85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "节点 {{ $labels.instance }} 磁盘使用率超过 85%"

  - name: alertmanager.config
    # Alertmanager 配置建议
    # group_by: ['alertname', 'service']
    # group_wait: 30s
    # group_interval: 5m
    # repeat_interval: 4h
    # inhibit_rules:
    #   - source_match: {severity: 'critical'}
    #     target_match: {severity: 'warning'}
    #     equal: ['instance']

6.4 修复后验证

验证项 方法 预期结果
外部可达性 Blackbox probe_success 持续 = 1
端到端 HTTP curl 健康检查接口 HTTP 200
错误率 Prometheus 查询 5xx rate < 0.1%
P95 延迟 Prometheus 查询 回到基线水平
数据库连接 pg_stat_activity 活跃连接数恢复正常
节点资源 node_exporter 指标 CPU/内存/磁盘恢复正常
告警恢复 Alertmanager 相关告警自动 Resolved
业务验证 关键业务流程测试 功能正常

总结

7.1 技术要点回顾

  1. 服务不可用排查遵循固定链路:告警确认 -> 影响评估 -> DNS -> LB -> 网关 -> 应用 -> 数据库,逐层排查定位断点。
  2. 先止血后找根因。服务恢复是第一优先级,但止血前要保存现场证据(日志、指标、内存转储)。
  3. 外部探测(Blackbox Exporter)是最可靠的可用性指标,内部指标在故障时可能采集不到。
  4. 超时配置必须全链路对齐:CDN > Nginx > 应用 > 数据库,上游超时大于下游。
  5. 数据库连接池是常见瓶颈点:慢查询占用连接、连接泄漏、max_connections 设置不合理都会导致全站不可用。
  6. DNS 变更要先降 TTL 再改记录,否则缓存过期时间过长会导致部分用户长时间不可用。
  7. 变更管理是预防故障的核心手段:审核、回滚方案、变更后验证缺一不可。
  8. 故障复盘产出时间线和改进项,每个改进项有责任人和截止时间,形成闭环。

7.2 进阶学习方向

  1. SRE 实践体系:  深入 Google SRE 的 SLI/SLO/Error Budget 框架,建立量化的可靠性管理体系。
    • 实践建议:为核心服务定义 SLO 并在 Grafana 中建立 SLO Dashboard
  2. 分布式追踪:  使用 OpenTelemetry + Jaeger/Tempo 实现全链路追踪,快速定位跨服务调用中的延迟源和错误源。
    • 实践建议:在应用中集成 OpenTelemetry SDK,建立 Trace 视图
  3. 混沌工程:  通过 Chaos Mesh/Litmus 主动注入故障(网络延迟、服务宕机、磁盘填满),验证系统韧性和 Runbook 有效性。
    • 实践建议:在 staging 环境建立定期的 Game Day 演练
  4. AIOps 与智能告警:  使用机器学习进行异常检测、告警降噪、根因推荐,减少 MTTD 和 MTTR。
    • 实践建议:从 Prometheus 的预测函数 predict_linear 开始,逐步引入更复杂的异常检测
  5. 事故管理流程标准化:  建立 Incident Commander、Communication Lead、Subject Matter Expert 的角色体系,让故障处理有序进行。
    • 实践建议:参考 PagerDuty 的 Incident Response 框架

7.3 参考资料

附录

A. 命令速查表

# 外部探测
curl -s -o /dev/null -w "HTTP:%{http_code} DNS:%{time_namelookup}s Conn:%{time_connect}s Total:%{time_total}s\n" https://target/health

# DNS 排查
dig domain +short               # 快速查看解析结果
dig domain @8.8.8.8 +noall +answer     # 指定 DNS 服务器查询
dig domain +trace               # 追踪 DNS 解析链路

# 服务状态
systemctl status <service>              # 查看服务状态
journalctl -u <service> -n 50          # 查看服务日志

# 系统资源
free -h                                # 内存使用
df -h                                  # 磁盘使用
uptime                                 # 负载
ss -tlnp                               # 监听端口
ss -s                                  # 连接统计

# OOM 检查
dmesg | grep -i oom | tail -10         # 内核 OOM 日志

# 数据库(PostgreSQL)
sudo -u postgres psql -c "SELECT state, count(*) FROM pg_stat_activity GROUP BY state;"
sudo -u postgres psql -c "SELECT pid, now()-query_start, left(query,80) FROM pg_stat_activity WHERE state='active' ORDER BY 2 DESC LIMIT 10;"

# 数据库(MySQL)
mysql -e "SHOW PROCESSLIST;"
mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_%';"

# Prometheus 查询
curl -s "http://prometheus:9090/api/v1/query?query=<PromQL>"

# 清理操作
journalctl --vacuum-size=500M          # 清理 journal 日志
find /var/log -name "*.gz" -mtime +7 -delete  # 清理旧日志

B. 配置参数详解

参数 组件 建议值 说明
statement_timeout PostgreSQL 60s 防止慢查询无限占用连接
max_connections PostgreSQL 200 所有应用连接池总和不超过此值
idle_in_transaction_session_timeout PostgreSQL 30s 空闲事务自动断开
maxpool / max_active 应用连接池 20-50 单实例最大连接数
connection_timeout 应用连接池 5s 获取连接超时,快速失败
proxy_read_timeout Nginx 60s 根据后端最长响应时间设置
proxy_connect_timeout Nginx 5s 连接超时快速失败
group_wait Alertmanager 30s 告警聚合等待时间
group_interval Alertmanager 5m 同组告警发送间隔
repeat_interval Alertmanager 4h 相同告警重复通知间隔
scrape_interval Prometheus 15s 指标采集间隔
evaluation_interval Prometheus 15s 告警规则评估间隔

C. 术语表

术语 英文 解释
止血 Mitigation 用最快手段恢复服务可用性,可能不解决根因
根因 Root Cause 导致故障的底层原因
爆炸半径 Blast Radius 故障影响的范围(用户数、服务数、地域)
变更回滚 Rollback 将系统恢复到变更前的状态
故障复盘 Post-Mortem / Incident Review 故障后的回顾分析会议
SLI Service Level Indicator 服务级别指标,量化服务质量的具体度量
SLO Service Level Objective 服务级别目标,SLI 的目标值
Error Budget Error Budget SLO 允许的故障预算
Runbook Runbook 标准化操作手册
MTTD Mean Time To Detect 从故障发生到被发现的平均时间
MTTR Mean Time To Recover 从故障发现到恢复的平均时间
告警风暴 Alert Storm 短时间内大量告警触发

D. 错误关键词速查

关键词 出现位置 含义 排查方向
OOM dmesg / journalctl -k 内存超限被内核杀掉 检查进程内存使用和 limits
Connection refused 应用日志 / Nginx error_log 目标端口未监听 检查目标服务是否启动
Connection timed out 应用日志 / Nginx error_log 网络不通或目标服务无响应 检查网络和防火墙
pool exhausted 应用日志 连接池耗尽 检查数据库连接和慢查询
Too many open files 应用日志 / dmesg 文件描述符耗尽 ulimit -n 和连接泄漏检查
NXDOMAIN dig 输出 DNS 记录不存在 检查 DNS 配置
SERVFAIL dig 输出 DNS 服务器故障 切换 DNS 或联系 DNS 服务商
no live upstreams Nginx error_log 所有后端不可用 检查所有后端服务状态
statement timeout PostgreSQL 日志 查询超时被终止 优化 SQL 或增加超时值
deadlock detected PostgreSQL 日志 死锁 检查事务并发和锁顺序

E. 排障顺序速记

0. 确认告警真实性(排除误报)
1. 评估影响范围(全站/部分/单接口)
2. 判断是否有近期变更 → 有则优先回滚
3. DNS 排查:dig 解析是否正确
4. LB/网关排查:Nginx error_log + upstream 状态
5. 应用排查:进程状态 + 日志 + OOM 检查
6. 数据库排查:连接数 + 慢查询 + 锁等待
7. 中间件排查:Redis/MQ 状态
8. 定位根因 → 止血(重启/回滚/扩容)
9. 验证恢复(curl + Prometheus + 业务验证)
10. 保存现场 → 安排复盘

掌握系统化的排查流程,是每一个运维工程师和开发者的必备技能。希望这份手册能帮助你在下一个深夜告警响起时,思路清晰地定位并解决问题。更多运维实战经验与工具分享,欢迎访问 云栈社区 与广大开发者交流。




上一篇:OpenClaw安全风险分析:公网暴露超4万实例与工信部安全指南解读
下一篇:杰理科技SoC芯片布局:蓝牙耳机、智能穿戴八大产品线如何渗透品牌市场?
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-20 11:07 , Processed in 0.497722 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表