作者:lancetang
(一文带你读懂为微服务编排而生的工作流引擎-Zeebe,文末有福利)
提到工作流,印象里都是OA系统各种请假审批流。事实上,广义上的工作流是对工作流程及其各操作步骤之间业务规则的抽象、概括、描述。简单理解,我们为了实现某个业务目标,抽象拆解出来的一系列步骤及这些步骤之间的协作关系,就是工作流。例如订单发货流、程序构建流等。业界通常用BPMN流程图来描述一个工作流。
以实现一个用户购买逻辑为例,如果不应用工作流模型,我们串联多个任务(步骤)一般是通过显示的代码调用:
校验、支付、发货一气呵成,流畅自然。正喝着枸杞红枣,产品一脸笑意跑过来:“我们新搞个充会员卡的业务,大概步骤就校验 -> 推荐 -> 支付 -> 充值。校验和支付前面都做过了,应该很快实现吧?”
精通if-else的你,听完的瞬间就已经构思好了代码:
一通写下来,总感觉哪里不对,“为什么加新的任务节点,要改已有的代码呢?这不符合开闭原则啊!”。
工作流模型正是为了解决这类问题而生:分离任务的实现和任务的协作关系。上面同样的用户购物逻辑,有了工作流模型,各个任务只实现自己原子的逻辑,任务协作关系使用流程图来表达。
当新的逻辑需要复用已有任务节点时,只需要要调整流程图,无需修改已有代码。
将任务实现与任务协作关系分离之后,就诞生了专门维护任务协作关系的程序 - 工作流引擎(也常称作流程引擎)。
其中最具有代表性的就非Activiti莫属了。在企业应用蓬勃发展的21世纪初,它几乎是实现流程自动化的标配。关于Activiti的介绍,网上已经有足够多的文章。今天我们要介绍是由Activiti的核心成员打造的另一款专为微服务编排而生的工作流引擎 - Zeebe。在开始之前,我们先理解下什么是微服务编排。
微服务架构的一大核心是把大的复杂的业务系统拆分成高内聚的微服务,每个服务负责相对独立的逻辑。例如一个电商系统,可能会拆分出支付微服务、订单微服务、仓储微服务、物流微服务等。服务拆分的好处无需赘述,但是要实现业务价值,不是看单个服务的能力,而是要协调所有服务保证企业端到端业务流的成功。那么,哪个服务来负责端到端业务流的成功呢?答案是没有。事实上,在公司内,端到端的业务流可能都没有正式的文档说明,从一个微服务到另一个微服务的事件流转都是在代码里隐式表达的。
很多微服务架构依赖一种相对纯粹的编舞模式(choreography pattern)来解决这个问题。在这种模式下,微服务通过向一个消息队列发送和接收事件来相互协作。编舞模式给开发者提供了很高的灵活度,但是编舞模式仍不能解决:
按照我们前面对工作流模型的阐述,工作流引擎很适合作为中控引擎,来编排调度微服务。那为什么诸如Activiti等传统的工作流引擎没能继续占领微服务编排的市场,而是诞生了新的微服务编排引擎-Zeebe?更有趣的是,Zeebe的核心开发,也是来自最初的Activiti团队。
答案是诸如Activiti等传统工作流引擎的架构无法适应当下微服务的场景:
Zeebe在设计之初,就考虑到了这些问题。
Zeebe是专为微服务编排设计的免费开源的工作流引擎,它提供了:
Zeebe架构主要包含4大组件:client, gateway, brokers 以及 exporters
客户端向Zeebe发送指令:
客户端程序可以完全独立于Zeebe扩缩容 - Zeebe brokers不执行任何业务逻辑。
客户端是嵌入到应用程序(执行业务逻辑的微服务)的库,用于跟Zeebe集群连接通信。
客户端通过基于HTTP/2协议的gRPC与Zeebe gateway连接。
Zeebe官方提供了Java和Go客户端。社区提供了C#,Ruby,JavaScript客户端实现。gRPC协议很方便生成其他语言的客户端。
Client中,执行单独任务的单元叫JobWorker。
Gateway作为Zeebe集群的入口,转发请求到brokers。Gateway是无状态(stateless)无会话(sessionless)的,可以按需增加节点,以负载均衡及高可用。
Broker是分布式的流程引擎,维护运行中流程实例的状态。Brokers可以分区以实现横向扩容、副本以实现容错。通常情况下,Zeebe集群都不止一个节点。
需要重点强调的是,broker不包含任何业务逻辑,它只负责:
Brokes形成一个对等网络(peer-to-peer),这样集群不会有单点故障。集群中所有节点都承担相同的职责,所以一个节点不可用后,节点的任务会被透明的重新分配到网络中其他节点。
exporter系统提供Zeebe内状态变化的事件流。这些事件流数据有很多潜在用处,包括但不限于:
exporter提供了简洁的API,可以流式导出数据到任何存储系统。Zeebe官方提供开箱即用的Elasticsearch exporter,社区也提供了其他exporters。
Zeebe能做到高吞吐、高可用的微服务编排,得益于三个关键实现:
Zeebe设计之初就考虑了分布式部署,可以在不依赖外部组件的情况下,搭建一个zeebe broker集群,集群中节点组成一个对等的网络(peer-to-peer network)。在网络中,所有的节点都有相同的职责,保整集群不会有单点故障。
Zeebe内部抽象了一个只追加写的队列(可以类比理解成kafka的topic),来处理和存储数据。当集群有多个broker节点时,会将队列划分成多个分区(partitions,或者分片shards),分布到各个节点上。每个分区有多个副本(replicas)。在所有的副本中,会根据raft协议选出一个leader,leader负责接收请求和执行所有处理逻辑。其他broker上的副本就是被动的跟随者(passive followers)。当leader不可用时,followers会透明地选出新的leader。
Zeebe消息驱动架构,体现在两个方面:
Zeebe内部实现,其实就是一系列作用在记录流(record streams)上的流处理器(stream processors)。流处理模型作为一个统一的实现方式,提供:
状态机(state machines)
Zeebe管理有状态的实体:任务、工作流实例等。在内部,这些实体实现为流处理器管理的状态机。状态机模式的概念很简单。一个状态机的实例总是处于某个逻辑状态。对于每一个状态,一系列转换(transitions)定义了下一步可能的状态。转换到下一个新状态可能产生输出或者副作用(side effects)。
如下图是任务(jobs)的状态机:
椭圆代表状态,箭头代表状态转换。要注意的是,状态转换只能用在特定的状态上。比如,不能在任务处于CREATED状态的时候完成(complete)任务。
事件和指令(events and commands)
状态机里的每个状态变化被称为事件(event)。Zeebe会把每个事件当成一条记录发布到流上。状态变化可以通过发送指令触发。Zeebe broker从两个源头接收指令:
指令一旦接收到,就会被当做记录写到流里。
有状态的流处理(stateful stream processing)
流处理器从流里有序地读取记录,然后根据记录关联的实体的生命周期,解析指令。流处理器循环的执行下面的步骤:
1.从流里消费指令(command)
2.根据状态生命周期和实体当前状态,判断指令是否适用
3.如果指令适用,应用到状态机。如果指令是客户端发送的,发送回响应。
4.如果指令不适用,拒绝。如果是客户端发过来的,发送错误响应信息。
5.发布新的事件,报告实体新的状态。
例如:处理Create Job指令,会产生Job Created事件。
指令触发器(command triggers)
一个实体的状态变化可以自动触发针对另一个实体的指令。例如:当一个任务完成了,相应的流程实例应该继续后续的任务,也就是说,Job Completed事件触发了Complete Activity指令。
处理背压(handling back-pressure)
当broker收到客户端请求,会先把请求写到事件流里,然后交由流处理器处理。如果处理太慢或者流里面堆积了太多客户端请求,处理器可能需要花很长时间才能处理新接收到的请求指令。如果broker继续从客户端接收新请求,待处理的任务(back log)会不断增加,任务处理延时会超过可以接收的时间。为了避免这种问题,Zeebe采用了一种背压机制。当broker接收到的请求超过其能在一定延时范围内处理的限度,broker就会拒绝(reject)一些请求。
broker能处理的最大请求速率取决于机器的处理能力、网络延时、系统的当前负载等。因此,Zeebe不会配置固定的最大请求限度。相反的,Zeebe采用自适应的算法动态的决定inflight(broker已经接收但是还没处理完成的)请求数限度。当新的请求被接收时,Inflight请求数增加;当请求处理完成,响应发回给客户端后, Inflight请求数减少。当Inflight请求数达到限度,broker拒绝后续的请求。
如果broker因为背压拒绝了客户端请求,客户端可以用合适的重试策略重试。如果拒绝率一直很高,说明broker持续处于高负载。在这种情况下,推荐降低请求速率。
业务微服务集成Zeebe client SDK,针对每个JobType,创建一个JobWorker,在JobWorker里实现业务逻辑。JobWorker的创建很简单,以Java SDK为例,在普通的Java方法上添加一个注解即可。
@Component@Slf4jpublic class SomeJob { @EnhancedJobWorker(type = 'some-service.SomeJob') public void handleTask(final EnhancedJobClient client, final ActivatedJob job) { // 业务逻辑 // .... // 根据业务逻辑执行情况,结单 if (success) { client.completeJob(job); } else { throw SomeException('失败原因'); }}
与Activiti引擎主动调用业务handler的模式不同,JobWorker创建之后,内部会启动一个轮询线程JobPoller,定期(默认100ms)从Broker轮询自己相关的事件(job created)。
拿到Broker端返回的任务后,会把任务信息传入业务逻辑Handler里执行。整个时序如下图:
Zeebe客户端主动轮询的模型,进一步解耦了引擎任务状态维护和微服务业务处理逻辑,可以让业务JobWorker根据自己的处理能力,以相对恒定的速率处理任务。当短时间内有大量任务创建时,Broker的队列模型,可以堆积任务,平滑流量。
当Zeebe处理任务、工作流或者内部维护时,会产生有序的记录流:
虽然客户端没办法直接审查流,但是Zeebe可以用exporter的方式,加载和配置用户代码处理每条记录。exporter提供了统一的入口处理写到流里的记录:
exporter配置好后,会在broker的启动阶段装载。在装载阶段,会去校验每个exporter的配置,如果有下面的问题,broker会启动失败:
在任何时候,对于某个partition,只会有一个主节点(leader node)。当节点变成某个partition的leader时,它要做的事情之一就是运行一个exporter stream processor实例。这个流处理器,会给每个配置好的exporter创建一个实例,然后把每条记录都转发到这些exporter实例上。
这意味着,对于每个分区的exporter,只会有且只有一个实例:如果有4个分区,并且有至少4个线程处理记录,那么可能有4个exporter实例同时在导出记录。
注意Zeebe只保证至少一次(at-least-once)的语义,也就是说,一条记录至少会被一个exporter看到一次,可能会多次。在这些情况下,可能会重复:
**注意:**虽然Zeebe尽力保证减少exporter处理的重复记录数,但是还是会出现重复记录,因此,有必要保证export操作的幂等性。可以在exporter代码中实现幂等性,但是如果导出到外部系统中,推荐在外部系统中做去重,这样降低Zeebe的负载。
如果错误出现在Exporter#open(Context)阶段,流处理器会失败然后重启修复错误。最坏的情况是,在错误修复前,没有exporter运行。
如果错误出现在Exporter#close阶段,错误信息会被记录到日志里,仍会保证其他exporter优雅完成工作。
如果错误出现在处理阶段,会无限重试同一条记录,直到不再产生错误。最坏的情况是,一个失败的exporter会导致其他exporters挂起。当前,需要exporter实现自己的重试/错误处理策略,后续可能改变。
对于每个装载的exporter,Zeebe自然会带来一些性能影响。对于某个partition,一个慢的exporter会导致其他exporter也变慢,最坏的情况下,会完全阻塞住一个线程。因此推荐exporter的逻辑尽可能的简单,把数据增强和转换等逻辑都放在外部系统中。
Zeebe作为Camunda公司的一个子项目,从2017年开始,独立开源运作。Camunda公司核心团队来自于早期的Activiti团队,主营以Activiti为核心的工作流咨询服务。从今年3月开始,Camunda宣布打包Zeebe引擎、控制台Operate、建模工具等提供SaaS服务,但核心的流程引擎Zeebe仍然以开源的方式迭代。
Zeebe不依赖外部组件,可以使用本地jar包、Docker等快速搭建集群环境。参考官方指引可以快速创建Demo项目。
官方给出的benchmark,8核32G单节点4分区1副本的配置下,能达到3.2w instance/s的性能。而且增加Broker节点,基本可以线性提高处理能力。
在我们实际6个8核16G的SKTE POD集群中,使用WeTest压测大师压测到了 3w instance/s的性能(每个流程2个节点,包含大约50个流程变量)。与官方数据有一定差距,受限于官方benchmark环境和配置给的不全,没办法完全模拟。看资源瓶颈主要在磁盘IO上。但这个数据相对我们之前Activiti 5~6 k instance/s就崩溃来说,已经是很大提升了。
另一方面,Zeebe还处于早期的迭代阶段,稳定性和功能完整度都还在持续优化中。目前支持到BPMN的ServiceTask任务,同时在任务调度上,也有小概率的miss掉任务的情况。但从一开始的架构设计、场景定位、社区活跃度等方面来看,是可以期待的项目。
立个flag,接下来会持续输出Zeebe内部实现的深入分析文章。分布式、异步化、Actor线程模型这些套路的实现还是很值得学习的。也欢迎对微服务编排、流程引擎感兴趣的同学拍砖交流~
【福利】
感谢一直支持腾讯技术工程的朋友,我们将抽2位粉丝送出 短鹅!
参与请戳这里,快把短鹅抱回家~
联系客服