Files
im_server/docs/IM-服务端架构与新功能开发指南.md
T

24 KiB
Raw Blame History

IM 服务端架构与新功能开发指南

文档状态:按 2026-09-08 代码实现整理
适用项目:im_server
文档目标:让新接手的开发者能够快速判断一个功能应放在哪里、如何调用、如何存储、如何做多节点一致性,以及如何验收。

1. 系统定位

im_server 是基于 Tinode 扩展的多租户 IM 业务服务,同时承担两类职责:

  1. IM 实时核心:WebSocket 连接、登录会话、Topic 订阅、消息路由、在线状态、音视频信令。
  2. C 端产品业务:注册、安全、好友、群治理、收藏、文件、工作台、会议、日报、举报等 HTTP API。

系统不是传统的 controller -> service -> repository 三层单体。它的核心是“Topic Actor 实时模型 + HTTP 业务扩展层 + Store 统一持久化边界”。开发新功能时必须先判断它属于实时 Topic 链路,还是 HTTP 业务链路,或者同时涉及两者。

2. 系统边界

2.1 服务与数据库归属

服务 直连数据库 负责数据
IM Server 租户业务库,默认库为 tinode 用户、认证、Topic、订阅、私聊/群聊消息、群业务、好友关系、文件元数据、C 端业务数据
Admin / HotGo hotgo 租户注册表、系统配置、字典、后台账号、菜单权限、后台命令 Outbox、管理端摘要数据

原则:服务与库一一对应,Admin 不直连租户库,IM 不直连 hotgo。跨系统写操作通过 Kafka 命令/事件,必须同步返回的少量查询通过受控的内部 HTTP API。

2.2 租户隔离

目标模型是“一个租户一个业务库”:

  • tenant_id 是请求上下文和路由键,不要因为租户库内没有 tenant_id 列就在代码中丢掉租户上下文。
  • 所有业务 Store 调用必须显式传递 types.TenantID。
  • store.ForTenant 和 tenantBusinessAdapter 负责验证租户与选择租户适配器。
  • MySQL 的 TenantRoutedAdapter.TenantAdapterFor 负责将租户路由到对应连接池。
  • 任何无租户上下文的业务查询都应失败关闭,不允许回退到旧的无租户查询。

3. 技术栈

领域 当前实现 用途
语言 Go 1.26 服务端主体
实时协议 Tinode JSON Protocol、WebSocket、Long Polling、gRPC 长连接、Topic 订阅、消息和集群通信
HTTP net/http + http.ServeMux C 端 REST API、文件 API、内部 API
数据库 MySQL + database/sql + sqlx 租户业务最终状态
热状态/缓存 Redis 7,go-redis/v9 在线状态、Seq、幂等、群热消息、对象缓存、关系 Set、限流、跨节点 fanout
异步事件 Kafka,segmentio/kafka-go 跨系统命令、消息异步持久化、审计、缓存失效、失败重试与死信
文件 阿里云 OSS,保留 FS/S3 适配器 文件对象存储,业务库只存元数据和引用
文件安全 阿里云内容安全 上传完成后的异步审核
离线推送 JPush,同时保留 FCM/TNPG 插件能力 离线提醒;客户端重连后按 Topic seq 补偿
音视频 Tinode 通话信令 + 声网 RTC Token 呼叫邀请/接听/挂断与 RTC 加入
认证 basic/token/code/rest 可插拔认证器 密码、Token、验证码和外部认证
可观测性 Go log、expvar、Prometheus exporter、pprof 日志、运行指标和性能诊断

4. 运行时架构

Client
  |-- WebSocket /v0/channels
  |      -> Session -> Hub -> Topic actor -> Store/Redis
  |                                 |-> local sessions
  |                                 `-> Redis fanout -> other IM nodes -> WebSocket
  |
  |-- HTTP /v1/im/*
  |      -> protocol middleware -> bearer/API key/tenant
  |      -> handler -> Store mapper -> tenant adapter -> tenant MySQL
  |                    |-> Redis cache/short state
  |                    `-> business event -> online WebSocket/Kafka
  |
  `-- Upload/Download
         -> media handler -> OSS
         -> file metadata/link -> tenant MySQL
         `-> file security task -> Kafka -> Aliyun security

