本指南提供详尽的 Temporal 工作流(Workflow)概览。

在日常交流中,术语 Workflow 经常代表工作流类型(Workflow Type)、工作流定义(Workflow Definition)或工作流执行(Workflow Execution)。Temporal 文档旨在明确及说明它们之间的区别。


工作流定义

工作流定义(Workflow Definition)是定义工作流执行(Workflow Execution)的约束的代码。

工作流定义也经常被称为工作流函数(Workflow Function)。在 Temporal 的文档中,工作流定义指代工作流执行(Workflow Execution)的实例的源,而工作流函数指代工作流函数执行(Workflow Function Execution)的实例的源。

工作流执行实际上执行一次直到完成,而在一个工作流执行的生命周期内,工作流函数执行可能发生多次。

我们强烈建议你使用有相应的 Temporal SDK 的语言写工作流定义。

确定性约束

开发工作流定义(Workflow Definition)的一个关键方面是确保它们表现出某些确定性特征 - 也就是说,确保每当相应的工作流函数执行(函数定义的实例)被重新执行时,以相同的序列发出相同的命令。

工作流执行的执行语义包括工作流函数的重新执行。在函数中,工作流 API 的使用产生命令(Commands)。命令告诉集群创建和添加哪些事件到工作流执行事件历史(Workflow Execution Event History)中。当工作流函数执行时,使用当前的事件历史与发出的命令进行比较。如果相应的事件已经以相同的顺序存在于映射到该命令的生成的事件历史中,并且该命令的某些特定元数据与事件的某些特定元数据匹配,那么函数执行(Function Execution)前进。

比如,使用 SDK 的 “Execute Activity” API 产生 ScheduleActivityTask 命令。在重新执行时,该 API 被调用,使用序列中相同位置上的事件与该命令进行比较。序列里的事件必须是 Activity 名称与命令中的相同的 ActivityTaskScheduled 事件。

如果生成的命令与当前事件历史中它需要匹配的事件不匹配,那么工作流执行返回非确定性(non-deterministic)错误。

下面是未按顺序生成命令或生成错误命令的两个原因:

  1. 改变正在被运行中的工作流执行使用的工作流定义的代码
  2. 本身有非确定性逻辑(比如内联的随机分支)
代码改动导致非确定性行为

对于工作流定义而言,一旦存在依赖它的工作流执行,那么改动它的方式非常有限。为减缓因代码变动而产生的非确定性问题,我们建议使用工作流版本(Workflow Versioning)。

比如,我们有一个定义如下序列的工作流定义:

  1. 启动并等待 Timer/sleep
  2. 生成并等待 Activity 执行(Activity Execution)
  3. 完成

我们启动 Worker,并使用上面的工作流定义生成工作流执行。Worker 会发出 StartTimer 命令,工作流执行会被挂起。

在 Timer 触发之前,我们将工作流定义改成如下序列:

  1. 生成并等待 Activity 执行
  2. 启动并等待 Timer/sleep
  3. 完成

当 Timer 触发时,下个工作流任务(Workflow Task)将导致工作流函数重新执行。Worker 看到的第一个命令是 ScheduleActivityTask 命令,它与期望的 TimerStarted 事件不匹配。

工作流执行会失败,并返回非确定性错误。

在下面的例子中,当重新执行已经包含事件(Event)的历史(History)时,不会导致非确定性错误:

固有的非确定性逻辑

固有的非确定机制是不管所有输入参数是否相同,在重新运行时,工作流函数执行(Workflow Function Execution)可能发出不同的命令(Command)序列。

比如,工作流定义不能有基于本地时间设置和随机数的内联逻辑分支(发出不同的命令序列)。在下面的伪代码中,local_clock() 函数返回本地时间,而非 Temporal 定义的时间。

所有 SDK 都提供使工作流定义能够拥有获取和使用时间、随机数及来自不可靠的数据源的数据的逻辑的 API。当使用这些 API 时,结果被当作事件历史(Event History)的一部分存储,这意味着即便有分支参与,被重新执行的工作流函数也将发布相同的命令序列。

换言之,不是纯粹地改变工作流执行的状态的所有操作都应该使用 Temporal SDK API。

工作流版本

