24 KiB
IM 服务端架构与新功能开发指南
文档状态:按 2026-09-08 代码实现整理
适用项目:im_server
文档目标:让新接手的开发者能够快速判断一个功能应放在哪里、如何调用、如何存储、如何做多节点一致性,以及如何验收。
1. 系统定位
im_server 是基于 Tinode 扩展的多租户 IM 业务服务,同时承担两类职责:
- IM 实时核心:WebSocket 连接、登录会话、Topic 订阅、消息路由、在线状态、音视频信令。
- 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 是服务入口,主要顺序为:
- 读取 JSON with comments 格式的
meetingim.conf/tinode.conf。 - 初始化日志、expvar、pprof 和集群 worker ID。
- 打开 Store,校验数据库版本与租户适配能力,同时初始化 Redis runtime。
- 初始化 Kafka、IM 内部 API 和 Admin 内部配置 API 客户端。
- 初始化认证器、验证器、文件处理器、Push 和音视频。
- 创建
SessionStore和Hub,启动 Redis 群消息 fanout 订阅。 - 启动 Kafka Admin Command、Config Event、Cache Invalidate、Message Persist 和 Dead Letter 消费者。
- 启动集群、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 一致性顺序
普通业务写的推荐顺序:
- 校验租户、身份、权限、幂等键和业务版本。
- 在同一事务中更新 MySQL 业务状态及 outbox(需要可靠事件时)。
- 提交事务。
- 失效本节点 Redis/进程内缓存。
- 投递 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 需求分析
- 从前后端对接文档提取请求、响应、权限、状态机、幂等、并发和通知要求。
- 标出数据归属:
hotgo公共数据或租户库业务数据。 - 选择入口:HTTP、WebSocket、Kafka Admin Command 或内部 API。
- 列出需要同步的状态:MySQL、Redis、Topic 内存、进程内缓存、C 端推送。
- 明确失败类型:可重试、不可重试、并发冲突、部分成功和补偿。
11.2 领域与数据库
- 在
server/store/types/按业务域定义实体、状态常量、查询参数和结果。 - 在
server/db/adapter.go增加最小必要的租户持久化接口,方法必须携带TenantID。 - 在 MySQL 适配器实现 SQL;复杂写操作使用事务,状态迁移在 SQL
WHERE中校验旧状态/版本。 - 在对应
*_schema.go添加幂等建表/升级 SQL,为高频查询、唯一幂等键和状态扫描建索引。 - 在
server/store/store.go增加 mapper,负责参数验证、调用 adapter 和缓存编排;handler 不应直接拼 SQL。
表命名遵循当前租户库风格,类型必须与 Go 全链路一致,尤其是 ID、seq、version 使用 BIGINT/int64。面向 JavaScript 客户端的 64 位 ID 优先以字符串输出,避免精度丢失。
11.3 API 或协议层
HTTP 功能:
- 在
server/im_http.go定义独立 request/response 结构,不要直接暴露数据库 row。 - 使用
decodeIMJSONBody、authenticateIMHTTPBearer、requireIMGroupMember等公共函数做解码、认证和权限检查。 - 在
server/main.go注册具体路由,路由顺序要避免通配前缀吞掉更具体的路径。 - 使用
writeIMSuccess/writeIMError统一输出,新增稳定业务reason。 - 资源更新需并发保护时使用 version/
If-Match,不允许用version=0绕过乐观锁。
WebSocket 功能:
- 在
datamodel.go增加最小协议字段,考虑 JSON 和 protobuf 转换兼容。 - 在 Session 做连接级验证,在 Topic 做资源权限与业务状态验证。
- 所有可重发消息必须有
client_mid/业务幂等键。 - 成功后同时考虑本节点 Session、跨节点 fanout、离线 Push 与历史补偿。
11.4 缓存、事件与审计
- 对高频读对象选择 cache-aside:先 Redis,miss 后读 MySQL 并回填。
- 所有写方法列出需失效的 key,包括对象 key、关系 Set、分页快照和未读数。
- 需可靠异步处理时同事务写 outbox,消费端使用
event_id + consumer_group幂等。 - 影响管理权限、账号安全、消息内容、文件处置或人工重放的操作要写审计。
- 业务状态变更需立即告知 C 端时,创建业务事件并通过已认证服务端链路投递,不信任客户端伪造的系统消息。
11.5 测试与文档
每个功能至少覆盖:
- 正常路径、无认证、跨租户、无权限、资源不存在和方法错误。
- 重复
Idempotency-Key/client_mid不产生重复数据。 - 同一幂等键配不同 payload 返回冲突。
- version 冲突和并发状态迁移。
- MySQL 成功后 Redis/Topic 缓存及在线客户端状态正确刷新。
- Kafka 重复消费、临时失败重试、不可重试失败和死信重放。
- 64 位 ID/seq 在 JSON 编解码时不丢失精度。
- 更新前后端对接文档、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. 禁止的实现方式
- 在 HTTP handler 内直接打开 MySQL 或拼接 SQL。
- 因租户库表没有
tenant_id列,就不向 Store 传递TenantID。 - Admin 直连
tinode或 IM 直连hotgo。 - 将 Kafka 当作 HTTP RPC 或实时 WebSocket fanout 使用。
- 将业务最终状态只存在 Redis。
- 为同一份高频数据同时增加进程内缓存和 Redis 缓存。
- 业务写成功后不失效 Redis、Topic 权限快照和成员分页快照。
- 重试请求没有
Idempotency-Key/client_mid/event_id。 - 使用
int或 JSON number 承载可能超过 JavaScript 安全范围的 ID/seq。 - 直接信任客户端构造的系统通知、管理员角色或审计信息。
- 文件上传成功后不创建业务引用和安全审核任务。
- 新增数据表只在开发者本机手工执行,不写幂等 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:已知未完成能力和深度优化项。