🕸 WS 会话内存:登记簿与背包(内存盘 2 图)

每个 WebSocket 会话在服务器身上记两本账:前台登记簿(注册表)和门禁计数(活连接),两本账必须重合;每个会话还背一个 KB 级小背包(私有状态),背包总重超 50MB 就拉警报。覆盖 m120 · m121 · 后端内存仪表盘「🕸 WS 会话内存」组(内存盘 2 图)。

🕸 两本账一对、一个背包一称:账本对不对得上,背包有没有超重 backend worker(一家分店) WS 会话都登记在本 worker 的注册表 前台登记簿(会话注册表) sessions_active · 登记的会话数 门禁计数(实际活连接) connections_active · 握手成功数 两线对照(同一 worker) 分叉起点:连接断了登记没销 ━ 登记簿(只进不出=尸体) ┄ 活连接(正常回落) 每个会话的小背包 非共享状态:光标 / 选中集 待发消息队列 …(KB 量级) 总量 = 会话数 × 单包大小 siliang_ws_total_state_bytes 两条超重路线:会话数爆 / 塞大对象 总量 > 50MB → 告警(口径卡规则) 总重告警线(口径卡规则) 告警线 50MB 正常:会话数 × KB 小包 超重:踩过 50MB 线 → 告警 两线为什么会分叉 ① 用户拔网线,服务器听不到「再见」 ② 正常靠心跳超时兜底回收连接 ③ 兜底前 Redis 抖 → teardown 半路夭折 ④ 登记没销 → 尸体(注册表泄漏) 互证:核心盘 WS 错误 teardown_step 高 事故现场 · 互校验口径 同一 instance 上:sessions_active − connections_active > 0 且持续不收敛 = 注册表泄漏(尸体只进不出) 处理:先查 Redis 是否抖过(teardown_step),再对照会话清理代码是否被回退;总重超重另看 m121 告警线。 一句话:登记簿=会话注册表 · 门禁=握手计数 · 背包=非共享状态;对账(m120)+ 称重(m121)就是本课两张图。

💡 一句话理解

把每个 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 触发告警(口径卡规则)

🛠 动手验证(3 步,在 Grafana 里亲手做一遍)

  1. 打开 m120 WS 会话互校验——挑一个流量正常的 worker,确认登记簿线与活连接线完全重合,一起涨一起跌。
  2. 换到流量低谷再看一次——两条线应该一起落回低位;如果一条落了另一条悬在半空,你已经亲眼看到「尸体」了。
  3. 打开 m121 WS 会话状态字节——看总量曲线离 50MB 告警线多远,再对照单包 P95 分布,算一算「会话数 × 单包」是不是能对上总账。

🧠 必知必会 看懂本课全部面板的地基