Admin / HotGo
  |-- Kafka admin command/config event -> IM consumer -> tenant business mutation
  `-- signed internal HTTP <-------> IM internal read API

4.1 启动顺序

server/main.go 是服务入口,主要顺序为:

  1. 读取 JSON with comments 格式的 meetingim.conf/tinode.conf。
  2. 初始化日志、expvar、pprof 和集群 worker ID。
  3. 打开 Store,校验数据库版本与租户适配能力,同时初始化 Redis runtime。
  4. 初始化 Kafka、IM 内部 API 和 Admin 内部配置 API 客户端。
  5. 初始化认证器、验证器、文件处理器、Push 和音视频。
  6. 创建 SessionStore 和 Hub,启动 Redis 群消息 fanout 订阅。
  7. 启动 Kafka Admin Command、Config Event、Cache Invalidate、Message Persist 和 Dead Letter 消费者。
  8. 启动集群、gRPC、HTTP/WebSocket 路由和优雅退出。

任何新增的基础组件都应明确放在这个顺序中,初始化失败时应快速终止启动,不要让必需依赖在不可用状态下继续运行。

5. 核心业务模型

5.1 Session

Session 表示一条客户端连接,保存租户、用户、设备、认证等级、已订阅 Topic 和输出队列。一个用户可以同时有多个 Session。

登录和断线时需同步 Redis 在线状态;心跳用于刷新 presence TTL。不应仅依赖本进程 Session 判断全局在线状态。

5.2 Hub

Hub 是 Topic 路由中心,核心通道包括:

通道 用途
join 加载/创建 Topic,将 Session 附加到 Topic
routeCli 客户端消息路由到 Topic
routeSrv 服务端业务消息路由到 Topic/Session
meta 未订阅 Topic 的元数据查询
unreg 卸载或删除 Topic
userStatus 用户停用/恢复等状态变更

Topic 索引不是纯字符串,而是 types.TopicKey{TenantID, Topic},以防止不同租户的同名 Topic 串数据。

5.3 Topic Actor

Topic 是会话和消息处理单元,通过 channel 串行处理加入、退出、客户端消息、服务端消息和元数据操作。主要分类包括:

Topic 含义
me 用户主 Topic,会话列表和用户级 presence
fnd 搜索/发现 Topic
usr... 客户端可见的 P2P 对端标识,入库前需归一化为真实 P2P Topic
grp... 群聊 Topic
chn... 频道 Topic
slf... 个人收藏/保存消息 Topic
sys... 系统通知 Topic

涉及消息发送、已读、撤回、编辑、通话信令、在线投递时,优先复用 Topic/Session 链路,不要另造一套无状态 HTTP 投递逻辑。

6. 四类标准调用链路

6.1 WebSocket 实时消息

Client packet
  -> hdl_websock.go 解码
  -> Session 认证/租户上下文
  -> Hub.join / Hub.routeCli
  -> Topic 权限和业务校验
  -> Redis 原子 Seq + client_mid 幂等
  -> 本地 Session 实时投递
  -> Redis Pub/Sub 跨节点 fanout
  -> Kafka persist 异步落 MySQL

群消息使用 im_group_messages,私聊仍使用私聊消息表。SeqId 全链路必须使用 int64/BIGINT,群 Topic seq 由 Redis 原子递增,数据库只作基线与最终持久化。

群热消息可在 Redis 保留 24 小时左右,历史补偿最终以 MySQL 为准。离线用户只接收 Push 提醒,重连后按 Topic seq 拉取缺口。

6.2 C 端 HTTP 业务请求

/v1/im/*
  -> withIMHTTPProtocol
  -> API key + X-IM-Schema + X-Request-ID
  -> Bearer token -> principal{tenant_id, uid}
  -> 写请求 Idempotency-Key 中间件
  -> serveIM... handler
  -> store.<Domain> mapper
  -> TenantBusinessAdapter
  -> tenant MySQL / Redis
  -> writeIMSuccess / writeIMError

所有已认证的 POST/PUT/PATCH/DELETE 默认要求 Idempotency-Key,幂等结果保留 24 小时。同一 key 使用不同请求体必须返回冲突,已完成请求应重放原响应。

HTTP 统一响应包含 code/data/reason/message/details/request_id/server_time。业务错误应返回稳定 reason,不应将 SQL 或内部异常直接暴露给客户端。

6.3 Admin 后台写操作

Admin API
  -> hotgo 本地权限校验
  -> 同事务写 command + outbox
  -> Kafka im.admin.command
  -> IM consumer 校验 tenant/event/actor
  -> event_id + consumer_group 幂等抢占
  -> 租户库业务变更
  -> 缓存失效 + Topic/Session 同步
  -> Kafka im.admin.command.result
  -> Admin 更新命令结果

不可重试的参数、权限、版本冲突应直接返回稳定失败原因。临时故障按退避策略重试,超过上限写 im.dead.letter 和租户失败表。人工重放必须生成新 event_id,并使用 parent_event_id 串起审计链。

6.4 文件链路

Client request upload
  -> IM 创建文件元数据
  -> OSS 上传/回调完成
  -> 租户库更新文件状态
  -> 绑定消息/群文件/业务引用
  -> Kafka im.file.security
  -> 阿里云安全检测
  -> 更新审核状态并通知用户

文件字节流不经 Kafka,Kafka 只传递文件 ID、OSS 对象信息和审核任务。任何业务文件都必须建立引用记录,否则垃圾回收可能将它当作孤儿文件删除。

7. 数据分层与一致性

7.1 进程内缓存

适合低频变更、全局或租户级、可短时间最终一致的数据:

  • 租户基本配置和状态。
  • 注册短信开关。
  • 字典和第三方配置(OSS、文件安全、短信、JPush、Agora)。

更新来源为定时刷新和 Kafka im.config.event/im.cache.invalidate。进程内缓存不应与 Redis 重复缓存同一份高频业务对象。

7.2 Redis

Redis 用于跨节点共享且高频的数据:

  • 用户在线状态、Session 和群成员节点索引。
  • 群消息原子 SeqId、client_mid 唯一占位和状态。
  • 24 小时左右的群热消息。
  • 用户、Topic、Subscription、群设置、群成员、成员分页快照和未读数缓存。
  • 好友/黑名单 Set。
  • 验证码、Action Token、临时占位、限流等 TTL 状态。
  • 群消息 Redis Pub/Sub 跨节点实时 fanout。

Redis key 必须包含统一前缀和租户维度,不允许跨租户共用业务 key。写 MySQL 成功后必须失效或刷新对应 Redis 数据,多节点通过 Kafka 失效事件收敛。

7.3 MySQL

MySQL 是最终业务状态、历史查询和审计依据。以下数据不得仅存 Redis:

  • 用户、凭据、好友、黑名单、Topic、订阅和群治理最终状态。
  • 私聊/群聊历史消息、撤回和编辑版本。
  • 文件元数据、业务引用和安全审核结果。
  • HTTP 幂等结果、Kafka 消费幂等、死信和审计数据。

7.4 一致性顺序

普通业务写的推荐顺序:

  1. 校验租户、身份、权限、幂等键和业务版本。
  2. 在同一事务中更新 MySQL 业务状态及 outbox(需要可靠事件时)。
  3. 提交事务。
  4. 失效本节点 Redis/进程内缓存。
  5. 投递 Kafka 失效或业务事件,通知其他节点和客户端。

如果业务要求“先实时到达,再异步落库”,则必须先完成 Redis 幂等占位和 seq 分配,投递 Kafka 持久化事件,并为失败准备重试和死信补偿。

8. Kafka 责任边界

Topic 主要用途
im.admin.command Admin 到 IM 的业务写命令
im.admin.command.result IM 回传命令结果
im.config.event 租户、字典、第三方配置变更
im.business.event 好友、群、会议、工作台等 C 端业务事件
im.message.persist 私聊消息异步落库
im.group.message.persist 群聊消息异步落库
im.message.push 离线 Push 任务,属增强链路
im.file.security 文件安全检测
im.cache.invalidate 多节点缓存失效
im.audit.event 跨系统审计
im.dead.letter 超过重试上限的事件

Kafka 不用于 WebSocket 建连、验证码、在线状态、实时群消息 fanout、OSS 字节流或必须当场返回结果的查询。

统一 Envelope 至少包含:event_id、trace_id、tenant_id/tenant_code、source_service、target_service、event_type、biz_type、biz_id、idempotency_key、version、occurred_at、actor、payload。

9. 代码组织

路径 职责 新功能常见修改
server/main.go 进程入口、组件初始化、HTTP 路由注册 新增路由或新基础组件启动
server/session.go 连接、登录、请求分发、设备和 Session 状态 账号登录或连接级功能
server/hub.go Topic 加载与全局消息路由 路由、Topic 生命周期
server/topic.go / init_topic.go Topic actor、权限、订阅和消息处理 消息、群权限、Topic 内实时行为
server/datamodel.go WebSocket 客户端/服务端协议结构 协议字段或服务端推送类型
server/im_http.go C 端产品 HTTP API 的路由处理与通用约定 大部分 C 端业务入口
server/internal_api.go IM 向 Admin 提供的签名内部查询 API 后台需要同步读取租户数据
server/admin_internal_config.go IM 调用 Admin 内部配置 API IM 需要 hotgo 配置兜底查询
server/kafka_*.go Kafka runtime、生产、消费、重试、死信和各业务 handler 跨系统或异步事件
server/im_business_events.go 业务事件持久化与在线投递 需主动推给 C 端的状态变更
server/redis_fanout.go 群消息跨节点实时投递 群大并发/未加载 Topic 投递
server/store/types/ 业务实体、值对象、查询参数和稳定错误 先定义领域数据结构
server/store/store.go Store mapper、参数验证、租户边界、缓存编排 对 handler 提供统一持久化 API
server/store/infrastructure.go HTTP 幂等、业务事件、Kafka 幂等/死信 Store 可靠基础链路
server/store/group_message_runtime.go Redis seq、唯一 key、热消息、presence、缓存和限流 Redis 新 key 或新的 TTL 状态
server/db/adapter.go 持久化能力接口 新增 Store 数据能力
server/db/mysql/tenant_adapter.go MySQL 租户业务 SQL 实现 业务 CRUD/事务
server/db/mysql/*_schema.go 扩展业务表建表/升级逻辑 新表、列、索引
server/media/ FS/OSS/S3 文件处理插件 文件存储能力
server/filesafety/ 文件安全适配器 文件检测能力
server/push/ 离线 Push 插件 厂商 Push 接入
server/auth/ / server/validate/ 认证和凭据验证插件 登录或验证方式
server/*_test.go 单元与链路测试 handler、Topic、Kafka、Redis 逻辑测试
server/db/mysql/*_test.go MySQL schema 和 SQL 回归 数据库变更测试

10. 新功能放置决策

问题 选择
是否是客户端建连、订阅、发消息或通话实时信令? WebSocket + Session/Hub/Topic
是否是 C 端业务 CRUD、列表、搜索、设置或审批? /v1/im/* HTTP + Store
是否是 Admin 对租户业务的写操作? Admin 本地 command/outbox + Kafka + IM consumer
Admin 是否必须当场读取租户详情? IM 受控内部 GET API
IM 是否需要读取 hotgo 的低频配置? 进程内缓存 + Kafka 刷新 + Admin 内部 API 兜底
是否需要跨节点高频共享、TTL 或原子计数? Redis
是否需要最终可追溯的业务状态? 租户 MySQL
是否跨系统、可异步、需重试/削峰/审计? Kafka
是否为在线群消息实时扩散? Redis Pub/Sub + WS,不用 Kafka 做主链路
是否为文件本体? OSS;MySQL 只存元数据/引用

11. 新功能标准开发流程

11.1 需求分析

  1. 从前后端对接文档提取请求、响应、权限、状态机、幂等、并发和通知要求。
  2. 标出数据归属:hotgo 公共数据或租户库业务数据。
  3. 选择入口:HTTP、WebSocket、Kafka Admin Command 或内部 API。
  4. 列出需要同步的状态:MySQL、Redis、Topic 内存、进程内缓存、C 端推送。
  5. 明确失败类型:可重试、不可重试、并发冲突、部分成功和补偿。

11.2 领域与数据库

  1. 在 server/store/types/ 按业务域定义实体、状态常量、查询参数和结果。
  2. 在 server/db/adapter.go 增加最小必要的租户持久化接口,方法必须携带 TenantID。
  3. 在 MySQL 适配器实现 SQL;复杂写操作使用事务,状态迁移在 SQL WHERE 中校验旧状态/版本。
  4. 在对应 *_schema.go 添加幂等建表/升级 SQL,为高频查询、唯一幂等键和状态扫描建索引。
  5. 在 server/store/store.go 增加 mapper,负责参数验证、调用 adapter 和缓存编排;handler 不应直接拼 SQL。

表命名遵循当前租户库风格,类型必须与 Go 全链路一致,尤其是 ID、seq、version 使用 BIGINT/int64。面向 JavaScript 客户端的 64 位 ID 优先以字符串输出,避免精度丢失。

11.3 API 或协议层

HTTP 功能:

  1. 在 server/im_http.go 定义独立 request/response 结构,不要直接暴露数据库 row。
  2. 使用 decodeIMJSONBody、authenticateIMHTTPBearer、requireIMGroupMember 等公共函数做解码、认证和权限检查。
  3. 在 server/main.go 注册具体路由,路由顺序要避免通配前缀吞掉更具体的路径。
  4. 使用 writeIMSuccess/writeIMError 统一输出,新增稳定业务 reason。
  5. 资源更新需并发保护时使用 version/If-Match,不允许用 version=0 绕过乐观锁。

WebSocket 功能:

  1. 在 datamodel.go 增加最小协议字段,考虑 JSON 和 protobuf 转换兼容。
  2. 在 Session 做连接级验证,在 Topic 做资源权限与业务状态验证。
  3. 所有可重发消息必须有 client_mid/业务幂等键。
  4. 成功后同时考虑本节点 Session、跨节点 fanout、离线 Push 与历史补偿。

11.4 缓存、事件与审计

  1. 对高频读对象选择 cache-aside:先 Redis,miss 后读 MySQL 并回填。
  2. 所有写方法列出需失效的 key,包括对象 key、关系 Set、分页快照和未读数。
  3. 需可靠异步处理时同事务写 outbox,消费端使用 event_id + consumer_group 幂等。
  4. 影响管理权限、账号安全、消息内容、文件处置或人工重放的操作要写审计。
  5. 业务状态变更需立即告知 C 端时,创建业务事件并通过已认证服务端链路投递,不信任客户端伪造的系统消息。

11.5 测试与文档

每个功能至少覆盖:

  1. 正常路径、无认证、跨租户、无权限、资源不存在和方法错误。
  2. 重复 Idempotency-Key/client_mid 不产生重复数据。
  3. 同一幂等键配不同 payload 返回冲突。
  4. version 冲突和并发状态迁移。
  5. MySQL 成功后 Redis/Topic 缓存及在线客户端状态正确刷新。
  6. Kafka 重复消费、临时失败重试、不可重试失败和死信重放。
  7. 64 位 ID/seq 在 JSON 编解码时不丢失精度。
  8. 更新前后端对接文档、SQL 变更集和未实现增强项。

12. 关键约定

12.1 ID 与时间

  • 用户 ID 由 Snowflake UID 体系生成,对外使用 usr... 编码形式或协议规定的字符串。
  • 业务 ID 必须全局或租户内唯一,不能用秒级时间戳直接代替唯一 ID。
  • SeqId、数据库主键和 version 使用 int64/BIGINT。
  • 持久化与对外时间使用 UTC,HTTP 输出 RFC3339/RFC3339Nano。

12.2 Topic 归一化

客户端传入的 usr...、slf 等可见 Topic 不一定是数据库真实 Topic。历史查询、上下文、收藏、转发、媒体、回执等接口在查库前必须统一归一化,并在鉴权、查询和响应之间保持明确的 Topic 语义。

12.3 权限

  • HTTP handler 负责验证主体和请求参数,Store/adapter 仍应使用租户和业务条件限制写入。
  • Topic 管理员、群主、普通成员权限以服务端为准,不接受客户端声明的角色。
  • 群主转让、设管理员、禁言、踢人等变更后,必须刷新 Topic 内 perUser 权限快照和 Redis 订阅/成员缓存,不能等待重连后生效。
  • 封禁、冻结、踢设备、全端退出和注销等账号治理必须同步处理已存在 Session。

12.4 可观测性

  • 日志使用 server/logs 的 Info/Warn/Err,默认输出 stderr,由容器日志系统收集。
  • 日志行前缀为 I/W/E 加 Go log flags 生成的日期时间;敏感值、Token、密码、验证码、证件号和第三方密钥不得记录。
  • HTTP 日志和异步事件应携带 request_id/trace_id、租户、业务类型和业务 ID。
  • 新增核心链路应增加请求数、失败数、延迟、队列深度/积压和死信指标。

13. 禁止的实现方式

  1. 在 HTTP handler 内直接打开 MySQL 或拼接 SQL。
  2. 因租户库表没有 tenant_id 列,就不向 Store 传递 TenantID。
  3. Admin 直连 tinode 或 IM 直连 hotgo。
  4. 将 Kafka 当作 HTTP RPC 或实时 WebSocket fanout 使用。
  5. 将业务最终状态只存在 Redis。
  6. 为同一份高频数据同时增加进程内缓存和 Redis 缓存。
  7. 业务写成功后不失效 Redis、Topic 权限快照和成员分页快照。
  8. 重试请求没有 Idempotency-Key/client_mid/event_id。
  9. 使用 int 或 JSON number 承载可能超过 JavaScript 安全范围的 ID/seq。
  10. 直接信任客户端构造的系统通知、管理员角色或审计信息。
  11. 文件上传成功后不创建业务引用和安全审核任务。
  12. 新增数据表只在开发者本机手工执行,不写幂等 schema SQL、升级逻辑和回归测试。

14. 开发与验收命令

MySQL 构建使用 build tag:

go build -tags mysql -o bin/tinode ./server
go build -tags mysql -o bin/init-db ./tinode-db

建议测试顺序:

# 先跑修改模块的定向测试
go test ./server -run 'TestName'

# Store 与 MySQL schema/adapter
go test -tags mysql ./server/store/... ./server/db/mysql/...

# 完整服务端回归
go test -tags mysql ./server/...

发版前至少检查:

  • 配置文件可解析,MySQL、Redis、Kafka、OSS 等必需依赖可连通。
  • 数据库版本与代码一致,所有新表/列/索引已在目标环境幂等执行。
  • Kafka Topic、消费组、重试和死信链路可用。
  • 单节点和多节点下的在线状态、群消息、权限变更均一致。
  • 新增接口与前后端对接文档一致,无密钥、密码或测试 Token 进入 Git。

15. 新功能提交清单

开发者在提交 PR/代码前应逐项确认:

  • 已确定功能入口是 HTTP、WebSocket、Kafka 还是内部 API。
  • 已确定数据归属 hotgo 还是租户库。
  • 所有 Store/adapter 调用都显式携带 TenantID。
  • 已设计幂等键、唯一索引、业务版本和状态机。
  • 已明确 MySQL、Redis、进程内缓存的使用和失效顺序。
  • 需跨系统可靠投递时使用 outbox + Kafka,消费端已幂等。
  • 已处理 Topic 内存权限快照和多节点同步。
  • 文件已存 OSS,业务引用和安全审核已建立。
  • SeqId/ID/version 已在 Go、JSON、Kafka 和 MySQL 全链路使用 64 位安全类型。
  • 已增加正常、权限、跨租户、幂等、并发、缓存和重试测试。
  • 已补充 API 对接文档、SQL 变更和未完成增强项。

16. 相关文档

  • docs/IM-前后端对接文档.md:C 端业务 API 和交互协议。
  • docs/IM-系统设计文档.md:原始产品系统设计。
  • docs/IM-Redis三层数据分层改造方案.md:进程内、Redis、MySQL 分层细节。
  • docs/IM-Kafka接入方案.md:Admin/IM 边界、Topic、幂等、重试和死信。
  • docs/IM-Kafka接入范围识别.md:按业务场景判断 Kafka 接入范围。
  • docs/IM-Kafka接入联调测试文档.md:Admin 与 IM 联调用例。
  • docs/third/IM-文件安全第三方接入记录.md:OSS 和文件安全链路。
  • docs/IM-未实现后续增强项.md:已知未完成能力和深度优化项。