Files
im_server/docs/IM-原生Cluster与RedisFanout职责边界.md
T

257 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# IM 原生 Cluster 与 Redis Fanout 职责边界
## 1. 背景
IM Server 当前同时存在两套跨 Pod 能力:
- Tinode 原生 Cluster:以 Topic master 为中心,负责把 Topic 请求路由到唯一 master 节点,由 master 统一处理 Topic 状态机。
- Redis Fanout:以 Redis Pub/Sub 为跨 Pod 实时总线,负责把服务端生成的实时事件广播到其他 Pod,再由各 Pod 投递给本机 WebSocket session。
测试环境目前是多 Pod standalone 部署,日志表现为 `Cluster: running as a standalone server.`,因此当前实际跨 Pod 主链路是 Redis Fanout。目标架构需要启用原生 Cluster,把 Topic 热路径、强顺序状态机和实时通话状态迁回 Topic master;Redis Fanout 保留为大数据量、缓存、补偿和用户级控制能力。
本文定义两套机制的职责边界,避免后续同时启用后出现重复投递、重复状态变更或状态源不一致。
## 2. 总原则
1. 同一类 Topic 事件只能有一个实时主链路。
2. 原生 Cluster 启用时,Topic 状态机相关事件优先由 Cluster 处理。
3. Redis Fanout 不作为 Topic master 的替代品;它是跨 Pod 投递、通知和 session control 总线。
4. 关键业务状态必须以 MySQL、Redis 原子状态或 Kafka 顺序消费作为权威,WebSocket fanout 只负责实时通知。
5. 可丢的临时状态可以只走 fanout;不可丢的状态必须有历史补拉或条件更新兜底。
6. 能利用原生 Cluster 内存态、高性能、强一致和强顺序的 Topic 热路径,应优先交给原生 Cluster。
7. 数据量大、需要广播、缓存、聚合、补偿或跨业务模块共享的数据,应优先交给 Redis/MySQL/Kafka。
## 3. 分工原则
两套机制不是互斥关系,而是按数据特征分工:
```text
强状态、强顺序、小范围热路径 -> 原生 Cluster
大数据量、跨节点广播、缓存、补偿 -> Redis / MySQL / Kafka
```
### 3.1 优先使用原生 Cluster 的场景
以下场景应尽量使用原生 Cluster,发挥 Topic master 的内存态和串行状态机能力:
- 单个 Topic 内的订阅、离开、权限变更和在线 session 状态。
- 单聊/群聊 Topic 内的 `pub`、`read`、`recv`、`get/set meta`。
- 通话实时状态机:`ringing`、`accept`、`hang-up`、`offer/answer/ice-candidate`。
- 对顺序敏感的 Topic 内临时事件,例如正在输入、同 Topic 内 presence 派发。
- 需要立即反馈客户端、且依赖 Topic 当前内存状态的操作。
这些能力的共同特点是:
- 数据范围集中在单个 Topic。
- 需要强顺序或强一致。
- 更适合由唯一 Topic master 串行处理。
- 不适合广播给所有 Pod 后各自判断。
### 3.2 优先使用 Redis / MySQL / Kafka 的场景
以下场景应优先使用 Redis、MySQL 或 Kafka,不强行塞进原生 Cluster:
- 全局用户在线状态查询、好友列表在线状态展示。
- 群成员在线节点集合、大群在线成员筛选。
- 大群 fanout、热点消息缓存、未读聚合、离线补偿。
- 用户级 session control:踢端、全端下线、同平台挤旧端、密码重置清退。
- 限流、短状态、幂等、分布式去重。
- 历史消息、通话记录、审计、后台统计。
- Push、文件安全、后台管理命令等异步任务。
这些能力的共同特点是:
- 数据量大或跨 Topic。
- 需要被 HTTP、Kafka、后台任务等多入口共享。
- 可以最终一致,或可以通过历史补拉/条件更新补偿。
- 不适合放在某个 Topic master 的内存里长期维护。
## 4. 目标启用模式
目标模式为:
```text
原生 Cluster Topic master + Redis 大数据量支撑 + MySQL/Kafka 权威状态
```
目标分工:
- 原生 Cluster 负责单个 Topic 内的实时请求路由、内存状态机、强顺序处理和低延迟下发。
- Redis 负责全局在线状态、大群在线成员集合、热点缓存、大数据量 fanout、限流、幂等和 session control。
- MySQL 负责最终历史、通话记录、关系状态和可补偿查询。
- Kafka 负责后台异步任务、Push、审计、文件安全和跨服务事件。
启用原生 Cluster 后的核心原则:
```text
Topic 内强状态、强顺序、小范围热路径,必须优先走原生 Cluster
跨 Topic、大数据量、可最终一致或需补偿的数据,继续走 Redis/MySQL/Kafka
```
当前 standalone + Redis Fanout 是过渡状态。切换到原生 Cluster 前,需要先完成部署形态、职责收敛、重复链路关闭和压测验收。
## 5. 职责边界表
| 能力 | 当前 standalone + Redis Fanout 过渡模式 | 目标原生 Cluster 模式 | 说明 |
| --- | --- | --- | --- |
| Topic master 路由 | 不启用 | Cluster 负责 | 原生 Cluster 通过一致性哈希确定 Topic master。 |
| `{sub}` 订阅 | 本 Pod Hub/Topic 处理,跨 Pod 投递依赖 Redis | Cluster `ProxyReqJoin` | 启 Cluster 后不要再用 Redis 模拟订阅路由。 |
| `{leave}` 离开 | 本 Pod 处理 | Cluster `ProxyReqLeave` | 与订阅同源。 |
| `{pub}` 消息写入 | 本 Pod 处理,生成 data 后 Redis fanout | Cluster `ProxyReqBroadcast` 到 Topic master | 启 Cluster 后 Topic data 不应再额外 Redis topic fanout。 |
| `{get/set}` meta | 本 Pod 处理 | Cluster `ProxyReqMeta` | 强依赖 Topic 状态时应由 master 处理。 |
| `read/recv` 回执 | Redis topic/user fanout 下发结果 | Cluster 路由 note 到 master | 启 Cluster 后回执状态由 master 生成,避免双份 info。 |
| 通话 `ringing/accept/hang-up` | Redis `call_event` fanout + DB 条件更新兜底 | Cluster `ProxyReqCall` | 启 Cluster 后 Redis `call_event` 必须跳过。 |
| `offer/answer/ice-candidate` | 当前可由现有通话状态机处理;声网模式下不走 SDP/ICE | Cluster 由 Topic master 转发 | 首版声网主要关注 accept/hang-up。 |
| Topic 内 data/info/pres 下发 | Redis realtime fanout | Cluster proxy response | 两者不能同时作为主链路。 |
| 用户在线状态查询 | Redis presence | 可继续 Redis presence | 即使启 Cluster,也建议 Redis 保持全局在线查询缓存。 |
| 群在线成员缓存 | Redis topic member presence | 可继续 Redis 缓存 | 用于大群本节点在线成员查找和展示。 |
| 踢端/全端下线 | Redis session control | Redis session control | 用户级控制不属于单个 Topic,建议继续 Redis。 |
| 密码重置清退 | Redis session control | Redis session control | 与 Cluster 不冲突。 |
| 同平台新登录挤旧端 | 本 Pod + Redis platform evict | 本 Pod + Redis platform evict | 继续用 Redis 保证跨 Pod 生效。 |
| 好友/群业务事件 | 本地投递 + Redis user/topic fanout | 可继续 Redis user fanout | 业务事件通常不是 Topic 状态机请求。 |
| 系统消息通知 | 本地投递 + Redis fanout | 视消息写入入口决定 | 如果写入会话 Topic,应避免和 Cluster topic 下发重复。 |
| 群消息 seq/热消息缓存 | Redis / MySQL | 可继续 Redis / MySQL | 大数据量缓存和历史权威不依赖原生 Cluster。 |
| Kafka 后台事件 | Kafka 负责异步持久化、Push、审计 | Kafka 继续负责 | Kafka 不做实时 WS fanout 主链路。 |
## 6. Redis Fanout 主链路范围
在当前 standalone 过渡模式下,Redis Fanout 是跨 Pod 实时投递主链路。启用原生 Cluster 后,Redis Fanout 需要收敛为配套能力,负责:
1. `message/user`:用户维度的系统事件、业务事件、me presence 下发。
2. `session_control/user|device|platform`:踢端、清退、同平台挤旧端。
3. presence 缓存:用户在线状态、节点 session 信息、群成员在线节点信息。
4. 大群在线成员筛选、热点消息缓存、未读聚合和历史补偿辅助。
5. 限流、短状态、幂等、分布式去重。
仅在 standalone 过渡模式下,Redis Fanout 额外负责:
1. `message/topic`:Topic 维度的 `data`、`info`、`pres` 跨 Pod下发。
2. `call_event/topic`:通话 `ringing`、`accept`、`hang-up` 进入 Topic 状态机。
这些 Topic 状态机相关 fanout 在原生 Cluster 启用后必须关闭或跳过。
Redis Fanout 不负责:
- 作为消息历史权威。
- 作为通话最终状态权威。
- 保证同一 Topic 的全局严格顺序。
- 替代 MySQL/Kafka 的审计、补偿和持久化。
## 7. 原生 Cluster 主链路范围
启用原生 Cluster 后,它应负责:
1. Topic owner/master 分配。
2. Topic 请求路由:`sub`、`leave`、`pub`、`get/set`、`note`。
3. Topic 内状态机:订阅、权限、回执、通话、presence 派发。
4. Topic master 到 proxy session 的响应和广播。
5. 节点故障后的 proxy session 清理与订阅失效通知。
原生 Cluster 不建议负责:
- 用户级 session control。
- 全局在线状态查询缓存。
- 业务后台事件可靠投递。
- Kafka 异步消费、Push、审计。
- Redis 热消息缓存、限流、短状态。
- 大群全量成员 fanout、全局未读聚合、历史数据扫描等大数据量任务。
## 8. 双开时必须避免的重复链路
如果 `globals.cluster != nil`,以下 Redis Fanout 应跳过或降级:
| Redis 入口 | Cluster 启用时处理 |
| --- | --- |
| `publishRealtimeTopicMessage` | 跳过。Topic data/info/pres 由 Cluster proxy response 下发。 |
| `publishRealtimeCallEvent` | 跳过。通话 note 由 `ProxyReqCall` 路由到 Topic master。 |
| 群消息在线投递 fanout | 需要专项确认;如果同一 data 已由 Cluster 下发,不得再广播。 |
| 会话 Topic 系统消息 fanout | 如果消息进入 Topic 状态机,应避免再次 topic fanout。 |
以下 Redis Fanout 可继续保留:
| Redis 入口 | 保留原因 |
| --- | --- |
| `publishRealtimeEvictUser` | 用户级清退,不归属单一 Topic。 |
| `publishRealtimeEvictDevice` | 设备级清退,不归属单一 Topic。 |
| `publishRealtimeEvictUserPlatform` | 同平台挤旧端,需要跨 Pod 控制 session。 |
| 用户 presence 缓存 | 全局在线查询和好友列表展示仍需要。 |
| 业务 user event | 多数业务事件不是 Topic 状态机请求。 |
## 9. 状态权威与顺序规则
### 9.1 消息顺序
- 权威顺序来自 `topic + seq`。
- Redis Fanout 到达乱序时,客户端必须按 seq 去重和重排。
- 服务端历史读取按 seq 返回,作为重连补偿来源。
### 9.2 通话状态
通话状态必须使用条件更新保证单向流转:
```text
started -> accepted -> finished
started -> missed
started -> declined
accepted -> disconnected
```
规则:
- `accept` 只能从 `started` 写入 `accepted`。
- `missed` 只能从 `started` 且 `accepted_at IS NULL` 写入。
- `finished` 只能结束未结束通话。
- 已 `accepted` 的通话不得被 `missed` 或 `declined` 覆盖。
- WebSocket 下发失败不能改变最终状态,客户端可通过通话记录或会话消息补偿。
### 9.3 Session Control
session control 以 Redis 短状态和 token/auth_version 为兜底:
- Redis 实时踢端用于立即关闭在线 WS。
- token/auth_version 用于防止旧 token 重新登录。
- stale session-control fanout 必须丢弃,避免旧事件踢掉新 session。
## 10. 启用原生 Cluster 的前置条件
启用原生 Cluster 前必须满足:
1. K8s 部署从普通 Deployment 改为适合稳定网络身份的形态,例如 StatefulSet 或稳定 Pod DNS。
2. 每个 Pod 配置唯一且稳定的 `cluster_self`。
3. 配置完整 `cluster_config.nodes`,包括节点名和可达地址。
4. 开放并验证 gRPC/cluster 节点间通信。
5. 明确 Redis Fanout 哪些保留、哪些关闭。
6. 压测 `sub/pub/read/recv/call/group` 跨节点链路。
7. 验证节点重启、扩缩容、Topic rehash、proxy subscription 失效和客户端重订阅。
8. 验证不会重复投递 data/info/pres/call event。
## 11. 观测与验收
必须保留以下观测指标:
- Redis fanout publish/receive/deliver 计数。
- Redis fanout `skip_native_cluster` 计数。
- Redis fanout `no_local_topic`、`no_local_session`、`hub_full` 计数。
- 通话状态转换计数:started、accepted、finished、missed、declined。
- DB 条件更新失败计数,用于识别迟到事件和乱序事件。
- Cluster 模式下 proxy queue、TopicMaster RPC 失败、cluster unreachable、rehash 次数。
验收用例至少覆盖:
1. A、B 连接到不同 Pod,P2P 消息实时到达。
2. 接收方未订阅当前会话,仅在线 me,能收到未读提醒或补偿。
3. 通话 accept/hang-up 跨 Pod 到达,且不会被 missed 覆盖。
4. 多端登录同一账号,所有端收到业务事件。
5. 密码重置或后台踢端能清退所有 Pod 上的 session。
6. 大群在线成员分布在多 Pod,消息只投递在线成员,离线靠 seq 补拉。
7. Redis 短暂不可用时,关键状态不写坏,客户端可补偿。
## 12. 当前结论
当前阶段职责划分如下:
- 当前测试环境仍是 standalone + Redis Fanout,这是过渡状态。
- 目标是启用原生 Cluster,让 Topic master 承担内存高性能、强一致、强顺序的实时状态机。
- Redis 不退出架构,而是转为大数据量、缓存、补偿、全局 presence、session control 和异步支撑能力。
- 启用原生 Cluster 时,Topic 状态机相关的 Redis topic/call fanout 必须关闭或降级,避免 data/info/pres/call event 双路投递。