工作流版本(Workflow Versioning)特性使在工作流定义内部,基于开发者指定的版本标识创建逻辑分支成为可能。当需要更新工作流定义的逻辑,但存在当前依赖它的运行中的工作流执行时,该特性很有用。处理工作流定义的不同版本的一种实用方式是不使用版本 API,而是在单独的任务队列(Task Queue)上,运行不同的版本。

处理不可靠的 Worker 进程

工作流定义(Workflow Definition)不处理 Worker 进程(Worker Process)的失败或重启。

工作流函数执行(Workflow Function Execution)完全注意不到 Worker 进程的失败或停机。Temporal 平台确保如果 Worker 进程或 Temporal 平台自己中断,那么工作流执行的状态会被恢复,进度重新启动。工作流执行可能失败的唯一原因应该是抛出错误或异常的代码,而不是因为底层的基础设施中断。

工作流类型

工作流类型(Workflow Type)是映射到工作流定义的名字。

workflow-type-cardinality.svg


工作流执行

Temporal 工作流执行(Workflow Execution)是持久的、可靠的、可扩展的函数执行。它是 Temporal 应用程序的主要执行单元。

每个 Temporal 工作流执行对它的本地状态拥有独占访问权限。所有工作流执行并发地执行。工作流执行之间通过 Signals 进行通信,与外界通过 Activities 进行通信。Temporal 应用程序可包含数百万到数十亿的工作流执行,但单个工作流执行有大小和吞吐量限制。

持久性

持久性是指没有强制的时间限制。

工作流执行是持久的,因为它执行 Temporal 工作流定义(Workflow Definition,也被称为 Temporal 工作流函数),你的应用程序代码一次,直到完成 - 无论你的代码执行数秒还是数年。

可靠性

可靠性是指对故障的响应能力。

工作流执行是可靠的,因为它能在故障后彻底地恢复。Temporal 平台确保工作流执行的状态在出现故障和运行中断时持续存在,并从最新状态开始重新执行。

扩展性

扩展性是指对负载的响应能力。

单个工作流执行有大小和吞吐量限制,但是可扩展,因为通过 Continue-As-New,可以降低负载。Temporal 应用程序是可扩展的,因为 Temporal 平台有能力支持数百万到数亿的工作流执行并发地执行,这是通过 Temporal ClusterWorker Processes 的设计和天然特性实现的。

命令 & 可等待对象

工作流执行做如下两件事:

workflow-execution-progession-simple.svg

在工作流定义(Workflow Definition)中使用 Workflow API 可以发布命令及提供可等待对象。

执行工作流函数会生成命令。工作进程(Worker Process)管理命令生成,并确保它映射到当前事件历史(Event History)。(如果想获取更多信息,请查看 Deterministic constraints。)每当工作流函数到达如果没有来自可等待对象的结果,那么它不能再前进的地方时,工作流执行分批处理命令,然后挂起进度,将命令发送到集群。

工作流执行只能在 Temporal SDK API 提供的可等待对象(Awaitable)上阻塞进度。

如下 API 提供可等待对象:

状态

工作流执行可以是打开的(Open)或关闭的(Closed)。

workflow-execution-statuses.svg

Open

Closed

Closed 状态意味着工作流执行无法取得进一步进展,原因如下:

工作流执行链

工作流执行链(Workflow Execution Chain)是共享相同 Workflow Id 的工作流执行序列。链中的每个节通常被称为工作流运行(Workflow Run)。序列中的每个工作流运行由以下之一连接:

工作流执行由它的 NamespaceWorkflow IdRun Id 唯一地标识。

工作流执行超时(Workflow Execution Timeout)应用到工作流执行链。工作流运行超时(Workflow Run Timeout)应用到单个工作流执行。

事件循环

工作流执行由被称为事件历史(Event History)的事件(Events)序列组成。事件由 Temporal 集群创建,以响应命令或由 Temporal Client 请求的操作(比如生成工作流执行的请求)。

workflow-execution-swim-lane-01.svg

时间约束

工作流能运行多久有限制吗?

没有,没有关于工作流执行能运行多久的时间限制。

