每个 WebSocket 会话在服务器身上记两本账:前台登记簿(注册表)和门禁计数(活连接),两本账必须重合;每个会话还背一个 KB 级小背包(私有状态),背包总重超 50MB 就拉警报。覆盖 m120 · m121 · 后端内存仪表盘「🕸 WS 会话内存」组(内存盘 2 图)。
把每个 WebSocket 会话想成一个住店客人:前台有本登记簿(会话注册表,记着「谁住店」),大门有台门禁计数器(握手计数,数着「此刻真在大楼里的人」)。两本账必须一模一样——对不上,就是有人退了房、前台没销登记,系统里留下一个「尸体」。每个客人还随身背个小背包(会话私有状态:光标位置、选中集、待发消息),单包 KB 量级;背包总重 = 会话数 × 单包大小,总重超 50MB 拉警报。
为什么会有两本账?因为「登记」和「人真的在」是两件事。用户打开画布,握手成功——门禁 +1,前台登记 +1,两本账一起涨。用户关页面,服务器通常能听到「再见」——门禁 −1,前台销登记 −1。但网络是靠不住的:用户可能直接拔网线(杀进程、进隧道),服务器根本听不到「再见」,只能靠心跳超时兜底——几个心跳没人应,才认定人走了,把两边都减掉。
兜底不是百分百可靠。清理(teardown)是一串步骤,中途要动 Redis;如果 Redis 恰好抖一下,teardown 半路夭折——门禁那侧靠连接断开自己减掉了,前台这侧的登记却没销掉。于是同一个 worker 上,登记簿的线开始高过门禁的线,而且再也不回来:分叉 = 注册表里有尸体。尸体不释放内存、广播还照发给空气,这就是注册表泄漏的直接证据。
背包是另一本账:每个会话身上挂着「非共享状态」(协作里你的光标在哪、选中了哪些节点、待发消息队列……)。单包很小(KB 量级),但它是乘法:总量 = 会话数 × 单包大小。所以总量爆表只有两条路——会话数爆了(正常小包 × 巨多会话),或者有人往背包里塞了大对象(把不该放的图片、大 JSON 挂在了会话上)。50MB 告警线(口径卡规则)就是为这两条路设的。
| 类比里的东西 | 系统里对应的东西 |
|---|---|
| 前台登记簿 | siliang_ws_sessions_active:会话注册表里的会话数(登记了就算,断了没销也在) |
| 门禁闸机计数 | siliang_ws_connections_active:握手成功的活连接数(人真在,才计数) |
| 两本账一致 | 同一 worker 两条线重合 = 健康态;这是面板设计的判定基线 |
| 退房没销登记 | 两线分叉的差值 = 注册表里的「尸体」:连接断了登记没销(teardown 半路夭折) |
| 随身小背包 | 会话非共享状态:光标/选中集/待发队列,单包 KB 量级(_ws_session_state_bytes_bucket 看分布) |
| 行李总重与告警线 | siliang_ws_total_state_bytes 总量 > 50MB 触发告警(口径卡规则) |
# 两本账(同一 worker 对照,主图谱口径) siliang_ws_sessions_active # 登记簿:注册表里的会话数 siliang_ws_connections_active # 门禁:实际活的连接数
# 判定:同一 instance 两线相减,持续 > 0 不收敛 = 尸体 # 互证:核心盘 WS 错误面板 teardown_step 是否升高 sum(rate(siliang_ws_errors_total[5m])) by (error_type)
# 错: 把 worker-A 的登记簿和 worker-B 的连接数比 # 对: by (instance) 拆开,每对线各自对照 siliang_ws_sessions_active by (instance) # vs connections_active by (instance)
# 单包分布(P95)与总重各看各的 histogram_quantile(0.95, sum(rate(siliang_ws_session_state_bytes_bucket[5m])) by (le)) siliang_ws_total_state_bytes # 总重 Gauge,直读
# 超重先做除法,分辨是哪条路 # 总重 60MB ÷ 会话 2 万 ≈ 3KB/包 → 会话数问题 # 总重 60MB ÷ 会话 2 千 ≈ 30KB/包 → 背包被塞大对象
# 口径卡核心告警(WS 相关这条) siliang_ws_total_state_bytes > 50MB # → 会话爆 or 塞了大对象
sessions_active,核心盘趋势图拿它看「在线规模」,内存盘拿它当「登记簿」跟连接数对账——用途不同,别混。 # 核心盘 c5:和在线用户/画布数放一起看规模 # 内存盘 m120:和 connections_active 放一起对账 # 数是同一个数,问的问题不一样
它是什么:在同一个 worker 上画两条线——登记簿上的会话数(注册表 sessions_active)与实际活的连接数(握手计数 connections_active)。设计上每个会话既被登记也被计数,所以两线应重合;这张图就是把两本账摆在一起让你当场对账。
回答什么问题:连接断掉之后,注册表有没有把它销掉?换句话说——WS 会话对象有没有在内存里越积越多(注册表泄漏)?
✅ 健康长啥样:两条线完全重合,随业务节奏同涨同落;低谷期一起回到底部,没有任何「悬空」的差值。
🚨 危险长啥样:两线分叉且差值持续不收敛 = 注册表里有「尸体」(连接断了登记没销)——注册表泄漏的直接证据。动作:先查该时段核心盘 WS 错误面板的 teardown_step(Redis 故障期清理夭折的痕迹),再对照会话清理代码是否被回退。
# 主图谱口径:同一 instance 对照(两条线画一起,不做相除)
siliang_ws_sessions_active vs siliang_ws_connections_active
联动:主图谱 · WS 会话互校验 ↗ · 相关课程:WS 画布协作(teardown_step 错误源) · 泄漏判定心法
它是什么:会话「小背包」的账本:每个会话随身携带的非共享状态有多大。正常单包 KB 量级;面板同时画总量曲线(ws_total_state_bytes)和单包 P95 分布(_ws_session_state_bytes_bucket),一个是总重、一个是典型行李规格。
回答什么问题:会话对象在内存里的总开销有多大?是在正常范围内(会话数 × KB 小包),还是已经超重(会话爆了,或者有人往背包里塞了大对象)?
✅ 健康长啥样:总量 = 会话数 × KB 级单包,随在线人数起伏;单包 P95 稳定在 KB 量级不漂移。
🚨 危险长啥样:总量 > 50MB 触发告警(口径卡规则)——要么会话数爆了,要么有人往背包里塞了大对象。动作:先做除法分辨(总量÷会话数),会话数问题去核心盘在线面板查掉线堆积;单包变大则排查最近把什么对象挂上了会话。
# 主图谱口径:总重 Gauge + 单包 P95 分布,两张小图拼一张看 siliang_ws_total_state_bytes + histogram_quantile(0.95, sum(rate(siliang_ws_session_state_bytes_bucket[5m])) by (le))
联动:主图谱 · WS 会话状态字节 ↗ · 相关课程:内存全局水位(50MB 线的出处) · 在线用户与协作规模
两条线靠肉眼看重合不够精确,把差值直接画出来:差值长期为 0 是健康,差值爬上去不回来就是尸体在堆积。
# 差值 = 登记簿 − 活连接(同一 worker,1 分钟抹抖) max_over_time(siliang_ws_sessions_active[1m]) - max_over_time(siliang_ws_connections_active[1m]) # → 每条线一个 worker;差值 > 0 且持续不收敛 = 注册表泄漏 # 同步互证:清理失败有没有在发生 sum(rate(siliang_ws_errors_total[5m])) by (error_type) # → teardown_step 抬升 = Redis 抖动期清理半路夭折的痕迹
判读:差值偶发出现又收敛(心跳超时兜底正常工作)没事;差值单调爬升必须当泄漏事故处理。
50MB 是口径卡的既定告警线,照抄即可;再补一条单包 P95 异常放大的预警,能在「塞大对象」发生的当天就发现,不用等总量触线。
# 主告警:口径卡规则(50MB) - alert: WsSessionStateBytesHigh expr: max(siliang_ws_total_state_bytes) > (50 * 1024 * 1024) labels: {severity: warning} annotations: {summary: "WS 会话状态总量 > 50MB:会话爆 or 塞了大对象"} # 预警:单包 P95 突然变大(正常 KB 量级,放大即回归信号) - alert: WsSessionStatePerSessionGrown expr: histogram_quantile(0.95, sum by (le) (rate(siliang_ws_session_state_bytes_bucket[5m]))) > 10 * 1024 # 示例阈值:单包 P95 超过 10KB 再按基线调 labels: {severity: info}
告警只说「总重超了」,分辨原因只要一步除法加两个对照。
# 第 1 步:总重 ÷ 会话数 = 平均单包,KB 量级正常,几十 KB 有鬼 max(siliang_ws_total_state_bytes) / sum(siliang_ws_sessions_active{job="siliang_backend_worker"}) # 第 2a 步:若是会话数问题 → 在线面板看是不是掉线堆积(连接没被回收) sum(siliang_ws_connections_active{job="siliang_backend_worker"}) # 第 2b 步:若是单包问题 → 单包 P95 对时间戳,对上最近的发布 histogram_quantile(0.95, sum by (le) (rate(siliang_ws_session_state_bytes_bucket[5m])))
判读:平均单包仍是 KB 级 = 会话数爆炸,查掉线/心跳;平均单包变大 = 背包被塞了大对象,回查该时段代码变更。
Redis 故障期 teardown 容易半路夭折,尸体当时不一定显形。抖动恢复后主动巡检一次,比等泄漏爬大再查省事得多。
# 巡检 1:抖动期有没有清理失败的痕迹 sum by (error_type) (increase(siliang_ws_errors_total[30m])) # → teardown_step > 0:有清理被打断的现场 # 巡检 2:现在两本账还对得上吗(30 分钟后复查差值是否收敛) max_over_time(siliang_ws_sessions_active[1m]) - max_over_time(siliang_ws_connections_active[1m]) # → 差值回到 0:兜底机制把尸体收干净了;没回 0:升级处理
# 错: sessions_active(253-backend-0) vs connections_active(253-backend-3) # 对: 同一 instance 内对照 # … by (instance) 成对看
# 错: "sessions_active=800,所以有 800 个用户在线" # 对: 在线人数看 sum(siliang_ws_online_users)(按 user_id 去重)
# 错: 看到 total_state_bytes > 50MB 就重启服务 # 对: total / sessions 先分清"会话爆"还是"单包变大"
# 错: 差值 > 0 立刻 pager # 对: 差值 > 0 持续(如 30m)不收敛才升级
# 错: 直接 review 会话注册表代码 # 对: error_type="teardown_step" 时间戳 × Redis 故障时间窗 对照
# 错: 只告警 total > 50MB # 对: histogram_quantile(0.95, …_session_state_bytes_bucket) 一并盯住
参考答案:一张真不行。登记簿回答「系统以为谁在」,活连接回答「谁真的在」。人正常离开时两边一起减,两本账没差别;人异常离开(拔网线)时只有连接侧能自己减——于是「登记簿比活连接多出来的部分」恰好就是「系统以为在、其实人走了」的尸体。两本账的差值就是泄漏探测器,这正是它存在的意义。
参考答案:靠心跳超时兜底——几个心跳没人应,就认定人没了,回收连接并销登记。意外是:清理(teardown)是一串步骤,中途要动 Redis;如果 Redis 恰好抖动,清理半路夭折,登记没销掉,注册表里就留下一个永远「在住」的尸体,两本账从此分叉。
参考答案:第一步做除法:总量÷会话数=平均单包,分辨是会话数爆了还是单包变大。第二步分头查:会话数问题去核心盘在线/连接面板看是不是掉线堆积;单包问题把单包 P95 曲线对时间戳,找把大对象挂上会话的那次发布。第三步修复后盯差值与总量回归基线,确认收敛。