Netflix Content Platform Engineering团队运行着很多商务流程,这些流程由在微服务上执行的异步编排驱动。其中一些是会运行好几天的长流程。这些流程在准备好视频流以供全球观众观看的过程中起着至关重要的作用。
这些流程包括:
传统上,这些流程中的一些是使用pub/sub(发布/订阅)模式,直接调用REST以及使用数据库管理状态这些方法的组合来实现,以ad-hoc的方式完成整体编排。但是,随着微服务数量的增加,以及流程复杂度的提高,如果没有中央式的编排,理解这些分布式工作流会变得非常困难。
我们将Conductor构建为“编排引擎”,来解决如下需求,代替应用中对样板文件的需要,同时提供交互式流程:
构建Conductor是为了满足上述需求,至今已经在Netflix使用了大概一年时间。到目前为止,它已经帮助编排了超过260万流程,这些流程包括简单的线性工作流,也包括非常复杂的运行数天的动态工作流。
现在,我们将 Conductor 开源,放到了社区里,希望能够从有类似需求的其他公司学习,并且加强它的功能。可以在 这里 找到Conductor的开发人员文档。
我们发现,使用点对点任务编排很难随着增长的业务需求和复杂度而完成扩展。Pub/sub模型适用于最简单的流程,但是很快你就会发现该方案的一些问题,包括:
该引擎的核心是状态机服务,也称为Decider服务。随着工作流事件的发生(比如,任务完成,失败等),Decider将工作流blueprint和该工作流的当前状态组合起来,确定下一个状态,并且调度合适的任务,并且/或者更新该工作流的状态。
Decider和一个分布式队列协同工作来管理调度的任务。我们在 Dynomite 之上使用 dyno-queues 来管理分布式延迟队列。该队列的recipe在今年早些时候已经开源了, 这里 是相关的博客文章。
任务,通过worker应用程序实现,通过API层通信。Worker有两种实现方式,要么通过可以被编排引擎调用的REST端点来实现,要么通过池循环来周期性检查待定任务实现。Worker想要设计成幂等的无状态功能。池模型允许我们处理worker上的反压力,并且可以提供基于队列深度的自动扩展能力。Conductor提供API监督每个worker的工作负载大小,可以用来自动扩展worker实例。
Worker和引擎的通信
API通过HTTP暴露——使用HTTP使得可以轻松地和不同的客户端集成。同时,添加另一种传输协议(比如,gRPC)应该是可能的并且相对直接。
我们使用 Dynomite “作为存储引擎”,以及Elasticsearch索引执行流。存储API是可插拔的,并且能够适应多种不同的存储系统,包括传统的RDBMS或者Apache Cassandra这样的no-sql存储。
工作流定义使用基于DSL的JSON来定义。工作流blueprint定义需要执行的一系列任务。每个任务要么是一个控制任务(比如,fork(分支),join(合并),decision(决策),sub workflow(子工作流)等等),要么是一个worker任务。对工作流的定义作版本化控制,提供管理升级以及迁移的灵活性。
一个工作流定义示例:
{ "name": "workflow_name", "description": "Description of workflow", "version": 1, "tasks": [ { "name": "name_of_task", "taskReferenceName": "ref_name_unique_within_blueprint", "inputParameters": { "movieId": "${workflow.input.movieId}", "url": "${workflow.input.fileLocation}" }, "type": "SIMPLE", ... (any other task specific parameters) }, {} ... ], "outputParameters": { "encoded_url": "${encode.output.location}" } }
每个任务的行为都受其模板的控制,该模板称为任务定义。任务定义为每个任务提供控制参数,比如超时,重试策略等。一个任务可以是一个由应用程序实现的worker任务,也可以是由编排服务器执行的系统任务。Conductor提供了开箱即用的系统任务,比如Decision,Fork,Join,Sub Workflow,以及一个SPI,允许集成自定义的系统任务。我们也增加了对HTTP任务的支持,可以辅助调用REST服务。
任务定义的JSON片段:
{ "name": "encode_task", "retryCount": 3, "timeoutSeconds": 1200, "inputKeys": [ "sourceRequestId", "qcElementType" ], "outputKeys": [ "state", "skipped", "result" ], "timeoutPolicy": "TIME_OUT_WF", "retryLogic": "FIXED", "retryDelaySeconds": 600, "responseTimeoutSeconds": 3600 }
任务的输入是一个map,可能是工作流初始化的一部分,或者其他任务的输出。这样的配置允许在工作流里路由输入/输出,或者允许其他任务作为输入,这样该任务可以在之上执行操作。比如,编码任务的输出可以提供给发布任务作为部署到CDN的输入。
定义任务输入的JSON片段:
{ "name": "name_of_task", "taskReferenceName": "ref_name_unique_within_blueprint", "inputParameters": { "movieId": "${workflow.input.movieId}", "url": "${workflow.input.fileLocation}" }, "type": "SIMPLE" }
让我们一起看看这个非常简单的编码以及部署的工作流:
这里总共涉及3个worker任务和一个控制任务(Errors):
这3个任务是由不同的worker实现的,使用任务API然后放到待定任务池里。这些是理想情况下幂等的任务,对任务的输入做操作,执行工作,并且更新状态。
UI是监控以及故障排除工作流执行情况的基本机制。UI提供对流程内部的洞察能力,允许基于不同的参数,包括输入/输出的搜索,并且提供了blueprint的视觉展现,以及它已经执行的路径,来帮助大家更好地理解流程的执行情况。对于每个工作流实例来说,UI提供了每个任务执行的细节信息,包括如下细节:
我们使用AWS的简单工作流做了早期版本。但是,最终选择构建Conductor,因为SWF有一些限制:
最近发布的Amazon Step Function在编排引擎里添加了一些我们需要的特性。Conductor可能可以引入 states语言 来定义工作流。
这里是我们已经运行了大概一年的生产实例的一些统计信息。大多数这些工作流都是被内容平台使用的,用来支持内容获取,吸收和编码的各种工作流。
===========================
译者介绍
崔婧雯,现就职于IBM,高级软件工程师,负责IBM WebSphere业务流程管理软件的系统测试工作。曾就职于VMware从事桌面虚拟化产品的质量保证工作。对虚拟化,中间件技术,业务流程管理有浓厚的兴趣。