# 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 双路投递。