几年前曾经写过一点点对于缓存框架设计的体会,这大半年和工作流系统打交道颇为丰富,因此想总结一点关于工作流系统的设计。
首先,明确工作流(workflow)系统的定义。维基百科上有极其简单的介绍。我记得以前在文章里面说过,作为大公司里面的小 team,为了做一些有趣的东西,从而更好的招人,通常有几个众人皆知的突破口:比如一个更符合业务需求的 storage,再比如一个自定义的工作流系统。在 Amazon 内部,我接触过好多个 workflow,而且大多以 Amazon SWF 为原型(当时学习的时候还写了一点体会,link 1 和 link 2),于是宏观上看,60% 的东西是一样的,大同小异;但是也有很多重要的元素大不相同,而它们被放到一起比较也是常事。几次折腾之后,我也慢慢在思考,如何去设计一个工作流系统,其中都有哪些重要的需要考虑到的方面。
Scalability
基本上随便设计什么基础设施,扩展性都是重要的考虑内容。作为 workflow 来讲,基本上工作节点的水平扩展是考量扩展性的最重要标志。既然工作节点可以水平扩展,那么这就意味着任务(task)必须是以 pull 的方式由工作节点主动去获取,而不是由 push 的方式从调度节点来分配(曾经非常简单地比较过 pull 和 push,但其实二者差异远不止文中内容之浅显)。任务的分配上,需要考虑这样的事情:如果有多个工作节点尝试来 pull 任务,该分配给谁?具体来说,比如这样的例子:如果每一个 task 节点允许同时执行 5 个任务,而现在可同时执行的总任务数只有 5 个,总共的 task 节点也有 5 个,最理想的状态应当是这 5 个被均匀分配到这 5 个节点去,但是采用简单的 pull 机制并不能保证这一点,有可能这 5 个任务全部跑到一台机器上去了,因为这并不超过一个节点可同时执行任务数量的上限。
另一方面,通常来讲,所有任务都应当是 idempotent 的,即可以重复提交执行,执行若干次和执行一次的结果是一样的。工作节点的任务执行可以在任意一步发生错误,随着节点数量的增加,这样的错误更多地成为一种常态,而不是 “异常”。工作节点的健康状态需要由某种方式来维系和通知,最典型和廉价有效的方式就是 “心跳”,我曾经写过一篇文章详细介绍一种心跳系统的设计,感兴趣的话,欢迎移步阅读。
功能性解耦
同步与异步任务
事实上,当考虑到了独立的资源管理功能,异步和同步任务的划分就变得自然而然。
分布式锁
在某些情况下,分布式锁变成一个必选项。比如前面提到的资源管理。有许多资源是要求操作是独占的,换言之,不支持两个操作并发调用,期间可能出现不可以预料的问题;另一方面,一个节点在对资源进行操作时,它需要和别的节点进行协作,从而两个工作节点的操作是有序和正确的,不至于发生冲突。
举个例子来说,工作节点 A 要查询当前 EMR 的状态,如果已经空闲 10 分钟,就要执行操作结束掉这个 EMR 资源;而工作节点 B 则查询该 EMR 的状态,如果没有被结束掉,就要往上面提交新的计算任务。这时候,如果没有分布式锁的协作,问题就来了,可能 B 节点先查询发现 EMR 状态还活着,就这这一瞬间,A 节点结束了它,可是 B 不知道,接着提交了一个计算任务到这个已经结束了的(死了的)EMR 资源上,于是这个提交的计算任务必然执行失败了。
有很多分布式锁的实现方式,简单的有强一致性的存储系统,当然也有更高效的实现,比如一些专门的分布式锁系统。
功能的可扩展性
之前讲到了性能架构上的可扩展性,在功能层面亦然。
可用性和可靠性
大多数 workflow,都采用了去中心节点的设计,保证不存在任何单点故障问题。所有的子系统都是。也保证在业务压力增加的情况下,标志着可用性的 latency 在预期范围之内。其它的内容不展开,介绍这方面的文章到处都是。
生命周期管理
这里既指 workflow 一次执行的生命周期管理,也指单个 task 的生命周期管理。
谈论这些必然涉及到这样几个问题:
任务 DAG 的设计和表达
这是 workflow 执行的流程图,也是所有 task 之间依赖关系的表述。我见过多种表达方式的,有 XML 的,也有 JSON 的,还有一些不知名的自己定义的格式的。有些 workflow 的定义可以以一个图形化工具来协助完成这个流程图。这个 DSL 的设计,一定程度上决定了 workflow 的使用是不是能够易于理解。另外提一句,这里提到的这个可选的图形化工具,毕竟只是一个辅助,它不是 workflow 的核心(你可以说这个 DSL 是核心的一部分,但这个帮助完成的工具显然不是)——我见过一个团队,workflow 整体设计得不怎么样,跑起来一堆问题,但是这个工具花了大量的时间精力去修缮,本末倒置。
另外,workflow 的状态和执行情况,还有对其的归档和管理,也需要一个整合工具来协助。这方面几乎所有 workflow 都具备,通常都是网页工具,以及命令行工具。
输入输出的管理
这也是一个 nice-to-have 的东西,对于每一个 task,都存在 input 和 output,它们可以完全交给用户自己来实现,比如用户把它们存储到文件里面,或者写到数据库里面,而 workflow 根本不管,每个 task 内部自己去读取相应的用户文件即可。但是更好的方法是,对于一些常用和简单的 input、output,是可以随着 execution 一起持久化到 workflow 和 task 的状态里面去的。这样也便于 workflow 的 definition 里面,放置一些根据前一步 task 执行结果来决策后续执行的表达式。
另外,还有一个稍微冷门的 use case,就是 input 和 output 的管理。通常 workflow 是重复执行的,而每次执行的 input 和 output 的数据规模往往是很多人关心的内容。关于这部分,我还没有见到任何一个 workflow 提供这样的功能。许多用户自己写工具和脚本来获取这样的信息。
独立的 metrics 和日志系统
对于 metrics,核心的内容也无非节点的健康状况、CPU、内存,task 执行时间分布,失败率等等几项。有些情况下用户还希望自行扩展。
关于日志,则主要指的是归档和合并。归档,指的是历史日志不丢失,或者在一定时间内不丢失,过期日志可以被覆写,从而不引起磁盘容量的问题;而合并,指的是日志能被以更统一的视角进行查询和浏览,出了问题不至于到每台机器上去手动查找。缺少这个功能,有时候会很麻烦。在工作中我遇到过一个资源被异常终止的问题,为了找到那个终止资源的节点,我查阅了几十个节点的日志,痛苦不堪。
版本控制和平滑部署
把这两个放一起是因为,代码升级是不可避免且经常要发生的。为了保证平滑部署,显然通常情况下,节点上的代码不能同时更新,需要一部分一部分进行。比如,先终止 50% 的节点,部署代码后,激活并确保成功,再进行剩下那 50% 的节点。但是在这期间存在新老代码并存的问题,这通常会带来很多奇形怪状的问题。对于这种问题,我见过这样两个解决方式:
无论选用哪一种,这种方式实现起来相对简单,但是也有不少问题,比如这种情况下,外部资源怎么处理?例如在外部 EMR 资源上执行 Spark 任务,但是已经有老代码被放到 EMR 上去执行了,这时候工作节点更新,这些 EMR 上正在执行的任务怎样处理?是作废还是保留,如果保留的话这些执行可还是依仗着老代码的,其结果的后续处理是否会和刚部署的新代码产生冲突。再比如对于 workflow 有定义上的改变(比如 DAG 的改变),对于现有的 execution,应当怎样处理,是更新还是保持原样(通常都是保持原样,因为更新带来的复杂问题非常多)。
最后,这幅图以一个典型实现简要地表述了这些组件和它们之间的关系:
文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》