在很多系统中,为了解耦,或者处理需要较长时间的任务时(例如,有些网络请求可能很慢,或者有一些请求属于CPU密集型,需要 等待一段时间),我们通常会引入任务队列。典型的任务队列由下面三部分组成:
通常,我们会使用这些中间件做broker(也还有其它选择,但是不那么普遍):
对于broker,我们通常要求能做到:
并不是所有的broker都具备上述的功能,例如 Redis 就没有ACK,也没有优先级,但是 Redis 作为日常使用来说,仍然是够用的。
在入队和出队时,我们都可以选择是否需要阻塞,这取决于我们的业务场景,例如当队列满时,入队阻塞会导致对应web请求卡住; 对应的,当队列为空时,出队阻塞会导致消费者阻塞。对于一般的应用来说,我们都会选择出队阻塞。
对于任务队列本身,我们也许要进行一些监控,主要包括:
当我们进行监控之后,偶尔我们会看到一些流量顶峰,这个时候就涉及到一个问题:流量削峰。
出现流量峰值的时候,通常是用户突然激增,或者搞活动,其实并没有很好的办法。通常,如果是搞活动,也就是预料之中的事情, 我们能做的也就是两件事:
对于突发的流量激增,我们能做的也就是紧急扩容,如果做得好的话,可以配合监控做自动扩缩容,这就对infra层有一定的考验。
任务队列中,不可避免的会出现一些任务执行失败的场景,为了复现,我们通常需要一个专用的队列用于存储该任务,通常就是我们所说的 dead letter。对于dead letter,我们通常是检查 dead letter 的日志,找到根本原因之后,再次将该任务入队重试。dead letter本身 存在的意义也就是存储任务执行失败的信息,例如参数、日志,方便排查问题,以便重现和修复。
任务失败时,除了立即移到dead letter queue,我们还可以配置重试策略,例如重试3次,3次都失败以后,移到dead letter queue中。 对于任务重试,我们通常都会采用指数回退进行延时。
对于最简单的情况,一个任务就是一个任务,这种情况下,任务的粒度非常小,例如给用户发送邮件。还有一种情况,一个任务下可能 包含多个子任务,这种情况下,为了简化应用层代码,通常我们都会在任务框架中实现,例如:
TaskA - Job1
- Job2
- Job3
- ...
也就是一个任务中包含了多个子任务,子任务之间还有可能会有依赖的情况,例如创建虚拟机这个任务就包含多个子任务,并且最后 一步启动虚拟机的前提一定是前面的步骤都执行完了。
当子任务之间有依赖时,最简单的办法就是线性执行,依赖描述中写明执行顺序,我们按照顺序一个一个来。还有一种复杂一些的办法, 就是采用有向无环图,将任务之间不会互相依赖的,做到并发执行,但是后者的难度通常会高不少。
这一篇文章中,介绍了任务队列典型的架构、常见的broker,以及任务队列中会涉及到的一些场景和概念,希望对大家有所帮助。