比较 Obelisk、Temporal 和 Restate
摘要
对三个工作流系统(Obelisk、Temporal、Restate)实现天气预报工作流的技术比较,重点突出架构、Rust API、确定性边界和部署模型等方面的差异。
<p><a href="https://lobste.rs/s/h1r6gc/comparing_obelisk_with_temporal_restate">评论</a></p>
查看缓存全文
缓存时间: 2026/07/15 13:44
# 对比 Obelisk、Temporal 与 Restate
来源:https://obeli.sk/blog/comparing-obelisk-temporal-restate/
2026-07-14
Obelisk、Temporal (https://temporal.io/) 和 Restate (https://restate.dev/) 都能让函数在进程消失后继续推进。架构上最大的区别在于,Obelisk 是一个工作流运行时:它自己加载并运行应用组件。而 Temporal 和 Restate 是编排器,其应用代码在独立部署的 Worker 或服务端点中运行。这一差异决定了各系统能预防哪些错误,以及实际部署什么。
我用这三个系统实现了同一个简单工作流,目的是比较它们的 Rust API、确定性边界、活动隔离、密钥管理和部署模型。预先说明:Temporal 的 Rust SDK (https://github.com/temporalio/sdk-rust) 目前处于公开预览阶段。Temporal 在其他语言中有成熟的 SDK,因此本 Rust 对比不应被视为对其整体开发者体验的定论。
## 工作流
该工作流从 Open-Meteo (https://open-meteo.com/) 并行获取阿姆斯特丹和巴黎的温度,等待一个持久的 1 秒定时器,然后返回温度更高的城市。它跨越了四个有用的边界:
1. 两次 HTTP 请求是副作用,不能在确定性工作流代码中执行。
2. 请求应并发执行。
3. 定时器必须在进程重启后仍然有效。
4. 最终比较是普通的确定性代码。
用伪代码表示,三个实现都做了以下事情:
```
amsterdam = start forecast(52.37, 4.90)
paris = start forecast(48.86, 2.35)
sleep durably for one second
return warmer(await amsterdam, await paris)
```
### Obelisk
Obelisk 组件暴露并导入用 WIT 定义的带类型接口。工作流同时导入活动的正常接口和生成的扩展函数,用于并发提交:
```
// WIT: compare: func() -> result;
fn compare() -> Result<String> {
let amsterdam = workflow_support::join_set_create();
let paris = workflow_support::join_set_create();
forecast_submit(&amsterdam, 52.37, 4.90);
forecast_submit(&paris, 48.86, 2.35);
workflow_support::sleep(ScheduleAt::In(Duration::Seconds(1)), None)
.map_err(|()| "sleep cancelled".to_string())?;
let amsterdam = forecast_await_next(&amsterdam)
.map_err(|err| format!("activity failed: {err:?}"))??;
let paris = forecast_await_next(&paris)
.map_err(|err| format!("activity failed: {err:?}"))??;
Ok(warmer("Amsterdam", amsterdam, "Paris", paris))
}
```
并发需要使用 Obelisk 生成的 `-submit` 和 `-await-next` 函数以及 join sets (https://obeli.sk/docs/latest/concepts/workflows/join-sets/)。它们为并发完成提供了一种线性的、持久的接口,回放时可以重现。同步导入仍然是普通的类型化函数调用。WIT 增加了一个模式定义和绑定生成步骤,同时让组件边界是类型检查的且语言无关。WIT 参考文档 (https://obeli.sk/docs/latest/concepts/wit-reference/) 涵盖了它的类型、结果、记录和命名规则。
### Temporal
Temporal 的公开预览版 Rust SDK 使用宏来定义类型化的工作流和活动。启动一个活动会返回一个持久的 future,其工作流感知的 `join!` 宏以确定性的声明顺序并发轮询两个 future:
```
pub async fn run(
ctx: &mut WorkflowContext,
input: CompareWeatherInput,
) -> WorkflowResult<String> {
let options = ActivityOptions::schedule_to_close_timeout(Duration::from_secs(30));
let amsterdam = ctx.start_activity(
WeatherActivities::fetch_forecast,
input.first,
options.clone(),
);
let paris = ctx.start_activity(
WeatherActivities::fetch_forecast,
input.second,
options,
);
ctx.timer(Duration::from_secs(1)).await;
let (amsterdam, paris) = temporalio_sdk::workflows::join!(amsterdam, paris);
let (amsterdam, paris) = (amsterdam?, paris?);
Ok(warmer(amsterdam, paris))
}
```
HTTP 请求是一个 `#[activity]` 方法,注册在 Worker 上。活动默认会重试;schedule-to-close 超时限制了所有尝试。API 是直接的 Rust 代码,尽管 0.5 版本仍处于公开预览阶段。
### Restate
Restate 将 HTTP 操作建模为一个独立服务,并通过生成的客户端调用它。它的 `DurableFuturesUnordered` 记录哪些调用已完成,因此在恢复期间不同网络完成顺序不会改变结果槽:
```
async fn run(&self, ctx: WorkflowContext<'_>) -> HandlerResult<String> {
let forecasts = ctx.service_client::<ForecastServiceClient>();
let mut calls = DurableFuturesUnordered::new();
calls.push(forecasts.fetch(Json(amsterdam())).call());
calls.push(forecasts.fetch(Json(paris())).call());
ctx.sleep(Duration::from_secs(1)).await?;
let mut results = [None, None];
while let Some((index, result)) = calls.next().await? {
results[index] = Some(result?.into_inner());
}
Ok(warmer(results))
}
```
`ForecastService` 处理器包含实际的 HTTP 操作。Restate 的 `ctx.run` 闭包将结果记录日志,并为操作提供自己的重试策略:
```
let forecast = ctx
.run(|| async move {
fetch_open_meteo(client, city).await
})
.name("fetch_open_meteo")
.retry_policy(RunRetryPolicy::default().max_attempts(3))
.await?;
```
该操作也可以是一个内联的 `ctx.run` 步骤。将操作分离为独立服务使得边界与其他运行时的活动相当;两个 Restate 服务仍可共享一个 Rust 二进制文件和端点。
## 子流程生命周期与取消
天气工作流等待两个预测结果。当父流程返回或取消时,如果子工作仍在进行中,不同系统的表现不同。
**Obelisk** 强制使用结构化并发 (https://obeli.sk/docs/latest/concepts/structured-concurrency/)。在 join set 中的所有子流程结束之前,父流程无法完成。关闭 join set 会取消名称选择加入 `-cancellable` 的待处理活动、定时器和子工作流;其他子工作流则会被等待。已取消的工作流代码不会再次运行,因此清理工作应放在祖先流程中,通常是一个最小化的 saga (https://obeli.sk/docs/latest/js/getting-started-fly-agent/#the-saga-pattern)。`-schedule` 是显式的分离替代方案。
**Temporal Rust** 不会自动 join 持久的 future,丢弃一个 future 不会取消其活动或子工作流。应用程序代码必须等待或取消它。子工作流添加了父关闭策略:默认终止、请求协作取消或遗弃子工作流。活动取消也是协作式的,通常通过心跳传递。参见 Rust 取消指南 (https://docs.temporal.io/develop/rust/workflows/cancellation) 和子工作流指南 (https://docs.temporal.io/develop/rust/workflows/child-workflows)。
**Restate Rust** 在创建 `CallFuture` 时持久提交服务调用,但返回时既不 join 也不取消该调用。`.send()` 使有意的单向工作变得明确。取消通过请求-响应调用树协作传播;`kill` 跳过清理。参见管理调用 (https://docs.restate.dev/services/invocation/managing-invocations)。
## 确定性:约定还是能力边界
所有三个系统都通过重放代码与持久化历史记录进行恢复。记录的副作用结果不会重复执行,因此代码必须在重放期间产生兼容的持久操作。区别在于运行时对首次执行的约束强度。
### Obelisk 消除环境非确定性
Obelisk 的 `wasm32-unknown-unknown` 工作流无法打开套接字、读取文件或环境变量、使用主机时间或随机数、或创建原生线程。它只能调用显式导入的 WIT 函数。这在重放之前就消除了大量意外非确定性;Obelisk 在代码更改后仍然会检查历史记录的兼容性。参见工作流确定性文档 (https://obeli.sk/docs/latest/concepts/workflows/)。
### Temporal 和 Restate 运行原生应用代码
Temporal 工作流作为原生代码运行在用户托管的 Worker 中。作者必须使用 SDK 活动和定时器,避免非确定性 API,并产生与事件历史兼容的命令。重放会检测不匹配,但这属于 SDK 规范,而非安全沙箱。参见 Temporal 的工作流定义 (https://docs.temporal.io/workflow-definition)。
Restate 同样将原生处理器重放为日志。非确定性工作属于 `ctx.run` 或独立服务,重放必须发出兼容的上下文操作。参见服务版本化 (https://docs.restate.dev/services/versioning)。
## 安全边界:服务器策略、API 和活动
Obelisk 将重启控制的主机策略与热可重新部署的应用分离开来。`server.toml` 设置 API 令牌和宽泛规则(例如 exec-activity 固定)。`deployment.toml` 描述组件以及更细粒度的 HTTP 和密钥能力;它必须通过验证,并且不能放宽服务器策略。参见配置概览 (https://obeli.sk/docs/latest/configuration/#overview) 和 exec activity 门控 (https://obeli.sk/docs/latest/configuration/#exec-activity-gating)。
Obelisk 的 API 默认需要令牌,而其 webhook 端口保持开放以处理应用流量。自托管 Temporal 默认使用无操作授权器,自托管 Restate 期望代理和网络控制来管理入口和管理。它们的云版增加身份验证。参见 Obelisk 的身份验证文档 (https://obeli.sk/docs/latest/authentication/)、Temporal 的自托管安全指南 (https://docs.temporal.io/self-hosted-guide/security) 和 Restate 的服务器安全指南 (https://docs.restate.dev/server/security)。
Temporal 活动和 Restate 服务处理器是普通的原生应用代码。它们可以使用进程环境、文件系统和网络。限制由部署平台负责。Obelisk 的 WASM 和 JS 活动运行在 WebAssembly 沙箱中。除非部署明确允许某个主机和方法,否则出站 HTTP 会被拒绝。Open-Meteo 活动需要以下条目:
```
[[activity_wasm.allowed_host]]
pattern = "https://api.open-meteo.com"
methods = ["GET"]
request_url_regex = "^GET https://api\\.open-meteo\\.com/v1/forecast$"
```
Obelisk 运行时强制执行允许列表。同样的规则可以注入一个凭证,而不会将值暴露给组件:
```
[[activity_wasm.allowed_host]]
pattern = "https://api.example.com"
methods = ["POST"]
[activity_wasm.allowed_host.secrets]
env_vars = ["API_TOKEN"]
replace_in = ["headers"]
```
组件将一个不透明占位符放入标头。主机仅在目的地获得批准后才替换真实值,从而将秘密排除在组件内存、日志和工作流历史之外。参见出站 HTTP 配置 (https://obeli.sk/docs/latest/configuration/#outbound-http-allowlist)。
这个边界也适用于生成代码。在 workflow-agent (https://github.com/obeli-sk/workflow-agent) 中,一个代理在 Obelisk 内部运行,编辑存储的组件源代码,并可以在操作员批准后热重新部署它。
## 与运行中的工作流交互
人工审批和状态读取揭示了工作流是单个等待函数,还是一个具有消息处理器的持久对象。
### Obelisk:在显式等待点使用类型化的单向通道
Obelisk 工作流可以调用一个类型化的 stub 活动 (https://obeli.sk/docs/latest/concepts/activities/stub/),该活动具有 WIT 签名但没有实现:
```
let approval = approval(order_id)?;
```
待处理的子流程可以通过 CLI、Web UI 或 API(包括另一个工作流)用类型化的结果来满足。使用 `-submit` 异步提交 stub 的工作流甚至可以在之后自行解析它。无论哪种方式,输入都有一个固定的到达点:它只能针对工作流已经创建的 stub。Obelisk 没有在工作流内定义的 Query 或处理器可以在任意点运行,尽管执行状态和历史记录仍然可以检查。两个 stub 可以形成一个请求/响应对 (https://obeli.sk/docs/latest/patterns/stub-rpc/)。
### Temporal:具有三种消息类型的持久对象
Temporal 将工作流建模为有状态服务。Signal 是一个没有结果的持久消息,Query 读取状态而不添加到历史记录中,Update 是一个跟踪的 RPC,可以改变状态并返回结果。
```
#[signal]
fn approve(&mut self, _ctx: &mut SyncWorkflowContext, input: Approval) {
self.approval = Some(input);
}
#[query]
fn status(&self, _ctx: &WorkflowContextView) -> Status {
self.status.clone()
}
#[update]
fn change_owner(&mut self, _ctx: &mut SyncWorkflowContext, owner: String) -> String {
std::mem::replace(&mut self.owner, owner)
}
```
异步的 Signal 和 Update 处理器可以在 `await` 点与主方法交错,因此共享状态需要精心设计,尽管处理器不会并行运行。参见 Temporal 的 Rust 消息传递指南 (https://docs.temporal.io/develop/rust/workflows/message-passing)。
### Restate:工作流处理器和持久承诺
Restate 位于这两个模型之间。其独占的 `run` 处理器是工作流键的单一写入者;共享处理器可以查询状态或解析持久承诺。承诺可以在工作流开始等待之前被解析,因此它对位置的依赖程度低于 Obelisk stub。
```
#[shared]
async fn approve(
&self,
ctx: SharedWorkflowContext<'_>,
decision: String,
) -> HandlerResult<()> {
ctx.resolve_promise("decision", decision);
Ok(())
}
// In run:
let decision: String = ctx.promise("decision").await?;
```
RPC 风格的更新可以向 `run` 发送信号,并等待第二个承诺进行确认。参见 Restate 的工作流教程 (https://docs.restate.dev/tour/workflows)。
## 实际部署了什么
列出此示例所需的进程,架构就变得清晰了。
| 部署关注点 | Obelisk | Temporal | Restate |
|------------|---------|----------|---------|
| 应用代码 | 由服务器托管的 WASM 组件 | 独立的 Rust Worker 进程 | 独立的 Rust 服务端点 |
| 持久化 | SQLite 或 PostgreSQL | Cassandra、MySQL 或 PostgreSQL(自托管) | 内置在单个服务器中;集群存储用于高可用 |
| 应用部署与运维 | 代码变更形成一个版本化部署,由 Obelisk 通过 CLI 或 API 管理 | 用户负责部署和运维 Worker 进程,即使使用 Temporal Cloud 也是如此 | 用户负责部署和运维应用服务,即使使用 Restate Cloud 也是如此 |
### Obelisk:应用部署是运行时概念
本地开发时,一个命令即可启动带有应用的服务器:
```
obelisk server run --deployment deployment.toml
```
同一个清单可以通过相同的 CLI 发送到远程实例:
```
obelisk --api-token "$TOKEN" deployment apply \
--api-url https://obelisk.example.com deployment.toml
```
`obelisk deployment apply` 存储一个版本化的清单并热激活它,无需重启服务器。本地工件进入内容寻址存储;OCI 组件保持为注册表引用。WASM 或 JS 摘要的更改会触发进行中的工作流针对新组件进行重放。兼容的执行会自动升级,不兼容的则保持在先前版本。参见部署文档 (https://obeli.sk/docs/latest/concepts/deployments/)。
最小的拓扑也非常小:一个 Obelisk 进程可以托管编排器、工作流、活动和内嵌的 SQLite 数据库。小型安装通常适合 256 到 512 MB 的虚拟机,尽管所需内存自然取决于工作负载。当部署超出单节点形态时,PostgreSQL 和高可用基础设施仍然是选项。
拥有组件还带来了一些有用的运维功能:
- **重放与推进**:预览并有选择地应用暂停工作流的下一个持久写入。参见重放与推进 CLI 文档 (https://obeli.sk/docs/latest/cli/#replaying-an-execution);0.38 公告 (https://obeli.sk/blog/announcing-obelisk-0-38/#step-through-debugging) 展示了 Web UI。
- **自动回溯**:
相似文章
@omarsar0: 刚刚就动态工作流进行了精彩的讨论。粗略笔记:- 适用于极少数用例 - 将其视为…
关于测试时计算中动态工作流的讨论,包括其有限的用例、对研究实验的好处,以及对更好基准测试的需求。提及了用于智能体编排的模型如Mythos和Opus 4.8。
@alswl: 最近一直在找 Agent Dynamic Workflow 的替代方案。 一开始是沿着 Temporal 这条线找的。Temporal 很强,但如果只是想把 agent、脚本、数据处理、运维任务动态串起来,有时候会觉得整套体系有点重。 然…
作者介绍了 Dagu 作为 Temporal 的轻量级替代方案,用于动态工作流编排,并附带 Northflank 平台的推广内容。
Orc(暂定名)- 可审计且声明式的 AI 工作流
开发者正在寻求关于"ORC"的反馈,这是一个早期的“编排即代码”工具,使用声明式 DSL 来定义、验证和版本控制 LLM 工作流。旨在服务于结合本地和云端模型的用户,它用可审计的、类似 Terraform 的定义取代了复杂的 Python 脚本,用于代理和工具执行。
Show HN: DBOSify – 基于Postgres的Temporal即插即用替代方案
DBOSify 是 Temporal Python 的即插即用替代方案,它使用 Postgres 替代 Temporal 服务器,无需额外基础设施即可实现持久化工作流。
SQLite:持久化工作流的全部所需
这篇博文认为,SQLite 结合 Litestream 进行异步备份,为许多工作流系统(尤其是 AI 智能体)提供了一种简单而有效的持久化执行方法,无需单独编排层或网络数据库。