最近工作中一直和 SWF(Amazon 的 Simple Work Flow)打交道,在一个基于 SWF 的工作流框架上面开发和修 bug。SWF 的 activity 超时时间是 5 分钟,在 activity task 开始执行以后,activity worker 需要主动发送心跳请求告知 service 端:“我还活着,我还在干活”,如果出现超过 5 分钟(可以配置)没有心跳,SWF 的 service 端就认为,你已经挂了,我需要把这个 activity 安排到别的 activity worker 上来执行了。借用 AWS 官网的一张图:
每台机器上有若干个 activity task 在被执行。可以看到,在 activity 任务启动起来以后,需要用不断的心跳来告知 service 端任务还在进行,activity worker 还活着。这个 “汇报” 需要 activity worker 所在的 host 主动进行,这也是 SWF 的 service 端无状态(几年前写过一点东西介绍它)的基本要求之一。任务都是由 worker 端去 pull 的,这些行为也都是 worker 端主动触发的。
这个机制描述起来很简单,但是实际在相关设计实现的时候,有许多有趣和值得琢磨的地方。
在我手头的这个 workflow 里面,心跳机制是这样实现的:
这个机制的基本好处是,所有 activity task 的心跳统一管理,通常情况下保证了心跳不会过快(默认配置下是一秒一个,或者不发送),同时保证了没有谁会被遗漏:
但是,这里又会浮现好多好多问题:
为什么要使用两个 queue?
首先,有这样一个事实:方法 A 在执行的时候,理论上每秒钟会执行一次,但是这里并没有强制的保证,使得前一秒的 A 执行一定会在这一秒的 A 开始之前完成。换言之,它们的理论启动时间是按序的,但是实际启动时间和实际的心跳执行时间是不定的,需要处理并发的情形。而到底最多可能存在多少个执行 A 的线程并行,取决于用于此心跳功能的线程池的配置。因此,在执行和判断的过程中,需要对当前 poll 出来的 heartbeatable 对象加锁。
使用两个 queue,这主要是为了记录在本次 cycle 里面,能够很容易判断某一个 heartbeatable 对象是否已经完成心跳行为。还没有完成心跳的,都在 main queue 里;完成了的,都放到 backup queue 里。
如果使用一个 queue,那么也是有解决方案的:
当然,这种方法的弊端在于,判断是否还需要发送心跳这件事情,不仅需要从 queue 里取对象,还要判断对象的计数器数值,明显比两个 queue 的解决方案复杂和开销大。因为两个 queue 的解决方案下,只需要尝试从 main queue 里面取对象就好,取不到了就说明本次 cycle 里没有需要发送心跳的对象了。看起来是多了一个 queue,但是方案其实还是简单一些。
心跳的频率保持在多久为好?
显然不是越高越好,不只是成本,因为心跳也是需要消耗资源的,比如 CPU 资源;而且,心跳在 service 端也有 throttling,当前 activity worker 发起太频繁的心跳,当前心跳可能被拒,还可能会让别的 activity worker 的正常心跳被拒了。
我们要解决的最核心问题是,正常情况下,必须保持上限 5 分钟内能发起一次成功心跳就好。
要这么说,尽量增大 cycle,那我设计一个每隔 5 分钟就执行一次的定时器就好了。但是问题没那么简单,首先要考虑心跳的发起不一定成功。如果在接近 5 分钟的时候才去尝试发起心跳,一旦失败了,也没有时间重试了。因此,要 trade-off。比如,配置 cycle 为 120 秒,这样的好处是,5 分钟的超时时间内,可以覆盖 1~2 个完整的 cycle。如果 cycle 配置为 3 分钟,那么 5 分钟无法严格保证一定覆盖有一个完整的 cycle。
确定心跳频率的有两个重要参数,一个是方法 A 的执行频率,一个则是一个 cycle 的时间长度。例如,前者为 1 per second,后者为 2 分钟,那么在理想情况下,一个 cycle 120 秒,可以处理 120 个 activity task,换言之,极限是 120 个 activity task 在这台机器上一起执行。超过了这个数,就意味着在一个 cycle 内,无法完成所有的心跳发送任务。
当然,实际情况没有那么理想,考虑到暂时性的网络问题,线程、CPU 资源的竞争等等,实际可以并行的 activity task 要比这个数低不少。
异常处理和重试
在上图中,步骤③有三个箭头,表示了心跳出现不同种情形的处理:
曾经遇到过一些这方面的问题,经过改进才有了上述的机制:
在 CPU 或者 load 达到一定程度的时候(比如这个时候有一个进程在 call service,占用了大量的 CPU 资源),就很容易发生心跳无法及时进行的问题,比如有时候线程已经初始化了,但是会 stuck 若干时间,因为没有足够的资源去进行。等到某一时刻,资源被释放(比如这个 call service 的进程结束),这个时候之前积攒的心跳任务会一下子爆发出来。不但这些心跳的顺序无法保证,而且严重的情况下会导致 throttling。如果没有当前 cycle 内的重试机制,那么下一次该对象的心跳需要等到下一个 cycle,很容易造成 activity task 的 timeout。
下面再说一个和心跳异常有关的问题。
有这样一个例子,在这个工作流框架内,我们需要管理 EMR 资源,有一个 activity 把 EMR cluster 初始化完成,另一个 activity 把实际执行的 steps 提交上去。但是发现在实际运行时有如下的问题:EMR cluster 已经初始化完成,但是 steps 迟迟没有办法提交上去,导致了这个 cluster 空闲太长时间,被框架内的 monitor 认为已经没有人使用了,需要回收,于是 EMR cluster 就被 terminate 了。但是这之后,steps 才被提交上去,但是这时候 cluster 已经处于 terminating 状态了,自然这个 step 提交就失败了。而经过分析,造成这个 EMR cluster 非预期的 termination,包括这样几个原因:
最后,我想说的是。设计一个好的工作流框架,还是有很多困难的地方,需要尤其考虑周全的地方。即便是基于 SWF 这样现有的 workflow 来搭积木和叠加功能,也有很多不易和有趣的地方。
文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》