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 永远收不到 那个事件。

早期实现InMemoryBrokerfeature/realtime/inmemory.go)。 进程内 channel fan-out。只在单进程下正确。多副本下事件丢 失,这是当前必须修的 bug。

当前修法:换成 RedisBrokerfeature/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 延迟预算:

跨 regionp50p95p99
同 region50 ms200 ms500 ms
北京 ↔ 上海80 ms300 ms800 ms
国内 ↔ 新加坡150 ms400 ms1 s

跨大陆 + 海外的实时不适合用 realtime — 切换到 polling 或 webhook。

readiness checks {#m2-readiness-checks}

当前不上多 region,但写代码时要避免给未来留坑

Check当前状态行动
Realtime Broker interface 跟 region 解耦✅ 已是
事件 payload 包含 project tenant_id
Redis channel 命名带 projectmortar: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-ID header;副本拉历史事件回放
  • 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(规划中)

后续迁移路径:

  1. 加 NSBroker impl + env-var switch (MORTAR_REALTIME_BACKEND=ns)
  2. 灰度:先在 staging cn-shenzhen 启用 NSBroker
  3. 跨 region cluster spin-up(cn-shenzhen 完整 Mortar 部署)
  4. project home region 字段加入;新 project 创建时让用户选
  5. 老 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 承诺;客户端必须接受”实时 ≠ 实时”

参考