Margrop
Articles266
Tags430
Categories23
1password AC ACP AI AP API AppDaemon Aqara CI/CD Caddy Cloudflare Cookie 认证 Cron D1 Date Diagrams.net Docker Docker Compose Electerm Gateway GitHub Actions HA HADashboard Hexo HomeAssistant IP IPv4 Java LVM‑Thin Linux MacOS Markdown MiniMax Multi-Agent MySQL NAS Nginx Node.js OpenAI OpenClaw OpenResty PPPoE Portainer PostgreSQL ProcessOn Prometheus Proxmox VE SOCKS5 SSL Session Shell Subagent TTS TimeMachine UML Uptime Kuma VPN VPS Web Windows Workers activate ad adb adblock agent aligenie aliyun alpine annotation aop authy autofs backup baidupan bash bitwarden boot brew browser caddy2 cdn centos cert certbot charles chat chrome classloader client clone closures cloudflare cmd command commit container crontab ctyun ddsm demo dependency deploy developer devtools dll dns docker domain download draw drawio dsm dump dylib edge exception export fail2ban feign firewall-cmd flow frp frpc frps fuckgfw function gcc gfw git github golang gperftools gridea grub gvt-g hacs havcs heap hello hexo hibernate hidpi hoisting homeassistant hosts html htmlparser https idea image img img2kvm import index install intel io ios ip iptables iptv ipv6 iso java javascript jetbrains jni jnilib jpa js json jsonb jupter jupyterlab jvm k8s kernel key kid kms kodi koolproxy koolproxyr kvm lan lastpass launchctl learning lede letsencrypt linux live low-code lvm lxc m3u8 mac macos mariadb markdown maven md5 microcode mirror modem modules monitor mount mstsc mysql n2n n5105 nas network nfs node node-red nodejs nohup notepad++ npm nssm ntp oop openfeign openssl os otp ovz p14 packet capture pat pdf pem perf ping pip plugin png powerbutton print pro proxy pve pvekclean python qcow2 qemu qemu-guest-agent rar reboot reflog remote remote desktop renew repo resize retina root route router rule rules runtime safari sata scipy-notebook scoping scp server slmgr so socks source spk spring springboot springfox ssh ssl stash string supernode svg svn swagger sync synology systemctl systemd tap tap-windows tapwindows telecom template terminal tls token totp tvbox txt ubuntu udisk ui undertow uninstall unlocker upgrade url v2ray vhd vim vlmcsd vm vmdk web websocket wechat windows with worker wow xiaoya xml yum zip 中国电信 云电脑 交换机 人机协作 代理 体检 健康检查 光猫 公网IP 内存 内网IP 写作 升级 协作 博客 反向代理 启动 夏令时 多智能体 多节点 多节点管理 天猫精灵 天翼云 安全 安装 定时任务 容器 容器网络 导入 小米 常用软件 广告屏蔽 序列号 应用市场 异常 感悟 打工 打工人 技术 抓包 排查 描述文件 故障 故障排查 效率 效率工具 无服务器 日记 时区 显卡虚拟化 智能家居 智能音箱 服务管理 架构 梯子 模块 流程 流程图 浏览器 漫游 激活 火绒 焦虑 玄学 生活 电信 画图 监控 直播源 直觉 端口扫描 管理 续期 网关 网络 网络风暴 群晖 脚本 腾讯 自动化 虚拟机 认证 证书 语雀 超时 路由 路由器 软件管家 软路由 运维 运维监控 通信机制 部署 配置 钉钉 镜像 镜像源 门窗传感器 问题排查 防火墙 阿里云 阿里源 集客

Hitokoto

Archive

排查一次诡异的健康检查假阳性:论监控系统的边界问题

排查一次诡异的健康检查假阳性:论监控系统的边界问题

前言

在运维工作中,我们通常会配置各种监控来保障服务的稳定性。然而,今天遇到的一个案例让我深刻认识到:监控系统的局限性可能导致严重的”假阳性”——监控系统显示一切正常,而实际业务已经出现严重问题。

本文将详细记录这次排查过程,分析问题根因,并提出改进方案。希望能给遇到类似问题的同学一些参考。

问题背景

业务场景

我们的 OpenClaw 系统部署在某云服务器上,通过钉钉和飞书等渠道接收用户消息。系统整体运行稳定,监控覆盖了各节点的核心指标。

问题现象

监控面板显示:

  • 所有服务状态:UP
  • Gateway 在线
  • 数据库连接正常
  • API 响应时间:23毫秒(平均)
  • 告警数量:0

