Realtime — 跨副本 / 跨 region 的 broker 演进
状态:跨副本 fan-out 的设计 + 实装 doc,同时是跨 region 的 readiness check。 这份 doc 覆盖两个阶段的问题,分清楚哪些是当前就要修的、 哪些是规划中才会遇到但当前必须为它留好接口。
Mortar realtime 经历两个独立但相关的扩展问题。两者都通过同一个
realtime.Broker 接口解决,但 backend impl 不同。
当前要解决的问题:跨副本 fan-out
即使单 region 内,Mortar 也跑多副本 —— 一个 LB 后面 N=3 个 Mortar 进程。
┌─ Mortar 副本 A ←── SSE 客户端 X
LB ─────────┼─ Mortar 副本 B ←── (publish 在这里发生)
└─ Mortar 副本 C ←── SSE 客户端 Y
客户端 X subscribe 到副本 A;一个写请求 落 在副本 B 上触发
pg_notify;如果副本之间不共享 broker,客户端 X 永远收不到
那个事件。
早期实现:InMemoryBroker(feature/realtime/inmemory.go)。
进程内 channel fan-out。只在单进程下正确。多副本下事件丢
失,这是当前必须修的 bug。
当前修法:换成 RedisBroker(feature/realtime/redis.go),
跨副本走 Redis pub/sub。一个副本 publish → Redis pub/sub → 所有
副本 subscribe → 每个副本 fan out 到自己的 SSE 客户端。
┌─ 副本 A ─→ SSE 客户端 X
Postgres LISTEN ──→ 副本 B ─→ Redis ─┼─ 副本 B ─→ (本地 SSE 客户端)
└─ 副本 C ─→ SSE 客户端 Y
这就是当前要 ship 的全部内容。 单 region 内多副本正确。
规划中要继承的问题:跨 region message bus
规划中,当 Mortar 扩到多 region 时(cn-hangzhou + cn-shenzhen),broker 需要跨 region fan out 而不只是跨副本。
为什么 RedisBroker 不能直接用于跨 region:
- Redis 不是为跨 region 设计的(pub/sub 没持久化,无跨 region replication 保证)
- 跨 region Redis 连接抖动 → 事件丢
- Redis pub/sub 无 retention,订阅者断线期间的事件全丢
规划中修法:换成一个跨 region message bus(阿里云 NS 或 Kafka MirrorMaker),跨 region 那一跳走 bus,每个 region 内仍用 Redis 做副本间 fan-out。
Postgres (cn-hz) ──┐
├──→ Cross-region bus (Aliyun NS / Kafka)
Postgres (cn-sz) ──┤ │
│ ┌────────┴────────┐
│ ▼ ▼
│ cn-hz region cn-sz region
│ Redis pub/sub Redis pub/sub
│ │ │
└─────┴──── 各 region 的 Mortar 副本
↓
SSE 客户端 (该 region)
为什么当前 doc 要提跨 region
关键设计点:当前的 broker interface 必须 swappable,这样后续切
跨 region message bus 时调用方零改动 —— realtime feature
代码不变,只是底下 Broker impl 从 RedisBroker 换成 NSBroker。
这是这份 doc 标题里 “readiness” 的意思 —— 当前就主动避免给 未来留坑。具体 readiness check 见 § “readiness checks”。
当前架构(跨副本 fan-out 之前)
Postgres LISTEN ──→ Mortar 副本 A (InMemory broker)
↓
SSE 客户端 (同副本)
跨副本客户端收不到别副本的事件(当前修)。 跨 region 客户端收不到别 region 的事件(规划中修)。
多 region 后(规划中)
Postgres (region 1) ──┐
├──→ Cross-region message bus
│ (Aliyun MQ / 阿里云 NS / Kafka)
Postgres (region 2) ──┤
│
▼
每 region Mortar 副本 (Redis pub/sub)
↓
SSE 客户端 (该 region)
跨 region 的消息总线选项:
| 选项 | 优势 | 劣势 |
|---|---|---|
| 阿里云 NS (Cloud Service Bus) | 完全 managed,跨 region 内置 | 阿里云锁定 |
| Kafka (跨 region MirrorMaker) | 行业标准 | 自管成本高 |
| AWS Kinesis (国际化) | AWS region 间 RPO ~1s | 国内连不到 |
| Direct Postgres logical replication | 简单 | 数据库压力大;不是 message bus |
推荐: Aliyun NS for 国内,Kafka for 国际化(AWS region)。最终选型规划中决定。
数据 sharding 模型
每个 project 有 home region(创建时选;不能改)。home region 是 那个 project 数据的真相之源。其他 region:
- Read-only mirror:从 home region 异步复制行(规划中)
- Realtime fan-out:home region 的 Postgres trigger → cross-region bus → 所有 region 的 broker → 所有 region 的 SSE 客户端
project X home: cn-hangzhou
- INSERT into table T (home region)
↓ NOTIFY
- cn-hangzhou Mortar publish "realtime:proj-X:T"
↓ NS topic "mortar.cross.realtime"
- cn-shenzhen Mortar subscribe + republish locally
- cn-shenzhen SSE clients receive event
跨 region 延迟预算:
| 跨 region | p50 | p95 | p99 |
|---|---|---|---|
| 同 region | 50 ms | 200 ms | 500 ms |
| 北京 ↔ 上海 | 80 ms | 300 ms | 800 ms |
| 国内 ↔ 新加坡 | 150 ms | 400 ms | 1 s |
跨大陆 + 海外的实时不适合用 realtime — 切换到 polling 或 webhook。
readiness checks {#m2-readiness-checks}
当前不上多 region,但写代码时要避免给未来留坑:
| Check | 当前状态 | 行动 |
|---|---|---|
| Realtime Broker interface 跟 region 解耦 | ✅ 已是 | 无 |
| 事件 payload 包含 project tenant_id | ✅ | 无 |
| Redis channel 命名带 project | ✅ mortar:realtime:<tenant_id>:<table> | 无 |
database LISTEN connection 池跨副本不可共享 | ✅ | 无 |
| 客户端 SSE 不依赖 server affinity (sticky session) | ⚠ | 当前改 — 任何副本都能 serve 任何客户端 |
| Broker 接口允许 backend swap (Redis → NS) | ⚠ | 当前把 realtime.Broker 拓展 — 加 NewMQBroker() |
第 5 条 (sticky session) — 现在客户端 disconnect 后 reconnect 到不 同副本会丢事件(in-flight messages 锁在前一个副本的 channel 里)。 当前修:
- 客户端 SSE 带
Last-Event-IDheader;副本拉历史事件回放 - Broker 加一个短 retention window(30 sec)用 Redis Stream 而非 pub/sub
第 6 条 — realtime.Broker 接口已经在;加一个 NSBroker impl 占
位代码就够当前:
// 当前 scaffold;后续真实装
type NSBroker struct {
nsClient *aliyunns.Client
topic string
}
func (b *NSBroker) Subscribe(...) { /* TODO 后续 */ }
func (b *NSBroker) Publish(...) { /* TODO 后续 */ }
跨 region cutover plan(规划中)
后续迁移路径:
- 加 NSBroker impl + env-var switch (
MORTAR_REALTIME_BACKEND=ns) - 灰度:先在 staging cn-shenzhen 启用 NSBroker
- 跨 region cluster spin-up(cn-shenzhen 完整 Mortar 部署)
- project home region 字段加入;新 project 创建时让用户选
- 老 project home region 默认 cn-hangzhou,迁移可选
零客户感知 — SSE 协议形状从今天起不变,只是底层 bus 换。
测试 + 仿真
当前加 cross-region 仿真测试:
# 在单机起 2 个 mortar + 1 个 redis + 1 个 假 NS (单 redis 模拟)
docker compose -f docker-compose.multi-region-sim.yml up
# 然后跑测试套件:客户端 A 连 mortar-1,客户端 B 连 mortar-2
# 在 A 上 publish,验证 B 收到
这套仿真当前落地,给后续真实装提供回归测试基线。
风险
- NS 不支持 1-byte messages: 阿里云 NS minimum payload 64 bytes — Mortar event 加 padding 或换 Kafka
- 跨 region 延迟 SLA 不能保证: 阿里云 region 间没有官方 RTT 承诺;客户端必须接受”实时 ≠ 实时”
参考
monitoring-ai-employee.md— cross-region 故障 AI 监控multi-region.md— 多 region 总体设计(规划中)architecture.md§ realtime feature