但是,编写准备无限运行的工作流执行应该多加注意。Temporal 集群存储工作流执行的整个生命周期的完整事件历史。在工作流执行事件历史(Workflow Execution Event History)中,存在 50,000 个事件的硬性限制,以及在大小方面有 50MB 的硬性限制。Temporal 集群在每 10,000 个事件,记录一次警告。当事件历史达到 50,000 个事件或 50MB 大小限制时,工作流执行会被强制终止。

为防止工作流执行失控,可以使用工作流执行超时(Workflow Execution Timeout)、工作流运行超时(Workflow Run Timeout),或都使用。工作流执行超时用于限制工作流执行链(Workflow Execution Chain)的持续时间,工作流运行超时用于限制单个工作流执行的持续时间。

你可以使用 Continue-As-New 特性以单个原子操作的方式,关闭当前工作流执行,创建新工作流执行。从 Continue-As-New 生成的工作流执行拥有相同的 Workflow Id、新 Run Id 和新事件历史,并被传递所有相应的参数。比如,对于产生庞大的事件历史的长期运行(long-running)的工作流执行而言,可以每天使用一次 Continue-As-New。

命令

命令(Command)是由 WorkerWorkflow Task Execution 完成后,向 Temporal Cluster 发布的请求操作。

集群取到的操作被当作 Event 记录在工作流执行的事件历史中。工作流执行可以在作为某些命令的结果的某些事件上等待。

工作流 API 的使用会生成命令。在工作流任务执行(Workflow Task Execution)期间,可能生成许多命令。在工作流任务(Workflow Task)已尽其所能地使用工作流函数(Workflow Function)后,命令被分批处理,并作为工作流任务执行(Workflow Task Execution)完成结果的一部分被传递到集群。当存在工作流任务执行完成请求时,事件历史中一定存在 WorkflowTaskStartedWorkflowTaskCompleted 事件。

commands.svg

命令的描述在 Command reference,定义在 Temporal gRPC API

事件

事件(Event)由 Temporal 集群创建,以响应外部故障和工作流执行(Workflow Execution)生成的命令。每个事件对应于 Server API 中定义的一个 enum

所有事件被记录在事件历史(Event History)中。

Event reference 提供可能出现在工作流执行事件历史(Workflow Execution Event History)中的所有可能事件的列表。

事件历史

用于应用程序的事件(Events)的追加日志(append-log):

事件历史限制

Temporal 集群为工作流的整个生命周期存储完整的事件历史。工作流执行事件历史(Workflow Execution Event History)有 50,000 个事件的硬性限制,以及在大小方面有 50MB 的硬性限制。Temporal 集群在每 10,000 个事件记录一次警告。当事件历史达到 50,000 个事件或 50MB 的大小限制,工作流执行被强制地终止。

Continue-As-New

通过 Continue-As-New 机制,最近的相关状态被传递给拥有新事件历史的新工作流。

作为预防性措施,Temporal 平台将全部事件历史(Event History)限制到 50,000 个事件或 50MB,并且在每 10,000 个事件或 10MB,警告你。为防止工作流执行事件历史(Workflow Execution Event History)超过该限制,可使用 Continue-As-New 启动一个拥有新事件历史的工作流执行。

通过参数传递给工作流执行或通过结果值返回的所有值被记录到事件历史中。Temporal 集群存储命名空间(Namespace)保留周期期间的工作流执行的全部事件历史。周期性地执行许多 Activity 的工作流执行可能命中大小限制。

非常庞大的事件历史可能会对工作流执行的性能产生不利影响。比如,在 Workflow Worker 失败的情况下,必须从 Temporal 集群拉取全部事件历史,并通过 Workflow Task 给到另外一个 Worker。如果事件历史非常大,加载它可能会花费一些时间。

Continue-As-New 特性使开发者能够以原子性操作的方式,完成当前工作流执行,然后启动一个新的。

新工作流执行拥有相同的 Workflow Id,不同的 Run Id,拥有它自己的事件历史。

Temporal Cron Jobs 情况下,Continue-As-New 实际上产生相同的效果。

Run Id

Run Id 是工作流执行(Workflow Execution)的全局唯一、平台级的标识符。