为什么有两本账
登记(注册表)与活连接(握手计数)是两个独立动作:握手成功两边一起加;正常断开两边一起减;异常断开只有连接侧自己减——这正是抓泄漏的缝隙。
# 两本账(同一 worker 对照,主图谱口径)
siliang_ws_sessions_active     # 登记簿:注册表里的会话数
siliang_ws_connections_active  # 门禁:实际活的连接数
尸体 = 注册表泄漏
连接断了但登记没销的会话。成因:teardown 清理链路半路夭折(常见于 Redis 抖动期)。危害:内存被占、广播发给空气。
# 判定:同一 instance 两线相减,持续 > 0 不收敛 = 尸体
# 互证:核心盘 WS 错误面板 teardown_step 是否升高
sum(rate(siliang_ws_errors_total[5m])) by (error_type)
对照口径:同一 instance
注册表是每个 worker 各自一份的,互校验必须拿同一个 worker 的两条线比,跨 worker 比没有意义。
# 错: 把 worker-A 的登记簿和 worker-B 的连接数比
# 对: by (instance) 拆开,每对线各自对照
siliang_ws_sessions_active by (instance)   # vs connections_active by (instance)
会话背包
每个会话随身携带的非共享状态:光标、选中集、待发消息队列。单包 KB 量级;不该塞图片/大 JSON——背包不是仓库。
# 单包分布(P95)与总重各看各的
histogram_quantile(0.95, sum(rate(siliang_ws_session_state_bytes_bucket[5m])) by (le))
siliang_ws_total_state_bytes            # 总重 Gauge,直读
总重公式与两条爆表路
总量 = 会话数 × 单包大小。超 50MB 只有两种可能:会话数爆了(在线人数事故),或有人往背包塞了大对象(代码回归)。
# 超重先做除法,分辨是哪条路
# 总重 60MB ÷ 会话 2 万 ≈ 3KB/包 → 会话数问题
# 总重 60MB ÷ 会话 2 千 ≈ 30KB/包 → 背包被塞大对象
50MB 告警线
来自内存盘口径卡的核心告警之一(另两条:deriv(RSS[1h])>5MB/h、沙箱跟踪>0)。告警触发先分母分子分别看。
# 口径卡核心告警(WS 相关这条)
siliang_ws_total_state_bytes > 50MB   # → 会话爆 or 塞了大对象
规模口径 vs 内存口径
同一个 sessions_active,核心盘趋势图拿它看「在线规模」,内存盘拿它当「登记簿」跟连接数对账——用途不同,别混。
# 核心盘 c5:和在线用户/画布数放一起看规模
# 内存盘 m120:和 connections_active 放一起对账
# 数是同一个数,问的问题不一样

📋 逐面板精讲 2 张图一张不落

🔵 WS 会话互校验(差值=注册表泄漏)

它是什么:在同一个 worker 上画两条线——登记簿上的会话数(注册表 sessions_active)与实际活的连接数(握手计数 connections_active)。设计上每个会话既被登记也被计数,所以两线应重合;这张图就是把两本账摆在一起让你当场对账。

回答什么问题:连接断掉之后,注册表有没有把它销掉?换句话说——WS 会话对象有没有在内存里越积越多(注册表泄漏)?

✅ 健康长啥样:两条线完全重合,随业务节奏同涨同落;低谷期一起回到底部,没有任何「悬空」的差值。

🚨 危险长啥样:两线分叉且差值持续不收敛 = 注册表里有「尸体」(连接断了登记没销)——注册表泄漏的直接证据。动作:先查该时段核心盘 WS 错误面板的 teardown_step(Redis 故障期清理夭折的痕迹),再对照会话清理代码是否被回退。

# 主图谱口径:同一 instance 对照(两条线画一起,不做相除)
siliang_ws_sessions_active vs siliang_ws_connections_active

联动:主图谱 · WS 会话互校验 ↗ · 相关课程:WS 画布协作(teardown_step 错误源) · 泄漏判定心法

🔵 WS 会话状态字节

它是什么:会话「小背包」的账本:每个会话随身携带的非共享状态有多大。正常单包 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 线的出处) · 在线用户与协作规模

🏭 生产实战 real world

场景 1 · 怀疑注册表泄漏:把「差值」算出来盯住

两条线靠肉眼看重合不够精确,把差值直接画出来:差值长期为 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 抖动期清理半路夭折的痕迹

判读:差值偶发出现又收敛(心跳超时兜底正常工作)没事;差值单调爬升必须当泄漏事故处理。

场景 2 · 给背包总重配 50MB 告警

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}

场景 3 · 50MB 告警响了:三分钟分辨是哪种爆炸

告警只说「总重超了」,分辨原因只要一步除法加两个对照。

# 第 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 级 = 会话数爆炸,查掉线/心跳;平均单包变大 = 背包被塞了大对象,回查该时段代码变更。

场景 4 · Redis 抖动后的半小时巡检

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:升级处理

⚠️ 常见坑 pitfalls

