监控与告警:在线数、消息延迟与连接质量
长连接服务的可用性不能只看「接口返回 200」,而要看「在线数有没有掉、消息有没有积压、连接质量有没有劣化」。四类指标缺一不可:连接数量类、连接质量类、消息延迟类、进程资源类。少了延迟类,故障只能靠用户投诉发现;少了质量类,就只能看到「掉线了」而不知道为什么掉。
指标清单
| 指标名 | 类型 | 含义 | 用途 |
|---|---|---|---|
im_ws_online_connections{app,gateway} | Gauge | 当前在线连接数 | 最核心的业务指标,告警首选 |
im_ws_online_users{app} | Gauge | 在线用户数(多端去重) | 与连接数之比反映多端与重连情况 |
im_ws_connects_total{app,gateway,result} | Counter | 建连次数,result=ok|fail | 建连速率与失败率 |
im_ws_disconnects_total{app,gateway,reason} | Counter | 断开次数,按原因分类 | 定位掉线根因 |
im_ws_handshake_duration_seconds{stage} | Histogram | 握手耗时,stage=upgrade|auth|register | 区分是 TLS、鉴权还是注册慢 |
im_ws_messages_received_total{type} | Counter | 上行消息数(区分业务与心跳) | 业务量与心跳分开看 |
im_ws_messages_sent_total{type,result} | Counter | 下行消息数与发送结果 | 发送失败率 |
im_ws_push_duration_seconds{target} | Histogram | 推送耗时,target=unicast|multicast|broadcast | 推送延迟分位 |
im_ws_write_queue_length{gateway} | Histogram | 写队列长度分布 | 背压前兆 |
im_ws_dropped_total{reason} | Counter | 丢弃数,reason=queue_full|slow_client|rate_limited | 主动丢弃必须可观测 |
im_ws_heartbeat_rtt_seconds | Histogram | 心跳往返延迟 | 连接质量的核心指标 |
im_ws_auth_errors_total{reason} | Counter | 鉴权失败原因分布 | 密钥轮换或恶意连接 |
go_goroutines / process_open_fds / process_max_fds | Gauge | 协程、fd 与上限 | 泄漏与容量水位 |
标签纪律:绝对不要用 uid、conn_id、room_id、msg_id 做 label。它们的基数等于在线数或消息数,会直接把时间序列数据库打爆;这类维度只能进日志或链路追踪。
常用查询
# 在线数(分实例)
im_ws_online_connections
# 5 分钟净增,区分「正常波动」与「持续泄漏/掉线」
sum(im_ws_online_connections) - sum(im_ws_online_connections offset 5m)
# 建连成功速率与失败率
sum(rate(im_ws_connects_total{result="ok"}[1m]))
sum(rate(im_ws_connects_total{result="fail"}[1m]))
/ sum(rate(im_ws_connects_total[1m]))
# 断开原因分布(Top5),一眼看出是超时还是客户端主动关
topk(5, sum(rate(im_ws_disconnects_total[5m])) by (reason))
# 推送延迟 P99(按单播/组播/广播分开)
histogram_quantile(0.99, sum(rate(im_ws_push_duration_seconds_bucket[5m])) by (le, target))
# 每用户连接数,异常升高说明客户端在疯狂重连
sum(im_ws_online_connections) / sum(im_ws_online_users)
# 协程/连接比,正常应接近 2~3
sum(go_goroutines) / sum(im_ws_online_connections)
# 丢弃率
sum(rate(im_ws_dropped_total[5m])) / sum(rate(im_ws_messages_sent_total[5m]))
curl -s 'http://127.0.0.1:9090/api/v1/query' \
--data-urlencode 'query=sum(im_ws_online_connections)' | jq -r '.data.result[0].value[1]'
102437
排查时必须能按实例下钻:整体在线数只掉 3%,但某一台网关掉了 90%,全局平均值会完全掩盖这个事实。
消息延迟怎么量
延迟要拆成三段测,混在一起就永远说不清是客户端慢还是服务端慢。
| 段落 | 测量方式 | 目标值(同城) | 判读 |
|---|---|---|---|
| 上行(客户端到服务端) | 消息体带 client_ts,收到的时刻相减 | P99 < 150ms | 受客户端时钟影响,只看相对变化 |
| 服务端内部 | 消息入队时刻到写成功时刻 | P99 < 50ms | 最干净的指标,也是告警首选 |
| 下行(服务端到客户端) | 客户端回 ACK 带 msg_id,发送到 ACK 的耗时 | P99 < 200ms | 与心跳 RTT 交叉验证 |
| 心跳 RTT | ping 发出到收到 pong | P99 < 100ms | 反映连接质量基线,不掺业务逻辑 |
内部延迟用一行代码就能埋好:
start := time.Now()
if err := c.Send(msg); err == nil {
pushDuration.WithLabelValues("unicast").Observe(time.Since(start).Seconds())
}
告警阈值建议
| 告警 | 触发条件 | 级别 | 处置 |
|---|---|---|---|
| 在线数骤降 | 5 分钟跌幅超过基线 20%(且绝对值 > 5000) | P0 | 先看是否全实例同跌,再查网关与代理 |
| 建连失败率 | 失败率 > 1% 持续 3 分钟 | P1 | 看 handshake_duration 是哪一段变慢 |
| 推送延迟劣化 | push P99 > 500ms 持续 5 分钟 | P1 | 查写队列水位、慢网关、GC |
| 写队列水位 | 队列长度 P99 超过容量 80% 持续 2 分钟 | P1 | 触发背压处置,摘除慢客户端 |
| 消息丢弃 | 丢弃率 > 0.1% 持续 5 分钟 | P2 | 评估限流阈值与客户端版本 |
| 协程泄漏前兆 | 协程/连接比 > 5,或 1 小时涨幅 > 20% | P1 | 按第 17 章抓栈快照 |
| fd 水位 | process_open_fds / process_max_fds > 0.8 | P1 | 提升上限并查泄漏 |
| 读超时占比 | read_timeout 占断开原因 > 20% | P2 | 排查网络、客户端心跳、代理 idle 超时 |
| 鉴权失败突增 | 5 分钟内失败数超过基线 10 倍 | P1 | 区分密钥轮换与恶意扫描 |
三条定阈值的纪律:必须带时间窗口(for: 3m),避免瞬时抖动误报;必须带流量下界(在线数 < 1000 时不报警),否则凌晨必误报;必须按实例或按机房分组,全局平均值等于没有告警。
大盘分层
| 层 | 看什么 | 判读 |
|---|---|---|
| 总览(一屏) | 在线数、msg/s、推送 P99、错误率、丢弃率 | 5 秒内判断「有没有事」 |
| 网关 | 分实例在线数、CPU、fd、协程、写队列 | 定位单机异常与容量倾斜 |
| 客户端 | 按平台与版本的重连次数、握手失败率 | 定位客户端缺陷或发布影响 |
| 接入层 | 101 响应码比例、upstream 超时、代理连接数 | 区分接入层与应用层责任 |
日志与采样
- 连接生命周期日志字段:
conn_id、uid、gateway、remote_ip、握手耗时、断开原因、在线时长。 - 慢推送日志:内部推送耗时超过 200ms 打一条,带
target、目标数、最慢网关。 - 不要为每条消息打一行 info 日志:10 万在线连接即使 30 秒一次心跳也有 3.3k 行/秒,磁盘会先撑不住。
- 指标标签名与日志字段名保持一致,排查时才能在指标与日志之间来回印证。
常见坑
| 坑 | 后果 | 正确做法 |
|---|---|---|
| 只监控在线数,不看断开原因 | 知道掉线却不知道为什么 | disconnects_total 按 reason 拆分 |
| 把心跳计入消息量 | 消息曲线变成心跳曲线,业务变化被掩盖 | 指标按 type 区分业务与心跳 |
用 uid 做 label | 时间序列基数爆炸,监控系统自身故障 | 高基数维度只进日志与链路 |
| 只对全局聚合告警 | 单实例掉线 90% 无法触发 | 按实例/机房分组并设置最小实例数 |
| 阈值写死绝对值 | 业务增长后频繁误报,运维开始忽略 | 基于基线、同比与百分比 |
| 忽略采集失败 | 目标进程消失后曲线是「平的」,看着正常 | 对 up == 0 与 scrape 失败单独告警 |
| 只算连接数不算用户数 | 客户端重连一次就多算一条在线 | 两者都采集,看比值是否异常 |
巡检项
- 每周核对在线数、各网关连接数之和、路由表条目数、Redis 计数四方是否一致,差异超 1% 即查。
- 每季度做一次告警演练:手动触发核心告警,验证通知真的能到人。
- 所有大盘必须有「昨天同一时刻」对比线与发布事件标注。
- 指标采集失败、日志写入失败、磁盘水位都必须有独立告警,静默故障最危险。
小结:在线数、连接质量、消息延迟、进程资源四类指标缺一不可——用 im_ws_online_connections 与 im_ws_connects_total{result} 区分「掉线」和「重连」,用 disconnects_total{reason} 定位掉线根因,把延迟拆成上行、内部、下行与心跳 RTT 分段测量;告警必须带时间窗口、流量下界与实例维度,并把高基数维度留在日志里,才能既看得见问题又不把监控系统自己压垮。