对讲机(连接)的一生:敲门 → 通话 → 广播 → 挂断销号。覆盖核心盘 🔵 组 7 张面板(c20–c26)· 后端核心仪表盘 siliang-backend。
WebSocket 就是对讲机:HTTP 是一问一答就挂电话,WebSocket 是接通后一直不挂、双方随时喊话。画布协作(你拖一个框、同伴立刻看到)全靠这 7 张面板盯着的“神经系统”。
这组图回答的只有一个问题:这条神经还通吗——连接接得进吗(c21)、消息传得动吗(c24/c25)、人还在吗(c26)、挂断挂得干净吗(c20/c22/c23)。
想象一间办公室,A 和 B 合伙画一张大图纸,每人手里一台对讲机。A 画了一笔,对着对讲机喊“我改了左上角!”;B 听到,立刻在自己那张图上照做——这就是 WebSocket 协作:连接一直是通的,谁有变更随时喊,不用像 HTTP 那样每次都重新拨号。
但办公室有两层楼(生产上是两台机器)。A 在 1 楼、B 在 2 楼,对讲机信号穿不了楼板。于是楼里装了个广播站(Redis pubsub):1 楼收到 A 的消息先广播给全楼,2 楼的对讲机再转告 B。广播站一坏,两人就“广播失明”——你画你的、我画我的,谁也看不见谁,这就是 redis_pubsub > 0 这条错误线的含义。
下班时要在前台登记簿上销号(teardown)。销号失败,登记簿上就留下“幽灵”:人走了,系统还以为他在——消息照发(发给空气)、名额照占(连接数只涨不跌)。这组 7 张图,盯的就是对讲机从敲门、通话到挂断销号的整条生命周期。
| 类比里的东西 | 系统里对应的东西 |
|---|---|
| 对讲机 | WebSocket 长连接(siliang_ws_connections_active)。一人开两个页签就是两台对讲机,所以连接数 ≠ 人数。 |
| 敲门 + 门卫查证件 | WS 握手三道门:鉴权 token / 连接数上限 / Origin 校验,结果按 outcome 记在 siliang_ws_connections_total(c21)。 |
| 对讲机里喊“我改了这行” | 业务消息(不含心跳):接收是 c24(用户动作进服务器),发送是 c25(服务器广播给房间里其他人)。 |
| 每隔几秒喊“还在吗” | 心跳 presence:ping / cursor / select(c26),顺带上报“这个人的鼠标在动”。 |
| 楼内广播站 | Redis pubsub 跨实例广播。广播线出问题 = redis_pubsub > 0(c22),两台机器各自为政。 |
| 下班销号 | teardown 清理。销不干净 = 幽灵 session(c22 的 teardown_step)+ 时长 P99 异常长(c23)+ 活跃连接只涨不跌(c20)。 |
# HTTP: 一问一答就挂;WS: 接通后随时互喊 # 握手走一次 HTTP Upgrade,之后长连接全双工
sum(siliang_ws_connections_active{job="siliang_backend_worker"})
# 连接数 ≈ 在线用户数 × 人均页签数(约每人 1–2 条)sum(rate(siliang_ws_connections_total[5m])) by (outcome) # accepted 之外任何线抬头 → 对照 outcome 定位是哪道门在拦人
sum(rate(siliang_ws_heartbeat_messages_total[5m])) by (type) # 与在线人数同节奏的稳定脉冲;归零 = 僵尸警报
sum(rate(siliang_ws_errors_total[5m])) by (error_type) # redis_pubsub > 0 = 广播失明:跨机器协作已断
# teardown_step 高 = Redis 故障正在制造幽灵 session # 旁证:内存盘「WS 会话互校验」两线分叉(m120)
histogram_quantile(0.99, sum(rate(siliang_ws_connection_duration_seconds_bucket[5m])) by (le)) # P99 异常长 = 有连接该断没断(与 c20 互证)
_active 结尾是 Gauge(温度计,直接读数);_total 结尾是 Counter(里程表,必须配 rate 看速度)。写错口径曲线就废了。 # _active(连接数)= Gauge,直接 sum 读数 # _total(连接/消息/心跳数)= Counter,必须 rate()
# 画布协作的“在线” → siliang_ws_*(本组) # 聊天的“在线” → siliang_chat_resume_active_*(c2/c6)
# 活跃度两问: # 谁在做什么动作 → messages_received by (type)(c24) # 还有多少活人 → heartbeat_messages by (type)(c26)
它是什么:画布协作此刻有多少条“对讲机”在线——所有 worker 上活跃 WebSocket 连接的总和,Gauge 直接读数。它和「在线用户数」是近亲:用户数按人去重,连接数不去重,所以一人开两个页签就是两条连接。
回答什么问题:现在有多少条活连接在服务?连接有没有只涨不跌(泄漏/僵尸堆积)?
✅ 与在线用户数成比例(约每人 1–2 条),随业务节奏起伏;<100 绿 / 100–500 黄。
🚨 持续上涨不回落 = 连接泄漏(断了没回收)——先看 c23 时长 P99 和 c22 的 teardown_step 互证,再对照发布记录;100–500 之间就该警惕,别等爆了才看。
# 面板 PromQL(照抄主图谱口径) sum(siliang_ws_connections_active{job="siliang_backend_worker"})
联动:主图谱 c20 ↗ · 在线用户数 c1(人数口径) · 相关课程:在线用户与协作规模 / WS 会话内存
它是什么:敲门之后的结果分布,按 outcome 拆成几条线:accepted=请进;其他是被拒之门外的各种原因——鉴权失败 / 超限 / Origin 不对。是 Counter,用 rate 看每秒速率。
回答什么问题:用户反馈“连不上画布”时,到底是哪道门把他拦住了?
✅ accepted 一枝独秀,其他拒绝线长期贴地。
🚨 哪种拒绝高就是哪类问题:鉴权失败高 = token 问题(前端或网关);超限高 = 撞连接数上限(先查 c20 是不是泄漏占满名额);Origin 高 = 跨域配置问题(多半伴随发布/域名变更)。
# 面板 PromQL(照抄主图谱口径) sum(rate(siliang_ws_connections_total[5m])) by (outcome)
联动:主图谱 c21 ↗ · 活跃连接数 c20(超限时先看它) · 相关课程:5xx vs 4xx / HTTP 健康
它是什么:协作神经系统的“病变类型”分布,按 error_type 拆开。两类重点病变:teardown_step 高 = Redis 故障留下“幽灵 session”(人走了系统以为还在);redis_pubsub > 0 = 跨实例广播失明——你在 A 机器画,B 机器上的同伴看不到。
回答什么问题:协作出问题的时候,病在哪条神经上?是挂断销号坏了,还是广播线断了?
✅ 长期贴地。
🚨 redis_pubsub 持续 > 0 = 广播失明,用户可感知的“协作失灵”,立即排查 Redis 健康与广播池(内存盘 m117 的 broadcaster_pubsub 池);teardown_step 高 = 幽灵 session 在堆积,对照 m120 会话互校验。
# 面板 PromQL(照抄主图谱口径) sum(rate(siliang_ws_errors_total[5m])) by (error_type)
联动:主图谱 c22 ↗ · Redis 广播池 m117 · 相关课程:连接池台账 / WS 会话内存
它是什么:一条“对讲机”从接通到挂断平均开多久(直方图插值出 P50/P95/P99 分位数)。连接总要结束——用户关页面、超时被踢——时长分布能暴露“该死的没死”。
回答什么问题:连接的生死轮回正常吗?有没有挂不掉的僵尸连接?
✅ 分位数形态稳定:P50 反映典型使用时长,P99 不异常拉长、不随时间持续爬升。
🚨 P99 异常长 = 僵尸连接(teardown 清理没触发)——与 c20 活跃连接只涨不跌互证,去查 teardown 逻辑与 Redis 健康。
# 面板 PromQL(照抄主图谱口径) histogram_quantile(0.50/0.95/0.99, sum(rate(siliang_ws_connection_duration_seconds_bucket[5m])) by (le))
联动:主图谱 c23 ↗ · 错误类型 c22(幽灵旁证) · 相关课程:P50/P95/P99 / 直方图水桶
它是什么:↑ 用户在画布上做的每个动作(拖拽/编辑…)传到服务器的速度,按 event type 拆开。注意口径是“不含心跳”——心跳单独算在 c26,这里只看真正的业务动作。
回答什么问题:协作活跃度如何?哪种操作最频繁(决定优化优先级)?
✅ 跟业务节奏走,工作时段起伏、深夜落回低位;与发送 QPS(c25)同步呼吸。
🚨 接收突然归零 = 用户侧断连或接入层故障(对照 c21 拒绝线和 c20 连接数);无活动时段却暴涨 = 异常客户端/脚本在刷。
# 面板 PromQL(照抄主图谱口径) sum(rate(siliang_ws_messages_received_total[5m])) by (type)
联动:主图谱 c24 ↗ · 发送 QPS c25(成对看) · 相关课程:rate() 算速度
它是什么:↓ 服务器把变更广播给房间里其他人的速度。接收是“进话”,发送是“出话”,一对进出要放在一起看才有意义。
回答什么问题:广播压力多大?广播链路(pubsub)还通吗?
✅ 与接收 QPS 同起伏——有人进话就有人出话,比例稳定。
🚨 接收正常而发送归零 = 广播链路(pubsub)出问题——马上看 c22 的 redis_pubsub 线和内存盘 m117 广播池是否打满。
# 面板 PromQL(照抄主图谱口径) sum(rate(siliang_ws_messages_sent_total[5m])) by (type)
它是什么:客户端每隔几秒报一次“我还活着”(presence:ping)和“我的光标在这”(cursor/select)。这是“活人”的直接计量——连接可以骗人,心跳不会。
回答什么问题:有多少“活人”在动?连接数里的水分有多大?
✅ 与在线人数同节奏的稳定脉冲;按 type 拆开各条线比例稳定。
🚨 心跳归零但连接还在 = 全是僵尸连接——对照 c20 若同时只涨不跌,立即排查 teardown 清理链路。
# 面板 PromQL(照抄主图谱口径) sum(rate(siliang_ws_heartbeat_messages_total[5m])) by (type)
联动:主图谱 c26 ↗ · 活跃连接数 c20(僵尸互证) · 相关课程:rate() / 在线用户口径
典型的“广播失明”投诉。别急着重启,三步定位:先确认是不是广播线断了,再确认是单机问题还是 Redis 整体不稳,最后查广播池有没有被打满。
# 第 1 步:先看是不是“广播失明”(跨机器协作断) sum(rate(siliang_ws_errors_total{error_type="redis_pubsub"}[5m])) # > 0 即实锤:Redis pubsub 广播线出问题,两台机器各自为政 # 第 2 步:按 instance 拆——单机失明还是 Redis 整体抖? sum(rate(siliang_ws_errors_total{error_type="redis_pubsub"}[5m])) by (instance) # 只有一台机器报 = 那台的 pubsub 订阅断了 # 两台都报 = Redis 本身在抖,去查 Redis 健康 # 第 3 步:看广播专用池是不是被打满(内存盘) max_over_time(siliang_redis_pool_connections{state="in_use", pool="broadcaster_pubsub"}[1m]) # 贴着 capacity = 广播借不到连接,消息发不出(对照 m117 使用率卡)
判读:redis_pubsub 回到 0 且发送 QPS(c25)恢复与接收同步起伏,才算修好。
连接数只涨不跌有两种可能:真热闹,或全是尸体。用“僵尸三联证”分辨:心跳、时长 P99、teardown 错误。
# 1) 确认形态:曲线是不是只涨不跌 sum(siliang_ws_connections_active{job="siliang_backend_worker"}) # 2) 三联证之一:心跳还有没有(活人才发心跳) sum(rate(siliang_ws_heartbeat_messages_total[5m])) # 连接在涨而心跳平平 = 新连接是真、旧连接是尸 # 3) 三联证之二:时长 P99 是不是异常长 histogram_quantile(0.99, sum(rate(siliang_ws_connection_duration_seconds_bucket[5m])) by (le)) # P99 拉长 = teardown 清理没触发,“该死的没死” # 4) 幽灵旁证:teardown_step 错误速率 sum(rate(siliang_ws_errors_total{error_type="teardown_step"}[5m])) # > 0 = Redis 故障正在制造幽灵 session(人走了登记还在)
“连不上”不是一种病,是四种。按 outcome 拆开,每种拒绝指向完全不同的病灶和负责人。
# 按 outcome 拆开看是哪道门在拦人 sum(rate(siliang_ws_connections_total[5m])) by (outcome) # 鉴权失败高 = token 过期/签发问题(前端或网关侧) # 超限高 = 撞连接数上限:先看 c20 是不是泄漏把名额占满 # Origin 高 = 跨域配置问题(多半伴随发布/域名变更) # 怀疑“超限”时的快速验证:对照活跃连接数绝对值 sum(siliang_ws_connections_active{job="siliang_backend_worker"}) # 若同时贴高位 → 大概率是泄漏占满名额,而不是真有那么多人 # 再用心跳 QPS 验水分:心跳没涨 = 名额被尸体占着
滚动发布时每个旧 worker 要把连接干净地交出去。逐 instance 看连接排空,发布后才算真正收工。
# 滚动发布时逐 worker 看:旧 instance 连接应依次排空归零 sum(siliang_ws_connections_active{job="siliang_backend_worker"}) by (instance) # 每个旧 instance 依次跌到 0 = 干净退场 # 卡在半空不归零 = 排空卡住(对照 HTTP 并发 c18 的 ops 排空口径) # 发布后协作功能回归:收发两条线应恢复同起伏 sum(rate(siliang_ws_messages_received_total[5m])) by (type) sum(rate(siliang_ws_messages_sent_total[5m])) by (type) # 有进有出 = 广播链路健康;发送归零 = pubsub 没接好
僵尸连接靠人盯图发现不了早期形态。用“连接爬升 + 心跳不涨”的组合条件做告警,让它在堆积早期就叫。
# 告警:连接斜率向上 且 心跳没有同步增长 = 疑似僵尸堆积 #(示例规则,阈值按业务节奏校准后启用) - alert: SiliangWSZombieSuspicion expr: | deriv(sum(siliang_ws_connections_active{job="siliang_backend_worker"})[1h:5m]) > 0 and sum(rate(siliang_ws_heartbeat_messages_total[5m])) < sum(rate(siliang_ws_heartbeat_messages_total[5m] offset 1h)) for: 15m labels: severity: warning annotations: summary: "WS 连接爬升但心跳未同步:疑似僵尸连接堆积,查 c23 P99 与 teardown_step"
判读:告警触发后先看 c23 的 P99——它拉长就是清理逻辑没跑,再看 c22 teardown_step 找 Redis 故障的痕迹。
# 错: sum(siliang_ws_connections_active) # → 当“在线人数”发周报 # 对: sum(siliang_ws_online_users{job="siliang_backend_worker"}) # → 按 user_id 去重
# 错: # 只看连接绝对值就下结论 # 对: sum(rate(siliang_ws_heartbeat_messages_total[5m])) # → 对照心跳验水分
_active 是温度计(Gauge),直接读数;对它求速率没有物理意义。正解:Counter 配 rate,Gauge 直接读。 # 错: rate(siliang_ws_connections_active[5m]) # → 对 Gauge 求 rate,无意义 # 对: sum(siliang_ws_connections_active{job="siliang_backend_worker"}) # → 直接读数
# 错: # 用 messages_received 判断“有多少人在动” → 混入了心跳,虚高 # 对: sum(rate(siliang_ws_heartbeat_messages_total[5m])) by (type) # → 心跳口径看活人
# 错: # 单点 redis_pubsub > 0 → 立即定级 P1 # 对: # 持续 > 0 + 发送 QPS 归零 → 才是广播失明,再按场景 1 下钻
# 错: histogram_quantile(0.50, …) # → P50 平稳就放心 # 对: histogram_quantile(0.99, sum(rate(siliang_ws_connection_duration_seconds_bucket[5m])) by (le))
# 错: histogram_quantile(0.99, rate(siliang_ws_connection_duration_seconds_bucket[5m])) # 对: histogram_quantile(0.99, sum(rate(siliang_ws_connection_duration_seconds_bucket[5m])) by (le))
# 错: # 症状: redis_pubsub > 0 → 直接滚动重启 backend # 对: # 正解: 先查 broadcaster_pubsub 池使用率(m117)与 Redis 健康
# 错: # 症状: 连不上 → 翻 backend 异常日志半天 # 对: sum(rate(siliang_ws_connections_total[5m])) by (outcome) # → 先定哪道门拦的
参考答案:先看 c22 错误类型分布——redis_pubsub 持续 > 0 就是广播失明(两台机器各自为政);再看 c25 发送 QPS 是否与 c24 接收同步,发送归零 = 广播链路断了;然后按 instance 拆错误线分辨单机订阅断还是 Redis 整体抖;最后查内存盘 m117 的 broadcaster_pubsub 池是否打满。全链路顺序和主图谱排查速查表“画布协作不同步”一行一致。
参考答案:c1 把所有 worker 上的 WS 会话按 user_id 去重后求和,是“多少人”;c20 是活跃连接总数,不去重——一人开两个页签就是两条连接,同一人跨 worker 还会被算成两人。所以连接数 ≈ 人数 × 人均页签数(约每人 1–2 条),它适合做容量和泄漏判断,不适合当“人数”汇报。
参考答案:幽灵 session 是“人走了、登记还在”——teardown 清理执行到一半 Redis 出故障,会话登记没销掉,系统还给他发消息、占名额。僵尸连接是更广的说法:连接没被正确关闭,服务器还替它占资源。露马脚的地方:c22 的 teardown_step 错误线、c23 时长 P99 异常长、c20 活跃连接只涨不跌、内存盘 m120 会话互校验两线分叉(注册表里有尸体)。
参考答案:心跳每隔几秒自动发一次,量级大且与“用户在做什么”无关;混进业务消息会把协作活跃度严重虚高。c26 回答的是“有多少活人”:活人才发心跳,心跳归零而连接还在 = 屋里全是僵尸。所以 c24/c25 回答“协作活跃度与广播压力”,c26 回答“连接里的水分”——三张图一起才能既看热闹又验人头。