实际情况:

  • 部分用户反馈消息处理很慢
  • 约20%的请求超时
  • 50%的请求响应时间超过500毫秒
  • 用户体验明显下降

明明监控显示一切正常,但用户已经开始投诉了。这是典型的监控假阳性——监控系统报告一切正常,但实际业务已经出现问题。

环境信息

  • 操作系统:Ubuntu 24.04 LTS
  • 应用:OpenClaw Gateway
  • 数据库:MySQL 8.0
  • 监控:Prometheus + Grafana
  • 语言:Node.js

问题分析

第一步:确认监控数据来源

首先,我需要了解监控采集的是哪些数据:

1
2
3
4
5
6
# 查看 Prometheus 抓取的目标
curl http://localhost:9090/api/v1/targets | jq '.data.activeTargets[] | {job: .labels.job, endpoint: .scrapeUrl}'

# 查看当前抓取的健康检查指标
curl -G 'http://localhost:9090/api/v1/query' \
--data-urlencode 'query=up{job="openclaw-gateway"}'

结果发现,健康检查端点只是一个简单的状态返回:

1
2
curl http://localhost:18789/health
# 返回:{"status":"UP"}

这个端点只返回”UP”字符串,根本不测试实际的业务逻辑。

第二步:模拟真实用户请求

为了验证实际响应时间,我编写了一个测试脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/bin/bash

# 测试脚本 - 模拟真实用户请求
BASE_URL="http://localhost:18789"
TEST_COUNT=100

echo "开始测试,共 $TEST_COUNT 次请求..."
echo "========================================"

success=0
timeout=0
total_time=0

for i in $(seq 1 $TEST_COUNT); do
start_time=$(date +%s%3N)
response=$(curl -s -w "\n%{http_code}\n%{time_total}" \
-o /dev/null \
--max-time 10 \
"$BASE_URL/api/v1/status")

end_time=$(date +%s%3N)
elapsed=$((end_time - start_time))

http_code=$(echo "$response" | tail -1)

if [ "$http_code" = "000" ]; then
timeout=$((timeout + 1))
echo "[超时] 请求 #$i"
elif [ "$http_code" = "200" ]; then
success=$((success + 1))
total_time=$((total_time + elapsed))
echo "[成功] 请求 #$i - ${elapsed}ms"
else
echo "[失败] 请求 #$i - HTTP $http_code"
fi
done

echo "========================================"
echo "测试完成:成功 $success 次,超时 $timeout 次"
if [ $success -gt 0 ]; then
echo "平均响应时间:$((total_time / success))ms"
fi

运行结果令人震惊:

1
2
测试完成:成功 78 次,超时 22 
平均响应时间:687ms

而监控显示的是”平均23毫秒”——差距高达30倍!

第三步:深入排查

既然确认了问题存在,我开始深入排查根因。

3.1 检查数据库连接池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 查看当前连接池使用情况
mysql -h localhost -u root -p -e "
SELECT
id AS connection_id,
user,
db,
command AS current_command,
time AS duration_seconds,
state
FROM information_schema.processlist
WHERE command != 'Sleep'
ORDER BY time DESC;
"

# 查看最大连接数配置
mysql -h localhost -u root -p -e "SHOW VARIABLES LIKE 'max_connections';"

发现连接池使用率已经超过90%,很多请求在等待可用连接。

3.2 检查慢查询日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 开启慢查询日志
mysql -h localhost -u root -p -e "SET GLOBAL slow_query_log = 'ON';"
mysql -h localhost -u root -p -e "SET GLOBAL long_query_time = 0.5;" # 超过500ms的查询都记录

# 查看慢查询
mysql -h localhost -u root -p -e "
SELECT
start_time,
query_time,
lock_time,
rows_sent,
rows_examined,
db,
LEFT(query, 100) AS query_preview
FROM mysql.slow_log
ORDER BY start_time DESC
LIMIT 10;
"

发现了多个超过1秒的查询,都是缺少索引的关联查询。

3.3 分析应用层日志

1
2
3
4
5
6
7
# 查看最近的应用错误日志
tail -100 /var/log/openclaw/error.log | grep -E "(pool|timeout|connection)"

# 典型错误日志:
# [2026-04-16T10:23:45.123Z] ERROR - Connection pool exhausted, waiting 5000ms for connection
# [2026-04-16T10:24:12.456Z] ERROR - Request timeout after 10000ms
# [2026-04-16T10:25:33.789Z] WARN - Slow query detected: 2341ms

第四步:定位根因

经过以上排查,问题根因逐渐清晰:

根因一:健康检查端点太简单

原有的 /health 端点只返回 {"status":"UP"},根本不测试实际的业务逻辑。这意味着即使用户请求超时,只要进程还在运行,健康检查就会显示”UP”。