Temporal 保证在任意给定时刻,具有给定的 Workflow Id 的工作流执行仅有一个可处于 Open 状态。但当工作流执行到达 Closed 状态时,可能有另一个具有相同 Workflow Id 的工作流执行处于 Open 状态。比如,Temporal Cron Job 是具有相同 Workflow Id 的工作流执行的链。链中的每个工作流执行被认为是一个运行(Run)。

Run Id 唯一地标识工作流执行,即使它与其它工作流执行共享 Workflow Id。

Workflow Id

Workflow Id 是工作流执行(Workflow Execution)的自定义的、应用级的标识符,对于命名空间(Namespace)里打开的(Open)工作流执行而言,它是唯一的。

Workflow Id 可以是业务流程标识,比如客户标识或订单标识。

可以使用 Workflow Id 重用策略(Workflow Id Reuse Policy)来管理 Workflow Id 能否被重用。Temporal 平台基于 Workflow Id 重用策略保证命名空间(Namespace)中 Workflow Id 的唯一性。

无论 Workflow Id 重用策略如何,不能使用与另一个打开的(Open)工作流执行相同的 Workflow Id 生成新工作流执行。使用与当前打开的工作流执行的 Id 相同的 Workflow Id 生成工作流执行的尝试会导致 Workflow execution already started 错误。

工作流执行由它的命名空间(Namespace)、Workflow Id 和 Run Id(Run Id)在所有命名空间中唯一地标识。

Workflow Id 重用策略

Workflow Id 重用策略(Workflow Id Reuse Policy )决定是否允许使用已被以前的、但现在已关闭的工作流执行使用过的 Workflow Id 生成工作流执行。

Workflow Id 重用策略有三个可能值:

仅当拥有相同的 Workflow Id 的已关闭工作流执行(Closed Workflow Execution)存在于相关的命名空间的保留周期内时,Workflow Id 重用策略才适用。比如,如果命名空间的保留周期是 30 天,那么 Workflow Id 重用策略仅会比较正在生成的工作流执行与最近 30 天的已关闭工作流执行的 Workflow Id 。

生成工作流执行时,如果使用不允许它生成的 Workflow Id 重用策略,那么服务器会阻止工作流执行的生成。

工作流执行超时

工作流执行超时(Workflow Execution Timeout)是工作流执行可以执行(拥有 Open 状态)的最大时间,包含重试和 Continue As New 的使用。

workflow-execution-timeout.svg

默认值是 ∞(无穷大)。如果达到该超时,工作流执行改变到超时(Timed Out)状态。该超时不同于工作流运行超时(Workflow Run Timeout)。该超时常被用于在经过一段时间后,停止 Temporal Cron Job 的执行。

工作流运行超时

工作流运行超时(Workflow Run Timeout)是单个工作流运行(Workflow Run)被限制到的最大时间。

workflow-run-timeout.svg

默认被设置为与工作流执行超时(Workflow Execution Timeout)相同的值。该超时常被用于限制单个 Temporal Cron Job Execution 的执行时间。

如果达到工作流运行超时,工作流执行将被终止。

工作流任务超时

工作流任务超时(Workflow Task Timeout)是 Worker 从任务队列(Task Queue)中拉到工作流任务(Workflow Task)后,允许 Worker 执行工作流任务的最大时间。

workflow-task-timeout.svg

默认值是 10 秒。该超时主要用来识别 Worker 是否宕机,以便在不同的 Worker 上恢复工作流执行。增加默认值的主要原因是适应拥有非常庞大的工作流执行历史的工作流执行,Worker 加载这样的工作流执行可能花费超过 10 秒的时间。

实现指南:


信号

信号(Signal)是发送给工作流执行(Workflow Execution)的异步请求。

信号向正在运行工作流执行传递数据。它不能向调用者返回数据;如果想这样做,请使用 Query。处理信号的工作流代码可以转换工作流状态。可以从 Temporal 客户端或工作流发送信号。当信号被发送时,它被集群接收,并被当作事件(Event)记录到工作流执行事件历史(Workflow Execution Event History)中。来自集群的成功响应代表信号已被持久化,将至少被传递到工作流执行一次。下一次调度的工作流任务(Workflow Task)将包含信号事件。

信号必须包含目的地(命名空间和 Workflow Id)和名称。它可以包含参数列表。

