Restate 架构拆解
Restate 的官方架构可以概括为:客户端请求先进入 Ingress,再按 key 路由到分区;分区处理器把所有动作先写入 Durable Log,然后再物化到本地状态并驱动服务执行。服务本身保持普通业务代码形态,可靠性由 Restate Server 和 SDK 协作完成。
总览
这里最重要的不是组件名,而是写路径:
业务动作先成为日志里的事实,再被处理器读取、物化、确认给 handler。
组件职责
| 组件 | 职责 | 教学版对应物 |
|---|---|---|
| Ingress | 接收 HTTP/Kafka/内部调用,解析目标服务与 key | FastAPI /api/orders |
| Keyed Routing | 根据 workflow id、object key、idempotency key 选择分区 | 教学版单分区,保留 idempotency key |
| Durable Log | 复制并排序所有 invocation、step、timer、state 事件 | MySQL journal_entries |
| Partition Processor | 读取日志、驱动 handler、维护物化状态 | 后台 worker_loop() |
| Partition Store | 快速读取 Journal、状态、计时器索引 | MySQL 查询 + 简化状态表 |
| Control Plane | 节点、分区、epoch、segment 元数据 | 教学版不实现,只解释 |
| Service SDK | 给业务代码提供 ctx.run、ctx.sleep 等动作 | DurableContext |
Invocation 生命周期
如果服务在 ctx.run("charge") 后崩溃,重试时处理器会把已有 Journal 交给新的 attempt。业务代码重新进入同一个函数,但 charge 的结果由 Journal 返回。
为什么要分区
生产系统不能把所有 invocation 放到一个全局锁后面。Restate 使用 key 做分区:
分区带来两个关键收益:
- 同一个 key 的状态和编排在同一分区内完成,热路径不需要跨节点事务。
- 不同 key 可以并发扩展,吞吐量随分区和 worker 扩展。
Journal 与物化状态
Restate 的文档强调 replicated log 是事实来源,RocksDB 里的 partition store 是可重建的物化视图。这个区分非常关键:
| 数据 | 性质 |
|---|---|
| Durable Log | 事实来源,决定动作顺序和提交点 |
| Partition Store | 为了快速读取 Journal、timer、state 的缓存/物化视图 |
| Snapshot | 限制恢复时需要重放的日志长度 |
教学版使用 MySQL 做两件事:既存 Journal,也查询 Journal。这简化了实现,但也模糊了“事实日志”和“物化视图”的边界。课程中会反复提醒这一点。
失败处理的关键:epoch fencing
分布式恢复有一个危险场景:旧 leader 以为自己还活着,新 leader 已经接管。Restate 用 leader epoch 给 attempt 和处理器加边界,低 epoch 的迟到事件会被拒绝。这就是 fencing。
教学版不实现 epoch,但会保留一个工程提示:
教学版限制
如果你把 examples/durable-mini 部署成多个 worker 并发处理同一个 invocation,当前代码不能保证 exactly-once。生产化必须引入锁、租约、epoch 或数据库条件更新。
与传统方案的区别
| 方案 | 优点 | 局限 |
|---|---|---|
| Cron + 状态表 | 简单,适合定时扫描 | 业务步骤仍需自己处理幂等和恢复 |
| 队列 + consumer retry | 解耦吞吐好 | retry 会重新进入 handler,步骤级恢复要自写 |
| Workflow 引擎 | 编排能力强 | 有些系统要求 DSL 或活动函数拆分较重 |
| Durable Execution runtime | 保留普通代码形态,步骤级恢复 | 运行时复杂,对日志和协议要求高 |
小结
Restate 的架构可以用三句话记住:
- Ingress 负责把调用变成可路由的 invocation。
- Partition Processor 负责把执行过程变成 log-first 的事实流。
- SDK Context 让普通业务代码以
run/sleep/call的方式参与持久化协议。