根因二:连接池配置不合理

数据库连接池最大值是50,而实际运行中经常需要超过50个连接。这导致请求在等待连接时超时。

根因三:部分查询缺少索引

慢查询日志显示,有几个关联查询需要扫描数万行数据才能返回结果,最长的一个查询耗时2.3秒。

解决方案

方案一:改进健康检查端点(推荐)

新的健康检查应该包含实际的业务逻辑测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 新的健康检查端点 - 位置:app.js
app.get('/health', async (req, res) => {
const startTime = Date.now();
const checks = {
database: { status: 'unknown', latency: 0 },
cache: { status: 'unknown', latency: 0 },
external: { status: 'unknown', latency: 0 }
};

try {
// 1. 测试数据库连接(真实查询)
const dbStart = Date.now();
await db.query('SELECT 1');
checks.database = {
status: 'UP',
latency: Date.now() - dbStart
};
} catch (err) {
checks.database = {
status: 'DOWN',
error: err.message
};
}

try {
// 2. 测试缓存
const cacheStart = Date.now();
await redis.ping();
checks.cache = {
status: 'UP',
latency: Date.now() - cacheStart
};
} catch (err) {
checks.cache = {
status: 'DOWN',
error: err.message
};
}

try {
// 3. 测试外部依赖(如API网关)
const extStart = Date.now();
await axios.get('http://api-gateway:8080/health');
checks.external = {
status: 'UP',
latency: Date.now() - extStart
};
} catch (err) {
checks.external = {
status: 'DOWN',
error: err.message
};
}

const overallLatency = Date.now() - startTime;
const allHealthy = Object.values(checks).every(c => c.status === 'UP');

res.status(allHealthy ? 200 : 503).json({
status: allHealthy ? 'UP' : 'DEGRADED',
timestamp: new Date().toISOString(),
latency: overallLatency,
checks: checks,
version: process.env.APP_VERSION || 'unknown'
});
});

方案二:调整数据库连接池配置

1
2
3
4
5
6
7
8
9
10
11
// 位置:config/database.js
module.exports = {
pool: {
min: 5, // 最小连接数
max: 100, // 从50提升到100,应对高峰
acquireTimeout: 10000, // 获取连接超时时间
idleTimeout: 30000, // 空闲连接超时
evictionRunsInterval: 1000, // 检查间隔
numIdleTimeoy: 30000 // 空闲超时
}
};

方案三:添加缺失的数据库索引

1
2
3
4
5
6
7
8
-- 为慢查询添加索引
ALTER TABLE messages ADD INDEX idx_created_at (created_at);
ALTER TABLE messages ADD INDEX idx_user_id_created (user_id, created_at);
ALTER TABLE sessions ADD INDEX idx_token_expires (token, expires_at);

-- 验证索引添加成功
SHOW INDEX FROM messages;
SHOW INDEX FROM sessions;

方案四:完善监控告警配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Prometheus 告警规则 - 位置:alerts/health.yml
groups:
- name: openclaw-health
interval: 30s
rules:
# 健康检查响应时间告警
- alert: HealthCheckSlow
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{path="/health"}[5m])) > 0.5
for: 5m
labels:
severity: warning
annotations:
summary: "健康检查响应时间过长"
description: "健康检查P95响应时间超过500ms,当前值:{{ $value }}s"

# 健康检查失败告警
- alert: HealthCheckFailed
expr: rate(http_requests_total{path="/health",status!="200"}[5m]) > 0
for: 1m
labels:
severity: critical
annotations:
summary: "健康检查失败"
description: "健康检查端点返回非200状态码"

# 连接池使用率告警
- alert: DatabasePoolExhausted
expr: db_pool_in_use / db_pool_max > 0.9
for: 5m
labels:
severity: warning
annotations:
summary: "数据库连接池使用率过高"
description: "连接池使用率超过90%,当前值:{{ $value | humanizePercentage }}"

一键排查脚本

如果遇到类似的”监控正常但业务异常”问题,可以使用以下脚本进行快速排查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#!/bin/bash
# 文件名:health-check-deeper.sh
# 用途:深度健康检查脚本

echo "=========================================="
echo "深度健康检查开始..."
echo "时间:$(date '+%Y-%m-%d %H:%M:%S')"
echo "=========================================="
echo ""

# 1. 测试基础连通性
echo "[1/8] 测试基础连通性..."
if curl -s --max-time 5 http://localhost:18789/health > /dev/null; then
echo " ✓ 基础连通性正常"
else
echo " ✗ 基础连通性失败"
fi