信号处理器是通过信号名称监听信号的工作流函数。信号被按照它们被集群接收到的顺序传递。对于工作流而言,一个信号的多次投递是棘手的问题,需要向信号处理器中添加检查重复的幂等(idempotency)逻辑。


查询

查询(Query)是用于获取工作流执行(Workflow Execution)的状态的同步操作。正在运行的工作流的状态会不断地改变。查询可以向外部世界暴漏内部的工作流执行状态。

查询被从 Temporal 客户端发送到工作流执行。API 调用是同步的。两端通过查询名称标识查询。工作流必须拥有用于处理查询及提供代表工作流执行的状态的数据的查询处理器。

查询是强一致的,并被确保返回最近的状态。这意味着(信号处理器返回的)数据反应在查询被发送之前进入的所有已确认事件的状态。如果创建事件的调用返回成功,那么认为事件已被确认。在查询未完成时创建的事件可能,也可能不反应在基于查询结果的工作流状态中。

查询可以携带参数,来指定它所请求的数据。每个工作流可以向多种类型的查询暴漏数据。

查询一定不能改变工作流的状态 - 也就是说,查询是只读的(read-only),不能包含任何阻塞代码。这意味着,比如,处理逻辑的查询不能调度 Activity 执行(Activity Execution)。

支持向已完成的工作流执行发送查询,但是每个查询可以配置查询拒绝条件。

栈追踪查询

在许多 SDK 中,Temporal SDK 暴漏预定义的 __stack_trace 查询,它返回工作流执行拥有的所有线程的栈追踪。这是调试生产环境中的工作流执行的一种很好的方式。比如,如果工作流长期阻塞在某个状态,你可以发送 __stack_trace 查询,它返回当前调用栈。__stack_trace 查询名称无需在代码中特殊处理。


子工作流

子工作流执行是从另一个工作流内生成的工作流执行(Workflow Execution)。

因为任何工作流都可以生成另一个工作流,所以工作流执行既可以是父(Parent),也可以是子工作流执行(Child Workflow Execution)。

parent-child-workflow-execution-relationship.svg

父工作流执行必须等待到子工作流执行生成。父可以选择性地等待子工作流执行的结果。如果父不等待子的结果,考虑使用子的父关闭策略(Parent Close Policy),它包含父对 Continue-As-New 的任何使用。

当父工作流执行到达关闭(Closed)状态时,集群根据子的父关闭策略向子工作流执行传播取消请求或终止。

如果子工作流执行使用 Continue-As-New,从父工作流执行的视角来看,运行(Run)的整个链被当作单个执行。

parent-child-workflow-execution-with-continue-as-new.svg

何时使用子工作流

考虑工作流执行事件历史(Workflow Execution Event History)大小限制

单独的工作流执行有事件历史(Event History)大小限制,这是使用子工作流的一些考虑。

一方面,因为子工作流执行有它们自己的事件历史,通常使用它们将较大的工作负载切分成较小的块。比如,在单个工作流执行的事件历史(Event History)中,没有足够的空间来生成 100,000 个 Activity 执行(Activity Executions)。但是父工作流执行可以生成 1,000 个子工作流执行,每个子工作流执行生成 1,000 个 Activity 执行,来实现总共 1,000,000 个 Activity 执行。

另外一方面,因为父工作流执行事件历史包含与子工作流执行的状态相关的事件(Events),所以单个父不应该生成超过 1,000 个子工作流执行。

通常,子工作流执行比 Activity 导致更多的事件被记录到事件历史中。因为事件历史中的每个条目(entry)都消耗计算资源,所以在非常大的工作负载中,这可能成为一个不确定性因素。因此,我们推荐在对子工作流有明确的需求之前,启动使用 Activity 的单个工作流实现。

考虑子工作流执行作为单独的服务

因为子工作流执行可被父工作流执行(Parent Workflow Execution)外部的完全独立的 Workers 集处理,所以它能充当完全独立的服务。但这也意味着父工作流执行和子工作流执行无法共享任何本地状态。与所有工作流执行一样,它们之间只能通过信号(Signals)进行通信。

考虑单个子工作流执行可以代表单个资源

像所有工作流执行一样,子工作流执行可以创建与资源之间的一对一映射。比如,管理主机升级的工作流应该给每个主机生成一个子工作流执行。

