以前,我曾经构建过那种仅仅依赖数据库的单体工作流,这总是感觉很棘手且难以维护。但当我转向微服务架构时,我才真正意识到分布式工作流有多么棘手复杂。处理分发、容错和长时间运行任务这些事情很快就会变得一塌糊涂。这时,Temporal.io 就能帮上大忙了。
照片由 Kelly Sikkema 拍摄,发布在 unsplash 上。
什么是 Temporal?定义Temporal并不简单。让我们先澄清Temporal不是什么。Temporal并不是一个无代码工作流引擎。它也不是一个用来维护微服务的编排工具。它也不是Azure中的durable functions这样的通用工具。
Temporal 是一个高度 有强烈意见的开源平台,它可以帮助你通过 SDK 或底层 gRPC API 通过代码编写持久且分布式的任务流。如果你第一次听到 Temporal,这些信息或许仍然让你感到困惑。我们先来看看 Temporal 的架构。
Temporal架构时间由三个组件组成。
Temporal 服务
这是 Temporal 的核心。它负责存储您的工作流和活动的状态,安排任务并执行它们。Temporal 服务器是由 Go 语言编写并开源的。可以自行托管,或作为平台即服务(PaaS)在 Temporal 云上使用。
临时工作者
临时工作者是一个执行你编写的工作流和活动的程序。它与Temporal服务器通信,获取任务并报告结果。临时工作者使用你编写的工作流,就像运行时一样。
Temporal 客户端(Temporal Client)
Temporal 客户端是您启动工作流并与其互动的地方。这里就是您进行操作的地方。
客户端应用和工作者应用通常是用你所偏好的语言或平台开发的应用程序,在你选择的主机环境里运行,无论是云端还是本地。
功能性:Temporal的主要功能是运行工作流,但同时也支持一些不太常见的应用场景。
除了执行工作流之外,Temporal 还允许你向正在运行的工作流发送信号以通知它正在发生的重要事件。你还可以在必要时取消或终止整个工作流程。
Temporal 包含一个叫做查询(queries)的功能,这让你可以查看正在运行或已结束的工作流的状态。你可以查看每个步骤的数据。此外,它还支持定时工作流,并提供了一些针对特殊情况的功能。
工作流 vs 活动Temporal.IO 定义了两个关键概念:工作流和活动。工作流充当主蓝图,协调整个流程的顺序和逻辑,规定任务的顺序和逻辑,确保执行的可靠性和确定性。活动本质上就是这些具体任务,确保执行既可靠又确定。
工作流必须是这样的:
- 确定性的,
- 幂等性,
- 无副作用,
确定性、幂等性以及无副作用,这意味着工作流不能做如下事情:
- 执行 IO 操作(如网络、磁盘)
- 访问或修改外部可变状态
- 做任何线程操作
- 使用系统时钟(例如
DateTime.Now
)做任何事情 - 使用 .NET 计时器(例如
Task.Delay
或Thread.Sleep
) - 调用任何返回随机值的方法,例如
Guid.NewGuid()
所有这些限制意味着这些活动必须是重要的事情发生的地方,比如你的业务相关的逻辑。活动可以有副作用,是非确定的,并能保持状态。
这种有意区分开工作流和活动的概念并不是一个全新的想法。这里只是为了好玩,列举了三个类似的概念。
- 行为模型:工作流就像可以相互通信的角色,而活动就像是角色的方法。
- Saga 模式:工作流就像是可以协调多个活动的 Saga(编排)。
- 函数核心与命令式外壳:工作流就像应用程序的功能核心,而活动则是命令式外壳。
Temporal.io 提供了六款官方 SDK(详情见:这里),还包括三种非官方的 SDK。其中一个是用于 .NET 的 SDK,详情可以在这里找到:文档。
.NET的文档通常做得很好。然而,当我尝试在本地运行示例时遇到了一些困难,值得一提的是,相比Java和Go,.NET的示例较少。此外,与Java或Go不同,.NET SDK目前还没有提供基于项目的教程。
更有趣的是,你的代码中由于工作流必须是确定性的这种需求,因此带来了一些限制。
为什么确定的特性到底为啥重要呢?
时间流程必须确定,这意味着同样的输入会得到相同的结果,不论何时或重播多少次都不例外。
当一个工作流被启动时,Temporal 会记录每个决策和事件在一个事件历史中,其中包括执行任务、启动定时器和接收到信号等事件。这个事件历史就像是一个全面的日志,记录了工作流中所有发生的动作。
确定的意味着:
- 无副作用,
- 无非确定性表现
在编程中发现副作用通常很简单,比如IO操作。然而,非确定性行为带来了更大的挑战。例如,在工作流中,你不能使用像Task.Delay
或其它任务相关的方法。一个有趣的例子是,你可以使用Task.Factory.StartNew
,但不能使用Task.Run
。这种限制是因为微软实现这两种方法的方式有所区别。这稍微有点技术含量,但如果你感兴趣想了解更多,可以看看这个视频,它讨论了Temporal团队在构建.NET SDK时遇到的挑战。我建议你熟悉一下确保工作流确定性的要求,详情请参见此链接:这里。
注意:
因为常用的 Task.Delay
方法也被禁止,Temporal 团队决定为这些标准方法创建了包装方法。也就是说,为了确保延迟可以记录在工作流的事件历史中并能够被确定性地重放,你应该使用一个特殊的方法叫做 Workflow.DelayAsync
,而不是标准的 Task.Delay
方法。同样的,对于 Task.Factory.StartNew
或 Task.Run
,建议使用 Workflow.RunTaskAsync
进行替代。
因为 Temporal Server 是一个外部依赖,我想知道测试一下它有多简单。
活动很容易测试。它们通常是普通的类,所以没什么特别的;你只需要用单元测试或集成测试来覆盖它们就行了。
另一方面,工作流的集成测试可能相当有挑战性。Temporal 通过让你在本地 Temporal 服务器上运行测试来简化这一点。
// 启动本地开发服务
await using var env = await WorkflowEnvironment.StartLocalAsync();
// 创建一个工作者
using var worker = new TemporalWorker(
env.Client,
new TemporalWorkerOptions($"task-queue-{Guid.NewGuid()}").
AddWorkflow<SayHelloWorkflow>());
// 只在代码执行时运行工作者
await worker.ExecuteAsync(async () =>
{
// 执行工作流并检查结果
var result = await env.Client.ExecuteWorkflowAsync(
(SayHelloWorkflow wf) => wf.RunAsync("Temporal"),
new(id: $"wf-{Guid.NewGuid()}", taskQueue: worker.Options.TaskQueue!));
// 确保结果为
Assert.Equal("Hello, Temporal!", result);
});
耗时较长的工作流程(例如持续一小时或更长时间)的端到端(E2E)测试通常不容易测试。Temporal.IO 提供了时间快进功能,允许你在工作流程中跳过时间,从而更容易测试长时间运行的工作流程。这对集成测试尤其有帮助,甚至对一些端到端测试也有用,但要测试整个系统仍然不可行(通常通过冒烟测试执行)。
在这种情形下,我建议专注于可观察性方面,并设置警报以便在出现问题时及时通知您。好在,Temporal.io 提供了内置的可观察性功能,并可以通过其用户界面访问,同时提供了各种可抓取的指标,Prometheus等工具可以抓取并利用这些指标。
数据保密可重播性的一个常被忽视的方面是,传递给工作流的数据会被保存在你的工作流执行的事件历史中。有时,最好在数据进入Temporal服务之前加密它,然后在退出服务时解密它。默认情况下,Temporal并没有内置加密功能。你需要自己搞定加密这部分。更多详情请参阅这里。
之所以提到这一点,是因为你可能没有注意到活动中数据的双重性。
- 不属于活动输入或输出部分的变量是绝对安全的。Temporal不会记录您的本地变量。
- 另一方面,返回或向活动发送机密数据可能会引发安全和隐私问题,因为输入和输出数据以纯文本格式存储在Temporal日志中。所以请务必小心。
在更新现有工作流程时,你需要考虑很多因素:
- 可能同时运行多个任务进程。这意味着队列中的旧任务可能会被新版本的任务进程处理。
- 已经启动的任务是否应该继续使用旧版本的代码?
- 当任务升级到新版本时,以前执行(基于旧版本)的任务历史记录仍然存在。如果新版本尝试重新运行这段历史记录并遇到不匹配的情况,可能会引发错误。
我认为有两种主要策略来解决这些问题。
与旧版兼容的工作流程确保您的工作流和活动可以向后兼容。您可以使用一个方便的补丁方法。
if (Temporalio.Workflows.Workflow.Patched("my-patch-01"))
{
// 新版本的代码逻辑
}
else
{
// 旧版本的代码逻辑
}
Workflow.Patched("MyPatchId")
在部署了补丁之后,对于任何工作流程的执行都将返回 true 这个值。
当难以使所有流程和工作流程保持兼容旧版本时,你可以利用“任务队列系统”将任务路由到正确的版本号。在这种情况下,任务队列系统可以确保任务被正确地分配到相应的版本中。
var client = new WorkflowClient(…); // 客户端实例化
var workflow = client.NewWorkflowStub<IMyWorkflow>(
new WorkflowOptions { TaskQueue = "task-queue-v2" }); // 任务队列v2,原为task-queue-v1
await workflow.MyWorkflowMethod(); // 等待工作流方法
这允许你同时运行两个独立版本的工作流程。
另一种方式还有一种处理工作流变化的方法。对于短生存期的工作流,可以先停止旧工作进程,再在启动具有更新定义的新工作进程前。Temporal 通过其持久层简化了这一点:您可以停止所有工作进程并启动新的工作进程,新的工作流请求将被存储在 Temporal 数据库中,等待新工作进程开始处理。
那些老旧的长期运行的工作流程怎么办?
有些工作流可能运行数月之久,因此确保后向兼容性非常重要。否则,只能停掉旧的工作流,可能会留下不完整的成果,然后启动一个新工作流来处理这些数据。处理这些问题的方法多种多样。
记得,在开始管理工作流程时,问问自己这样的问题:“如果需求发生变化会怎么样?”之类的。
活动在 Temporal.io里 ,活动是工作流中的任务单元,代表 外部任务或操作。它们被设计用于处理实际的业务逻辑、计算或I/O任务,这些任务不能(或不应)直接在工作流中执行,因为工作流的限制,比如长时间运行或阻塞操作。
不过,活动可能会很贵。当Temporal开始一个活动时,会发生很多事:
- 活动已安排,
- 正在执行,
- 返回结果。
这一切都是通过网络调用和数据传输来实现的。为了减少这种网络调用和数据传输的开销,Temporal 引入了本地活动功能。
本地活动本地活动功能是一种在同一进程内运行活动的方式。这种方式更快且更经济。例如说,你可以使用本地活动功能来生成随机数或生成新的唯一标识符。
public class Guid生成器
{
[Activity]
public Task<Guid> 生成ID()
{
return Task.FromResult(Guid.NewGuid());
}
}
...
// 如果你的作业执行超时时间合理,这可以是可选的
var localActivityOptions = new 本地活动选项
{
调度至关闭超时时间 = TimeSpan.FromSeconds(5)
};
var id = await Workflow.ExecuteLocalActivityAsync(
(Guid生成器 activity) => activity.生成ID(),
localActivityOptions);
正如你所见,本地活动和普通活动是一样的,但你得使用 ExecuteLocalActivityAsync
方法来调用它。
Temporal 并不是万能的。它有自己的局限性和限制。其中最大的挑战之一便是它的复杂性。作为云原生解决方案中的一个,Temporal 力求遵循云原生架构的原则。然而,分布式系统可能会以多种方式失败,这使得调试和理解其内部工作变得相当困难。
如果你想要一个简单的流程引擎,Temporal可能不是你的最佳选择。Temporal真正擅长的是处理包含多个微服务相互通讯的分布式工作流,在这种情况下,多个微服务需要相互通讯。
好的一面是,Temporal.io 可以免费使用。它是一个开源平台,用户可以在 MIT 协议下免费下载、使用和托管 Temporal 的服务器和 SDK,这些操作均在 MIT 协议下进行。
更新:感谢 Chad Retz 的反馈,他是 Temporal.io SDK 的一位主要作者,我在本文中澄清了一些要点。Chad,感谢你的见解!
共同学习,写下你的评论
评论加载中...
作者其他优质文章