概述
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 最佳实践
- 建立标准 Runbook: 针对每种高频故障类型(502、OOM、磁盘满、连接池满)编写标准操作手册,值班人员可以直接按步骤执行,不需要每次从零开始分析。
- 外部探测优先于内部指标: 内部指标可能在故障时采集不到。用 Blackbox Exporter 从外部探测 HTTP/TCP/DNS 可用性,这是最接近用户视角的监控。
- 告警必须包含上下文: 告警消息中要包含受影响的服务名、节点、当前指标值和阈值,而不是只说“服务异常”。运维人员看到告警就要能判断严重程度和排查方向。
- 止血和根因分析分开: 先恢复服务,再分析根因。但止血前一定要保存现场:日志、指标截图、核心转储。否则重启后证据丢失。
- 超时配置全链路对齐: CDN 超时 > Nginx proxy_read_timeout > 应用请求超时 > 数据库 statement_timeout。上游超时必须大于下游,否则会出现上游断连但下游还在处理的情况。
- 数据库连接池合理配置: 应用连接池最大连接数 * 应用实例数 < 数据库 max_connections。预留 10-20% 给管理连接和监控。
- 变更管理是故障预防的核心: 绝大多数生产故障由变更引发。所有变更(代码、配置、基础设施)必须有审核、有回滚方案、有验证步骤。
- 故障复盘要形成改进项: 每次故障都要做复盘,产出 timeline、root cause、改进措施、责任人和截止时间。复盘不是追责,是改进流程。
5.2 注意事项
- 不要在故障期间做不可逆操作: 除非完全确认根因,否则不要做数据删除、配置覆盖等无法回退的操作。
- 重启不是万能解药: 重启可以止血,但如果根因没解决(比如内存泄漏),重启后问题会复现。重启前保存现场证据。
- 注意告警风暴: 一个组件故障可能触发关联组件的大量告警。要配置 Alertmanager 的 group_by 和 inhibit_rules,避免告警淹没真正的根因信息。
- 并发排查要沟通: 多人同时排查同一个故障时,必须有人协调。两个人同时重启不同组件可能互相干扰。
- 生产环境禁止未经验证的操作: 不确定效果的命令先在 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 技术要点回顾
- 服务不可用排查遵循固定链路:告警确认 -> 影响评估 -> DNS -> LB -> 网关 -> 应用 -> 数据库,逐层排查定位断点。
- 先止血后找根因。服务恢复是第一优先级,但止血前要保存现场证据(日志、指标、内存转储)。
- 外部探测(Blackbox Exporter)是最可靠的可用性指标,内部指标在故障时可能采集不到。
- 超时配置必须全链路对齐:CDN > Nginx > 应用 > 数据库,上游超时大于下游。
- 数据库连接池是常见瓶颈点:慢查询占用连接、连接泄漏、max_connections 设置不合理都会导致全站不可用。
- DNS 变更要先降 TTL 再改记录,否则缓存过期时间过长会导致部分用户长时间不可用。
- 变更管理是预防故障的核心手段:审核、回滚方案、变更后验证缺一不可。
- 故障复盘产出时间线和改进项,每个改进项有责任人和截止时间,形成闭环。
7.2 进阶学习方向
- SRE 实践体系: 深入 Google SRE 的 SLI/SLO/Error Budget 框架,建立量化的可靠性管理体系。
- 实践建议:为核心服务定义 SLO 并在 Grafana 中建立 SLO Dashboard
- 分布式追踪: 使用 OpenTelemetry + Jaeger/Tempo 实现全链路追踪,快速定位跨服务调用中的延迟源和错误源。
- 实践建议:在应用中集成 OpenTelemetry SDK,建立 Trace 视图
- 混沌工程: 通过 Chaos Mesh/Litmus 主动注入故障(网络延迟、服务宕机、磁盘填满),验证系统韧性和 Runbook 有效性。
- 实践建议:在 staging 环境建立定期的 Game Day 演练
- AIOps 与智能告警: 使用机器学习进行异常检测、告警降噪、根因推荐,减少 MTTD 和 MTTR。
- 实践建议:从 Prometheus 的预测函数 predict_linear 开始,逐步引入更复杂的异常检测
- 事故管理流程标准化: 建立 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. 保存现场 → 安排复盘
掌握系统化的排查流程,是每一个运维工程师和开发者的必备技能。希望这份手册能帮助你在下一个深夜告警响起时,思路清晰地定位并解决问题。更多运维实战经验与工具分享,欢迎访问 云栈社区 与广大开发者交流。