坑 1 · 跨 worker 对账 — 拿 worker-A 的登记簿和 worker-B 的连接数比,发现「分叉」虚惊一场。原因:注册表是每个 worker 各一份的,跨实例没有可比性。正解:互校验必须 by (instance),只比同一个 worker 的两条线。
# 错: sessions_active(253-backend-0) vs connections_active(253-backend-3)
# 对: 同一 instance 内对照   # … by (instance) 成对看
坑 2 · 把会话数当人数 — 拿 m120 的会话数向老板汇报「在线人数」。原因:一人多页签=多会话,会话口径不去重(去重的人数在核心盘 c1,口径完全不同)。正解:汇报人数用核心盘在线用户面板;m120 只用于对账。
# 错: "sessions_active=800,所以有 800 个用户在线"
# 对: 在线人数看 sum(siliang_ws_online_users)(按 user_id 去重)
坑 3 · 告警响了只会盯着总量 — 50MB 告警触发后不知道从哪下手。原因:总量是乘法的结果,只看总量无法分辨因子。正解:总量÷会话数做除法,两个因子分别对照各自的基线。
# 错: 看到 total_state_bytes > 50MB 就重启服务
# 对: total / sessions 先分清"会话爆"还是"单包变大"
坑 4 · 以为连接一断登记立即销 — 看到短暂的小差值就报警。原因:异常断开靠心跳超时兜底,兜底本身有窗口期,短差值会自动收敛。正解:只把「持续不收敛」的差值当泄漏,偶发收敛的差值是兜底在工作。
# 错: 差值 > 0 立刻 pager
# 对: 差值 > 0 持续(如 30m)不收敛才升级
坑 5 · 忘了 teardown 依赖 Redis — 只盯会话代码找尸体成因,忽略外部依赖。原因:清理链路半路要动 Redis,Redis 抖动期的清理失败是尸体最常见来源。正解:查尸体先查 teardown_step 错误的时间戳,对上 Redis 抖动再说代码。
# 错: 直接 review 会话注册表代码
# 对: error_type="teardown_step" 时间戳 × Redis 故障时间窗 对照
坑 6 · 单包分布不看只看总量 — 只画 total_state_bytes 一条线,单包变大很久才发现。原因:会话数少时,大单包撑不起总量、绕过 50MB 线,但它是代码回归的最早信号。正解:总量和单包 P95 两条一起看。
# 错: 只告警 total > 50MB
# 对: histogram_quantile(0.95, …_session_state_bytes_bucket) 一并盯住

🎓 费曼自测 合上书,能讲出来才算懂

Q1 · 为什么 WS 会话要同时有「注册表」和「活连接」两本账?一张不行吗?

参考答案:一张真不行。登记簿回答「系统以为谁在」,活连接回答「谁真的在」。人正常离开时两边一起减,两本账没差别;人异常离开(拔网线)时只有连接侧能自己减——于是「登记簿比活连接多出来的部分」恰好就是「系统以为在、其实人走了」的尸体。两本账的差值就是泄漏探测器,这正是它存在的意义。

Q2 · 用户直接拔网线后,服务器靠什么发现人走了?中间会发生什么意外?

参考答案:靠心跳超时兜底——几个心跳没人应,就认定人没了,回收连接并销登记。意外是:清理(teardown)是一串步骤,中途要动 Redis;如果 Redis 恰好抖动,清理半路夭折,登记没销掉,注册表里就留下一个永远「在住」的尸体,两本账从此分叉。

Q3 · 会话状态总量 > 50MB 触发告警,说说你的排查三步。

参考答案:第一步做除法:总量÷会话数=平均单包,分辨是会话数爆了还是单包变大。第二步分头查:会话数问题去核心盘在线/连接面板看是不是掉线堆积;单包问题把单包 P95 曲线对时间戳,找把大对象挂上会话的那次发布。第三步修复后盯差值与总量回归基线,确认收敛。

← 上一课:🔌 连接池台账:backend 视角 📚 课程目录 下一课:🛡 防护与缓存:护栏拦了什么 →