原文来源:https://cadenceworkflow.io/
大量的用例跨越了单一的请求-应答,需要跟踪复杂的状态,响应异步事件,并与外部不可靠的依赖项通信。构建此类应用程序的通常方法是将无状态服务、数据库、cron作业和队列系统等大杂烩在一起。这对开发人员的开发效率产生了负面影响,因为大多数代码都是专门用于管道的,在大量低级细节后面隐藏了实际的业务逻辑。这样的系统经常存在可用性问题,因为很难保持所有组件的健康。
Cadence解决方案是一种不考虑故障的有状态编程模型,它掩盖了构建可伸缩分布式应用程序的大部分复杂性。本质上,Cadence提供了一个持久的虚拟内存,它不链接到特定的进程,并在各种主机和软件故障中使用局部变量保存完整的应用程序状态,包括函数堆栈。这允许您使用编程语言的全部功能编写代码,而Cadence则负责应用程序的持久性、可用性和可伸缩性。
Cadence由编程框架(或客户端库)和托管服务(或后端)组成。该框架使开发人员能够用熟悉的语言编写和协调任务。
该框架使开发人员能够用熟悉的语言编写忽略错误的代码。
后端服务是无状态的,依赖于持久存储。目前支持Cassandra和MySQL存储。可以添加到提供多行单碎片事务的任何其他数据库的适配器。有不同的服务部署模型。在Uber,的团队运营着由数百个应用程序共享的多租户集群。
作为Cadence开发人员,我们面临一个非技术性的难题:如何定位和描述Cadence平台。
我们称之为工作流。但是当大多数人听到“工作流”这个词时,他们会想到低代码和ui。虽然这些可能对非技术用户有用,但它们给软件工程师带来的痛苦往往大于价值。对于“hello world”演示应用程序来说,大多数ui和低代码DSL非常棒,但是任何包含100多个元素或几千行JSON DSL的图表都是完全不实际的。因此,将Cadence定位为一个工作流并不理想,因为它会让那些喜欢只使用代码的开发人员望而却步。
我们称之为协调器。但这一术语相当狭隘,并拒绝了希望实现业务流程自动化解决方案的客户。
我们称之为耐用功能平台。从技术上讲,这是一个正确的术语。但微软生态系统之外的大多数开发人员从未听说过持久功能。
我们相信命名的问题来自这样一个事实:Cadence确实是一种编写分布式应用程序的新方法。它足够通用,可以应用于任何超出单个请求-回复的用例。它可以用于构建工作流或编排平台的传统领域中的应用程序。但对于传统上依赖于数据库和/或任务队列的多个用例来说,这也是开发人员生产力的巨大提升。
定时轮询,通常称为分布式cron,是指定期执行业务逻辑。对于这些场景,Cadence的优势在于它可以保证执行、复杂的错误处理、重试策略和对执行历史的可见性。
另一个重要的维度是规模。有些用例需要对大量的实体定期执行。在Uber,有些应用程序可以为每个客户创建定期工作流。假设有1亿多个并行cron作业不需要单独的批处理框架。
定时轮询通常是其他用例的一部分。例如,每月生成一次报告是定期的服务编排。或者是一个事件驱动的工作流,它为客户累积忠诚度积分,并每月应用一次这些积分。
有许多Cadence周期性执行的例子。例如:
Uber后端服务,每分钟为每个城市的每个十六进制重新计算各种统计数据。
每月优步业务报告生成。
一些业务流程通常是以多个微服务调用的形式实现的。并且实现必须保证所有调用最终都必须成功,即使出现长期的下游服务故障。在某些情况下,应该执行补偿回滚逻辑,而不是试图通过长时间重试来完成过程。Saga模式是补偿API标准化的一种方法。
Cadence非常适合这种情况。它保证工作流代码最终完成,内置了对无限指数活动重试的支持,并简化了补偿逻辑的编码。它还提供了对每个工作流状态的完全可见性,与基于队列的编排不同,在队列中几乎不可能获得每个单独请求的当前状态。
以下是一些基于Cadence的服务编排场景的真实示例:
使用Cadence工作流通过Banzai Cloud启动Kubernetes;
使用Uber的客户困扰票务流程和编排引擎改善用户体验
轮询正在执行定期操作,以检查状态更改。例如ping主机、调用restapi或为新上传的文件列出amazons3存储桶。
Cadence支持长时间运行的活动和无限制的重试,使它非常适合。
一些实际的用例:
网络、主机和服务监控;
正在处理上载到FTP或S3的文件;
轮询外部API以使特定资源可用。
许多应用程序监听多个事件源,更新相应业务实体的状态,并且在达到某种状态时必须执行操作。节奏是一个很好的适合其中许多。它直接支持异步事件(aka signals),有一个简单的编程模型,可以掩盖状态持久性的许多复杂性,并通过内置的重试确保外部操作的执行。
一些实际的用例:
由消费者行为检测生成的对欺诈事件作出反应的工作流;
客户忠诚度计划,其中工作流累积奖励点并在请求时应用。
在大量主机或数据库之间划分大型数据集,或者在amazons3存储桶中包含数十亿个文件,这是很常见的。Cadence是一个理想的解决方案,可以以可伸缩和弹性的方式实现此类数据的完整扫描。标准模式是运行一个活动(或多个并行活动,用于分区数据集)来执行扫描并将其进度返回到Cadence。在主机发生故障的情况下,将在其他主机上重试该活动,并从上次报告的进度继续执行。
一些实际的用例:
对所有工作流执行记录执行定期扫描的Cadence内部系统工作流。
许多批处理作业不是纯粹的数据操作程序。对于这些,现有的大数据框架是最合适的。但是,如果处理一个记录需要外部API调用,而这些调用可能会失败并且可能需要很长时间,那么Cadence可能会更好。
内部Uber客户使用Cadence生成月末报表。每个语句都需要调用多个微服务,有些语句可能非常大。之所以选择Cadence,是因为它为财务数据的持久性提供了严格的保证,并且能够无缝地处理长时间运行的操作、重试和间歇性故障。
在公共云中提供一个新的数据中心或一个机器池是一个潜在的长时间运行的操作,有很多可能出现间歇性故障。当需要调配和配置几十万甚至几十万的资源时,规模也是一个问题。对于资源调配场景,一个有用的特性是Cadence支持将活动执行路由到特定的进程或主机。
许多操作都需要某种类型的锁定,以确保一次对一个资源执行的变异不超过一个。Cadence通过业务ID提供了强大的唯一性保证。这可用于以容错和可伸缩的方式实现此类锁定行为。
一些实际的用例:
使用Cadence工作流启动Kubernetes,由Banzai Cloud提供;
在HashiCorp consur中使用Cadence编排集群生命周期。
实现CI/CD管道并将应用程序部署到容器、虚拟机或物理机是一个非常重要的过程。它的业务逻辑必须处理围绕滚动升级、金丝雀部署和回滚的复杂需求。Cadence是构建部署解决方案的完美平台,因为它提供了所有必要的保证和抽象,允许开发人员专注于业务逻辑。
一些实际的用例:
Uber内部部署基础设施;
更新推送至物联网设备。
假设您必须创建一个类似于amazonrds的自操作数据库。Cadence用于多个项目,这些项目自动管理和自动恢复各种产品,如MySQL、Elasticsearch和apache Cassandra。
这种系统通常是不同用例的混合体。他们需要使用轮询来监视资源的状态。它们必须对数据库的管理接口执行编排API调用。如果需要,他们必须提供新的硬件或Docker实例。他们需要推送配置更新,并定期执行备份等其他操作。
Cadence的性能和可伸缩性足以支持交互式应用程序。它可以用来跟踪UI会话状态,同时执行后台操作。例如,在下订单时,当后台任务评估客户是否存在欺诈行为时,客户可能需要经过几个屏幕。
Cadence支持直接用Java和Go等编程语言实现业务逻辑。但有些情况下,使用特定领域的语言更合适。或者可能有一个遗留系统使用某种形式的DSL来定义流程,但它在操作上不稳定且不可伸缩。这也适用于较新的系统,如apacheflow、各种BPMN引擎和AWS Step函数。
可以使用Cadence SDK编写解释DSL定义的应用程序。当在Cadence上运行时,它会自动变得高度容错、可伸缩和持久。Cadence已经被用来抨击一些Uber内部的DSL引擎。客户继续使用现有的流程定义,但Cadence被用作执行引擎。
在Cadence之上统一所有公司的工作流引擎有很多好处。最明显的一点是支持单个产品比支持多个产品更有效。它也很难击败Cadence的可伸缩性和稳定性,这是它带来的每一个集成。在某些情况下,共享引擎的能力可能会带来巨大的好处。
许多公司都构建了定制的ETL和ML培训和部署解决方案。Cadence非常适合用于此类应用的控制平面。
Cadence的一个重要特性是它能够将任务执行路由到特定的进程或主机。控制如何将ML模型和其他大文件分配给主机是很有用的。例如,如果一个ML模型是按城市划分的,那么请求应该被路由到包含相应城市模型的主机上。
Cadence是一种新的开发人员友好的分布式应用程序开发方法。它借用了工作流自动化领域的核心术语。所以它的概念包括工作流和活动。工作流可以对事件作出反应并通过查询返回内部状态。
部署拓扑解释了如何将所有这些概念映射到可部署的软件组件。
Cadence核心抽象是一个忽略故障的有状态工作流。工作流代码的状态(包括它创建的本地变量和线程)不受进程和Cadence服务失败的影响。这是一个非常强大的概念,因为它封装了状态、处理线程、持久计时器和事件处理程序。
Cadence核心抽象是一个忽略故障的有状态工作流。工作流代码的状态(包括它创建的本地变量和线程)不受进程和Cadence服务失败的影响。这是一个非常强大的概念,因为它封装了状态、处理线程、持久计时器和事件处理程序。
让我们看一个用例。客户注册了一个试用期的应用程序。期满后,如果客户没有取消,应每月收取一次续费。必须通过电子邮件通知客户有关费用,并应能够随时取消订阅。
这个用例的业务逻辑不是很复杂,可以用几十行代码来表达。但任何实际的实现都必须确保业务流程具有容错性和可伸缩性。设计这样一个系统有多种方法。
一种方法是以数据库为中心。应用程序进程将定期扫描数据库表以查找处于特定状态的客户,执行必要的操作,并更新状态以反映这一点。虽然可行,但这种方法有各种缺点。最明显的是客户状态的状态机很快变得极其复杂。例如,向信用卡充值或发送电子邮件可能会由于下游系统不可用而失败。失败的调用可能需要长时间重试,最好使用指数重试策略。应该对这些调用进行限制,以避免外部系统过载。如果单个客户记录因任何原因无法处理,则应支持毒丸,以避免阻塞整个流程。基于数据库的方法通常也存在性能问题。对于需要对处于特定状态的记录进行持续轮询的情况,数据库效率不高。
另一种常用的方法是使用计时器服务和队列。任何更新都被推送到队列中,然后从队列中使用的工作线程更新数据库,并可能在下游队列中推送更多消息。对于需要调度的操作,可以使用外部计时器服务。这种方法通常可以扩展得更好,因为数据库不会经常被轮询以获取更改。但是它使得编程模型更加复杂和容易出错,因为在队列系统和数据库之间通常没有事务更新。
使用Cadence,可以将整个逻辑封装在一个简单的持久函数中,该函数直接实现业务逻辑。因为函数是有状态的,所以实现者不需要使用任何额外的系统来确保持久性和容错性。
下面是实现订阅管理用例的示例工作流。它是在Java中的,但也支持Go。Python和.NET库正在进行活动开发。
public interface SubscriptionWorkflow {
@WorkflowMethod
void execute(String customerId);
}
public class SubscriptionWorkflowImpl implements SubscriptionWorkflow {
private final SubscriptionActivities activities =
Workflow.newActivityStub(SubscriptionActivities.class);
@Override
public void execute(String customerId) {
activities.sendWelcomeEmail(customerId);
try {
boolean trialPeriod = true;
while (true) {
Workflow.sleep(Duration.ofDays(30));
activities.chargeMonthlyFee(customerId);
if (trialPeriod) {
activities.sendEndOfTrialEmail(customerId);
trialPeriod = false;
} else {
activities.sendMonthlyChargeEmail(customerId);
}
}
} catch (CancellationException e) {
activities.processSubscriptionCancellation(customerId);
activities.sendSorryToSeeYouGoEmail(customerId);
}
}
}
再次注意,这段代码直接实现了业务逻辑。如果任何被调用的操作(aka活动)花费了很长时间,那么代码不会改变。如果下游处理服务停机那么长时间,可以在chargeMonthlyFee上阻塞一天。与阻止睡眠30天是工作流代码中的正常操作相同的方法。
Cadence对开放工作流实例的数量几乎没有可伸缩性限制。因此,即使你的网站拥有数亿消费者,上述代码也不会改变。
学习Cadence的开发人员经常问的问题是“如何处理工作流中的工作流工作进程失败/重新启动”?答案是你不要这样。工作流代码完全不受任何工人或甚至Cadence服务本身的故障和停机的影响。一旦它们被恢复,并且工作流需要处理某些事件(如计时器或活动完成),工作流的当前状态将完全恢复并继续执行。工作流失败的唯一原因是工作流业务代码引发异常,而不是基础架构中断。
另一个常见的问题是一个worker是否可以处理比其缓存大小或它可以支持的线程数更多的工作流实例。答案是,当工作流处于阻塞状态时,可以安全地从工作线程中删除。后来,当需要(以外部事件的形式)出现时,它可以在不同或相同的工人身上复活。因此,假设一个worker可以处理数百万个开放的工作流执行,假设它可以处理更新率。
工作流状态恢复利用了事件源,它对代码的编写方式施加了一些限制。主要的限制是工作流代码必须是确定性的,这意味着如果执行多次,它必须产生完全相同的结果。这就排除了来自工作流代码的任何外部API调用,因为外部调用可以间歇性地失败或随时更改其输出。这就是为什么所有与外部世界的交流都应该通过活动进行。出于同样的原因,工作流代码必须使用Cadence api来获取当前时间、睡眠和创建新线程。
工作流ID由客户端在启动工作流时分配。它通常是一个业务级别的ID,如客户ID或order ID。
Cadence保证在任何时候每个域都只能有一个具有给定ID的工作流(跨所有工作流类型)。尝试启动具有相同ID的工作流将失败,并出现WorkflowExecutionAlreadyStarted错误。
如果存在具有相同ID的已完成工作流,则尝试启动工作流取决于WorkflowIdReusePolicy选项:
AllowDuplicateFailedOnly意味着只有在以前执行的具有相同ID的工作流失败时,才允许启动工作流。
AllowDuplicate意味着它可以独立于以前的工作流完成状态启动。
RejectDuplicate表示根本不允许使用相同的工作流ID启动工作流执行。
默认值为AllowDuplicateFailedOnly。
为了区分具有相同工作流ID的多个工作流运行,Cadence使用两个ID标识工作流:工作流ID和运行ID。运行ID是服务分配的UUID。准确地说,任何工作流都是由三元组唯一标识的:域名、工作流ID和运行ID
工作流可以作为子工作流执行其他工作流:工作流:工作流:. 子工作流完成或失败将报告给其父工作流。
使用子工作流的一些原因是:
子工作流可以由不包含父工作流代码的一组单独的工作线程托管。因此,它将充当一个独立的服务,可以从多个其他工作流中调用。
单个工作流的大小有限。例如,它不能执行100k活动。子工作流可以用于将问题划分为更小的块。一个父母有1000个孩子,每个执行1000个活动是100万个已执行的活动。
子工作流可用于使用其ID管理某些资源,以确保唯一性。例如,管理主机升级的工作流可以为每个主机有一个子工作流(主机名是工作流ID),并使用它们来确保主机上的所有操作都已序列化。
子工作流可用于执行某些周期性逻辑,而不必放大父历史记录的大小。当父级启动子级时,它执行周期性的逻辑调用,该调用根据需要继续多次,然后完成。从父点if视图来看,它只是一个子工作流调用。
与在单个工作流中并置所有应用程序逻辑相比,子工作流的主要限制是缺少共享状态。父级和子级只能通过异步信号进行通信。但如果它们之间存在紧密耦合,那么使用单个工作流并仅依赖于共享对象状态可能会更简单。
我们建议从单个工作流实现开始,如果您的问题在已执行活动和已处理信号的数量方面有限制。它比多个异步通信工作流更直接
工作流代码不受基础结构级停机和故障的影响。但它仍然可能由于业务逻辑级别的故障而失败。例如,活动可能会由于超出重试间隔而失败,并且应用程序代码或工作流代码有错误而无法处理该错误。
有些工作流需要保证即使在出现此类故障的情况下也能保持运行。为了支持此类用例,可以在启动工作流时指定可选的指数重试策略。如果指定了它,则工作流失败将在计算的重试间隔之后从头重新启动工作流。以下是重试策略参数:
InitialInterval是第一次重试之前的延迟。
BackoffCoefficient。重试策略是指数型的。该系数指定重试间隔的增长速度。系数1表示重试间隔始终等于InitialInterval。
MaximumInterval指定重试之间的最大间隔。适用于系数大于1。
MaximumAttempts指定出现故障时尝试执行工作流的次数。如果超过此限制,则工作流将失败而不重试。如果指定了ExpirationInterval,则不需要。
ExpirationInterval指定在出现故障时尝试执行工作流的时间。如果超过此间隔,则工作流将失败而不重试。如果指定了MaximumAttempts,则不需要。
NonRetryableErrorReasons允许指定不应重试的错误。例如,在某些情况下,重试无效参数错误是没有意义的。
无故障无状态工作流代码是Cadence的核心抽象。但是,由于确定性的执行要求,它们不允许直接调用任何外部API。相反,它们协调活动的执行。在最简单的形式中,Cadence活动是受支持语言之一的函数或对象方法。Cadence无法在失败的情况下恢复活动状态。因此,活动函数可以不受限制地包含任何代码。
活动通过任务列表异步调用。任务列表本质上是一个队列,用于存储活动任务,直到它被可用的工作线程拾取为止。工作线程通过调用活动的实现函数来处理活动。当函数返回时,worker将结果报告给Cadence服务,后者反过来通知工作流完成情况。通过从不同的流程完成活动,可以完全异步地实现该活动。
Cadence不会对活动持续时间施加任何系统限制。由应用程序选择执行的超时。这些是可配置的活动超时:
ScheduleToStart是从工作流请求活动执行到工作人员开始执行的最长时间。触发此超时的通常原因是所有工作线程都已关闭或无法跟上请求速率。我们建议将此超时设置为工作流愿意在所有可能的工作进程中断的情况下等待活动执行的最长时间。
StartClose是活动被工作线程选中后可以执行的最长时间。
ScheduleToClose是从工作流请求执行活动到完成活动的最长时间。
Heartbeat是Heartbeat请求之间的最长时间。请参阅长时间运行的活动。
需要ScheduleToClose或ScheduleToStart和StartClose超时。
由于Cadence无法恢复活动的状态,而且它们可以与任何外部系统通信,因此会出现故障。因此,Cadence支持自动活动重试。当任何关联的策略都可以被调用时。以下是重试策略参数:
InitialInterval是第一次重试之前的延迟。
BackoffCoefficient。重试策略是指数型的。该系数指定重试间隔的增长速度。系数1表示重试间隔始终等于InitialInterval。
MaximumInterval指定重试之间的最大间隔。适用于系数大于1。
MaximumAttempts指定出现故障时尝试执行工作流的次数。如果超过此限制,则工作流将失败而不重试。如果指定了ExpirationInterval,则不需要。
ExpirationInterval指定在出现故障时尝试执行工作流的时间。如果超过此间隔,则工作流将失败而不重试。如果指定了MaximumAttempts,则不需要。
NonRetryableErrorReasons允许指定不应重试的错误。例如,在某些情况下,重试无效参数错误是没有意义的。
有些情况下,失败时不应重试单个活动,而是应重试整个工作流部分。例如,媒体编码工作流将文件下载到主机,对其进行处理,然后将结果上载回存储。在此工作流中,如果承载辅助进程的主机死亡,则应在其他主机上重试所有这三个活动。这样的重试应该由工作流代码处理,因为它们非常特定于用例。
对于长时间运行的活动,我们建议您指定相对较短的心跳超时和持续的心跳。这样,即使是运行很长时间的活动,也可以及时处理工作人员的故障。指定心跳超时的活动应定期从其实现中调用heartbeat方法。
心跳信号请求可以包括特定于应用程序的有效负载。这对于保存活动执行进度非常有用。如果某个活动由于错过心跳而超时,下一次尝试执行该活动时可以访问该进度并从该点继续执行。
作为特长竞选活动的领导者可以使用。节奏超时使用第二分辨率。因此,它不是实时应用程序的解决方案。但是如果在几秒钟内对流程失败做出反应是可以的,那么Cadence心跳活动就是一个很好的选择。
这种领导人选举的一个常见用例是监控。活动执行一个内部循环,该循环定期轮询某些API并检查某些条件。它也在每次迭代中心跳。如果条件满足,则活动将完成,这将允许其工作流处理它。如果活动工作线程死亡,则该活动在超过检测信号间隔后超时,并在其他工作线程上重试。同样的模式也适用于在amazons3存储桶中轮询新文件或REST或其他同步api中的响应。
工作流可以请求取消活动。目前,一项活动被取消的唯一途径是通过心跳。心跳信号请求失败,并出现一个特殊错误,指示活动已取消。然后由活动实现执行所有必要的清理并报告已完成。由工作流实现决定是要等待活动取消确认,还是不等待就继续。
活动心跳信号失败的另一个常见情况是,调用它的工作流处于已完成状态。在这种情况下,活动也将执行清理。
活动通过任务列表发送给工人。任务列表是工作人员监听的队列。任务列表是高度动态和轻量级的。它们不需要显式注册。每个工作进程有一个任务列表是可以的。通过单个任务列表调用多个活动类型是正常的。在某些情况下(如主机路由),在多个任务列表上调用相同的活动类型是正常的。
以下是在单个工作流中使用多个活动任务列表的一些用例:
流量控制。从任务列表中使用的工作线程仅在活动任务具有可用容量时才请求该活动任务。因此,请求峰值不会使工作人员过载。如果请求活动执行的速度比工作人员处理它们的速度快,则它们将被积压在任务列表中。
节流。每个活动工作线程都可以指定允许其处理任务列表中活动的最大速率。即使它有备用容量,也不会超过这个限制。还支持全局任务列表速率限制。此限制适用于给定任务列表的所有工作线程。它经常用于限制活动调用的下游服务的负载。
独立地部署一组活动。考虑一个承载活动的服务,它可以独立于其他活动和工作流进行部署。要将活动任务发送到此服务,需要一个单独的任务列表。
具有不同能力的工人。例如,GPU盒上的工人与非GPU盒上的工人。在这种情况下,有两个单独的任务列表允许工作流选择将执行请求发送到哪个活动。
将活动路由到特定主机。例如,在媒体编码的情况下,转换和上载活动必须与下载活动在同一主机上运行。
将活动路由到特定进程。例如,一些活动加载大型数据集并在过程中缓存它。依赖于此数据集的活动应路由到同一进程。
多重优先权。每个优先级一个任务列表,每个优先级有一个工作池。
版本控制。活动的新的向后不兼容实现可能使用不同的任务列表。
默认情况下,活动是函数或方法,具体取决于客户端库语言。函数一返回,活动就完成。但在某些情况下,活动实现是异步的。例如,它通过消息队列转发到外部系统。而回复来自另一个队列。
为了支持这样的用例,Cadence允许在活动函数完成时不完成的活动实现。在这种情况下,应该使用单独的API来完成活动。这个API可以从原始活动工作人员使用的任何进程调用,即使是在不同的编程语言中也是如此。
有些活动的生命周期很短,不需要查询语义、流量控制、速率限制和路由功能。对于这些节奏支持所谓的局部活动特征。本地活动与调用它们的工作流在同一工作进程中执行。考虑将本地活动用于以下功能:
不超过几秒钟。
不需要全局速率限制。
不需要路由到特定的工作线程或工作线程池。
可以在与调用它们的工作流相同的二进制文件中实现。
本地活动的主要好处是,与通常的活动调用相比,本地活动在利用Cadence服务资源方面效率更高,延迟开销也更低。
忽略故障的有状态工作流可以在外部事件时发出信号。信号始终是指向特定工作流实例的点对点。信号总是按照接收的顺序进行处理。
有多种情况下信号是有用的。
Cadence并不是ApacheFlink或ApacheSpark这样的通用流处理引擎的替代品。但在某些情况下,它更适合。例如,当所有应该聚合和关联的事件总是应用于具有清晰ID的某个业务实体时,然后当满足某个条件时,应该执行操作。
主要的限制是单个Cadence工作流的吞吐量非常有限,而工作流的数量实际上是无限的。因此,如果您需要聚合每个客户的事件,并且您的应用程序有1亿个客户,并且每个客户每秒生成的事件不超过20个,那么Cadence就可以正常工作了。但是,如果您想为美国客户聚合所有事件,那么这些事件的发生率将超出单个工作流的容量。
例如,物联网设备生成事件,某个事件序列指示应重新配置该设备。将为每个设备创建一个工作流实例,每个实例将管理设备的状态机,并在必要时执行重新配置活动。
另一个用例是客户忠诚度计划。每次客户进行购买时,都会在apachekafka中生成一个事件供下游系统处理。忠诚度服务Kafka消费者接收事件并使用Cadence signalWorkflowExecution API向客户发送有关购买的工作流。工作流将累积购买的计数。如果达到指定的阈值,工作流将执行一个活动,通知某些外部服务客户已达到下一级忠诚度计划。工作流还执行活动,定期向客户发送有关其当前状态的消息。
许多业务流程都涉及到人工参与者。实现外部交互的标准节奏模式是执行在外部系统中创建人工任务的活动。它可以是带有表单的电子邮件,或者外部数据库中的记录,或者移动应用程序通知。当用户更改任务的状态时,会向相应的工作流发送一个信号。例如,在提交表单或确认移动应用程序通知时。有些任务有多个可能的操作,如索赔、退货、完成、拒绝。因此可以发送与之相关的多个信号。
如果发生了一些外部事件,一些业务流程应该改变它们的行为。例如,在执行订单装运工作流时,项目数量的任何更改都可以以信号的形式传递。
另一个例子是服务部署工作流。当向Kubernetes集群推出新的软件版本时,发现了一些问题。在调查问题时,可以使用一个信号来请求工作流暂停。然后,可以使用continue或rollback信号来执行适当的操作。
Cadence工作流非常一致,因此可以将它们用作执行操作的同步点。例如,需要按顺序处理单个用户的所有消息,但底层消息传递基础结构可以并行地传递它们。Cadence的解决方案是为每个用户提供一个工作流,并在收到事件时向其发出信号。然后,工作流将缓冲内部数据结构中的所有信号,然后为接收到的每个信号调用一个活动。
工作流代码是有状态的,Cadence框架在各种软件和硬件故障时保持它。在工作流执行期间,状态会不断变化。为了向外部世界公开这种内部状态,Cadence提供了一个同步查询特性。从工作流实现者的角度来看,查询公开为外部实体调用的同步回调。每个工作流类型可以提供多个这样的回调,向不同的外部系统公开不同的信息。
要执行查询,外部客户机调用同步Cadence API,提供域、工作流ID、查询名称和可选的查询参数。
查询回调必须是只读的,不能以任何方式更改工作流状态。另一个限制是查询回调不能包含任何阻塞代码。以上两个限制都排除了从查询处理程序调用活动的能力。
Cadence团队目前正致力于实现更新特性,该特性在调用方式上类似于查询,但将支持工作流状态变异和本地活动调用。
Cadence客户机库公开了一些现成的预定义查询。目前唯一支持的内置查询是stack_trace。此查询返回所有工作流拥有的线程的堆栈。这是解决生产中任何工作流问题的好方法。
当工作流调用活动时,它将ScheduleActivityTask决策发送到Cadence服务。因此,服务更新工作流状态并将活动任务分派给实现该活动的工作人员。使用中间队列而不是直接调用工作进程。因此,服务将向该队列添加一个活动任务,工作线程将使用长轮询请求接收该任务。Cadence将用于分派活动任务的队列称为活动任务列表。
类似地,当工作流需要处理外部事件时,将创建一个决策任务。决策任务列表用于将其传递给工作流工作人员(也称为决策者)。
虽然Cadence任务列表是队列,但它们与常用的队列技术有一些不同。主要的一点是,它们不需要显式注册,而是按需创建的。任务列表的数量不受限制。一个常见的用例是为每个工作进程提供一个任务列表,并使用它向流程交付活动任务。另一个用例是每个工人池都有一个任务列表。
使用任务列表传递任务而不是通过同步RPC调用活动工作线程有多种优势:
工人不需要有任何开放的端口,这是更安全的。
Worker不需要通过DNS或任何其他网络发现机制来公布自己。
当所有工作线程都关闭时,消息将保存在任务列表中,等待工作进程恢复。
只有当消息有空闲容量时,工作线程才会轮询消息,因此消息不会过载。
跨大量工作人员进行自动负载平衡。
任务列表支持服务器端限制。这允许您将任务分派率限制为工作人员池,并且在峰值发生时仍然支持以更高的速率添加任务。
任务列表可用于将请求路由到特定的工作人员池,甚至是特定的进程。
Cadence全局域功能为客户端提供了在发生数据中心故障转移时从另一个群集继续执行工作流的能力。尽管可以将全局域配置为复制到任意数量的群集,但它仅在单个群集中被视为活动域。
Cadence引入了一个新的顶级实体Global Domains,它支持跨集群复制工作流执行。客户机应用程序需要运行工人轮询所有集群上的活动/决策任务。Cadence将只在当前活动集群上分派任务;备用集群上的工作线程将处于空闲状态,直到全局域发生故障转移。
因为Cadence是一个提供高度一致语义的服务,所以我们只允许在活动集群上发生StartWorkflowExecution、SignalWorkflowExecution等外部事件。全局域依赖于本地群集(local_Quorum)上的轻量级事务(paxos)来更新工作流执行状态,并创建异步应用的复制任务,以跨群集复制状态。如果应用程序在全局域处于待机模式的群集上进行这些API调用,Cadence将使用包含当前活动群集名称的DomainNotActivieError拒绝这些调用。应用程序负责将外部事件转发到当前处于活动状态的群集。
3.6.2.1、全局(IsGlobal)
此配置用于区分集群本地域和全局域。它控制在更新时创建复制任务,允许在集群之间复制状态。只有当域设置为只读时才可设置。
3.6.2.2、集群(Clusters)
域可以故障转移到的群集列表,包括当前活动群集。这也是一个只读设置,只能在设置域时设置。路线图上的重新复制特性将允许在将来更新此配置以添加/删除群集。
3.6.2.3、活动集群名称(Active Cluster Name)
全局域的当前活动群集的名称。每次全局域故障转移到另一个群集时,都会更新此配置。
3.6.2.4、文件版本(Failover Version)
唯一的故障转移版本,也表示全局域的当前活动群集。Cadence允许从任何集群触发故障转移,所以故障转移版本的设计方式是在两个集群上错误地同时触发故障转移时不允许发生冲突。
与只为活动执行提供一次语义的局部域不同,全局域只能支持至少一次语义。Cadence XDC依赖于跨集群的异步事件复制,因此在发生故障转移时,由于复制任务延迟,活动可能会在新的活动集群上再次被调度。这也意味着,无论何时在新群集进行故障转移后更新工作流执行,都无法应用该执行之前的任何复制任务。这会导致上一个活动集群中工作流执行所取得的某些进展丢失。在这种冲突解决过程中,Cadence在放弃复制任务之前,会将任何外部事件(如信号)重新注入到新的历史中。即使在故障转移期间某些进度可能会回滚,但Cadence可以保证工作流不会卡住,并将继续向前推进。
在主集群和备用集群上都允许所有可见性api。这使得Cadence Web能够无缝地为全局域工作,因为可以从域复制到的任何集群中查询工作流执行的所有可见性记录。即使全局域处于待机状态,直接调用Cadence可见性API的应用程序也将继续工作。但是,在从备用集群查询工作流执行状态时,由于复制延迟,它们可能会看到延迟。
Cadence CLI还可用于查询域配置或执行故障转移。