父关闭策略

父关闭策略( Parent Close Policy)决定如果子工作流执行(Child Workflow Execution)的父改变到关闭(Closed)状态(已完成、已失败或超时),子工作流执行会发生什么。

有三个可能的值:

ParentClosePolicy proto 定义。

每个子工作流执行可以有它自己的父关闭策略(Parent Close Policy)。该策略仅应用到子工作流执行,无其它影响。

parent-close-policy.svg

可以按照子设置策略,这意味着你可以基于每个子选择不传播终止/取消。对于异步地启动子工作流而言,这很用(查看 relevant issue here 或相应的 SDK 文档)。


Temporal Cron Job

Temporal Cron Job 是调用中提供的 Cron 调度(Schedule)生成的一系列工作流执行。

temporal-cron-job.svg

Temporal Cron Job 与经典 unix cron job 类似。正如 unix cron job 接收命令和调度,基于调度执行命令一样。可以给 Cron 调度提供生成工作流执行的调用。如果提供 Cron 调度,Temporal 服务在每次调度为相关的工作流类型生成一个执行。

系列内的每个工作流执行被认为是一个运行( Run)。

Temporal 服务立刻生成运行链中的第一个工作流执行。但它计算及应用一个退避(backoff)(firstWorkflowTaskBackoff),使该工作流执行的第一个工作流任务(Workflow Task)在调度时间达到之前,不会被放进任务队列(Task Queue)。在每个运行完成、失败或到达 Workflow Run Timeout 后,相同的事情发生:带有基于当前服务器时间和 Cron 调度定义计算来的新 firstWorkflowTaskBackoff 的下一个运行被立刻创建。

Temporal 服务仅在当前运行(Run)完成、失败或到达工作流运行超时(Workflow Run Timeout)后,才生成下一个运行。这意味着,如果提供重试策略(Retry Policy),并且运行失败或达到工作流运行超时,那么按照重试策略重试该运行,直到运行完成或重试策略被用光。如果当前运行仍处于打开(Open)状态(包括重试),按照 Cron 调度,应该生成下一次运行,那么服务器会自动地在当前运行成功完成后,启动新运行。使用该新运行的开始时间和 Cron 定义计算应用到新运行的 firstWorkflowTaskBackoff

Cron 调度运行到到达工作流执行超时(Workflow Execution Timeout)或终止工作流。

temporal-cron-job-failure-with-retry.svg

Cron 调度

默认情况下,用 UTC 时间解释 Cron 调度。

以字符串的形式,提供 Cron 调度,并且必须符合两种规范中的一个:

经典规范

经典规范看起来像:

比如,15 8 * * * 导致 UTC 时间的每天上午 8 点 15 生成一个工作流执行。使用 crontab guru site 测试你的 cron 表达式。

robfig 与定义的调度和间隔

你也可以传递 robfig/cron documentation 中描述的任意预定义的调度(predefined schedules)或间隔(intervals)。

比如,“@weekly” 导致每周的周六到周日之间的午夜生成一次工作流执行。

间隔(interval)只带一个能被 time.ParseDuration 接收的字符串。

时区

该特性仅应用在 Temporal 1.15 及更高版本

可以通过在规范前面加上 CRON_TZ=America/New_York (或者是你期望的来自于 tz 的时区)的方式,改变解释 Cron 调度的时区。CRON_TZ=America/New_York 15 8 * * * 在纽约时间的每天上午 8 点 15 生成一个工作流执行。

在生产环境使用时区会引入大量的复杂性和故障模式。我们建议尽可能用 UTC(默认)指定 Cron 调度。

如果想了解更多关于时区的信息,请查看官方文档

如何停止 Temporal Cron Job

Temporal Cron Job 在被终止或到达工作流执行超时(Workflow Execution Timeout)之前,不会停止生成运行(Run)。

取消请求仅影响当前运行。

在任意请求中使用 Workflow Id 来取消或终止。

实现指南:


调度(Schedule)

调度工作流(Scheduled Workflow)特性在 Temporal Server 版本 1.17 中可用。但该特性处于试验性阶段,默认不开启。

如果想了解更多关于调度的信息,请查看官方文档