# 2. 测试真实业务端点
echo "[2/8] 测试真实业务端点..."
start=$(date +%s%3N)
response=$(curl -s --max-time 10 http://localhost:18789/api/v1/status)
elapsed=$(($(date +%s%3N) - start))
echo " 响应时间:${elapsed}ms"
if [ $elapsed -gt 1000 ]; then
echo " ⚠ 警告:响应时间超过1秒"
fi

# 3. 测试数据库连接
echo "[3/8] 测试数据库连接..."
if mysql -h localhost -u root -p"${MYSQL_PASSWORD}" -e "SELECT 1" > /dev/null 2>&1; then
echo " ✓ 数据库连接正常"
else
echo " ✗ 数据库连接失败"
fi

# 4. 检查数据库连接池
echo "[4/8] 检查数据库连接池..."
pool_usage=$(mysql -h localhost -u root -p"${MYSQL_PASSWORD}" -N -e "
SELECT COUNT(*)
FROM information_schema.processlist
WHERE command != 'Sleep';
")
echo " 当前活动连接数:$pool_usage"
if [ $pool_usage -gt 80 ]; then
echo " ⚠ 警告:连接数过高"
fi

# 5. 测试缓存
echo "[5/8] 测试缓存..."
if redis-cli ping > /dev/null 2>&1; then
echo " ✓ 缓存正常"
else
echo " ✗ 缓存失败"
fi

# 6. 检查磁盘空间
echo "[6/8] 检查磁盘空间..."
disk_usage=$(df / | tail -1 | awk '{print $5}' | sed 's/%//')
echo " 磁盘使用率:${disk_usage}%"
if [ $disk_usage -gt 80 ]; then
echo " ⚠ 警告:磁盘使用率过高"
fi

# 7. 检查内存使用
echo "[7/8] 检查内存使用..."
mem_usage=$(free | grep Mem | awk '{printf "%.0f", $3/$2 * 100}')
echo " 内存使用率:${mem_usage}%"
if [ $mem_usage -gt 85 ]; then
echo " ⚠ 警告:内存使用率过高"
fi

# 8. 检查进程状态
echo "[8/8] 检查关键进程..."
if pgrep -f "openclaw-gateway" > /dev/null; then
echo " ✓ Gateway 进程运行中"
else
echo " ✗ Gateway 进程未运行"
fi

echo ""
echo "=========================================="
echo "深度健康检查完成"
echo "=========================================="

常见问题解答

Q1:为什么健康检查端点不能只返回”UP”?

A:因为”UP”只能说明进程还在运行,不能说明服务能正常处理请求。一个真正的健康检查应该包含实际的业务逻辑测试,比如数据库查询、缓存读写、外部依赖调用等。

Q2:如何判断监控是否有效?

A:可以问自己一个问题:当用户开始投诉时,我的监控是否能第一时间发现? 如果答案是”不能”,那监控就需要改进了。

Q3:健康检查应该测试哪些内容?

A:建议包含以下内容:

  1. 依赖服务(数据库、缓存、外部API)
  2. 关键业务逻辑
  3. 响应时间
  4. 资源使用率

Q4:监控过度会导致什么问题?

A:过多的监控会增加系统负载,还可能产生告警疲劳。建议只监控关键指标,设置合理的告警阈值。

Q5:如何避免监控假阳性?

A:核心思路是让监控更贴近真实用户场景:

  1. 健康检查要包含实际业务逻辑
  2. 告警阈值要根据用户体验来设置,而不是技术指标
  3. 定期审查监控配置,确保覆盖真实风险点

经验总结

  1. 监控要跟着业务走,而不是跟着技术走。 技术指标只是表象,业务指标才是本质。

  2. “UP”不等于”正常”。 健康检查显示UP,只能说明进程还在运行,不能说明用户体验良好。

  3. 定期审查监控配置。 随着业务发展,原有的监控可能已经不能覆盖新的风险点,需要定期更新。

  4. 主动监控优于被动告警。 与其等待用户投诉,不如主动监控真实用户体验。

  5. 告警阈值要合理。 设置得太宽松会漏问题,设置得太严格会产生告警疲劳。

延伸阅读

结语

这次问题虽然最终解决了,但过程让我深刻认识到监控系统的局限性。最好的监控系统不是告诉你”一切正常”,而是能提前发现潜在问题。

希望这篇文章能帮到你。如果你也有类似的经历,欢迎在评论区分享。


作者:小六,一个在上海努力搬砖的程序员

Author:Margrop
Link:http://blog.margrop.com/post/2026-04-16-troubleshooting-health-check-false-positive/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可