近期在后台任务应用上遇到多机消费同一个任务队列的场景,须要引入必定的任务分配机制解决,由于以前也遇到过相似的问题,在此整理一下几种可能的想法,也但愿和你们交流讨论更合理、更高效的方案。数据库
假设咱们有一个集群,用于处理一系列不一样的任务,这时候咱们须要对任务进行的必定的分配,使得集群中的每台机器都负责一部分任务。编程
通常来讲会有以下几个要求:缓存
在这种场景下,该如何设计任务的分配方案?bash
为了方便后续的展开,先约束一些表达:多线程
Source
: 用于表示任务的来源Cluster
: 用于表示整个集群Task
: 用于表示抽象的任务Worker
: 用于表示实际执行任务的具体单元(如物理机)四者之间的关系能够用下图表示:并发
最简单,但也是很是有效的方案,在进行任务分配前须要提早肯定机器数量N,为每一个任务进行编号(或直接使用其id),同时为每一个执行任务的机器实例进行编号(0,1,2...)。负载均衡
即便用下面的公式:dom
Worker = TaskId % Cluster.size()
复制代码
若是任务没有id标识,那么能够经过随机数的方式来分配任务,在任务数量足够多的状况下,能够保证分配的均衡性,即:分布式
Worker = random.nextInt() % Cluster.size()
复制代码
简单取模分配的优势是足够简单,虽然负载均衡的效果比较粗糙,但能够很快达到想要的效果,在作紧急任务分机分流的时候比较有用。但从长期上看,须要维护机器数量N的实时更新和推送,而且在机器数量发生变更的时候,可能会出现集群内部的短暂不一致,若是业务对这个比较敏感,还须要进一步优化。优化
为了达到“每一个任务只被一台机器执行”的目标,能够考虑使用分布式锁机制,当有多个Worker去消费Task时,只有第一个争抢到锁的Worker才可以执行该Task。
理论上讲,每次抢到锁的Worker都是随机的,那么也就近似的实现了负载均衡;在有成熟中间件依赖的前提下,实现一个分布式锁也并不难(能够借助缓存系统的并发控制实现),而且不用考虑机器数量变化的问题。
但这个方案也有着不少的缺陷,首先争抢锁的过程自己就会消耗Worker的资源,另外因为没法预测究竟哪一个Worker可以争抢到Task的锁,因此基本不能保证整个集群的负载均衡。
我我的认为这种方案只适合于内容很是简单、数量比较多,同时执行频率很是高的任务分发(类比多线程读写缓存的场景)。
若是要作到比较精细的负载均衡,那么最好的方式就是根据集群的状态、以及任务自己的特性去量身定制一套任务分配的规则,而后经过一个中心的路由层来实现任务的调度,即:
一个简单可行的分配规则是在调度前,计算Worker的CPU、内存等负载,计算一个权重,选择压力最小的机器去运行任务;再进一步能够根据任务自己的复杂度作更精细的拆分。
该方案最大的问题在于,自主去实现一个路由层的成本比较高,另外有出现单点问题的风险(若是路由层挂了,整个任务调度就所有瘫痪了)。
这个是类比以前看到的,基于消息队列的分布式数据库解决方案(原文),借助一个可靠的Broker,咱们能够很容易构建出一个生产者-消费者模型。
Source产出的Task将所有投入消息队列中,下游的Worker接收Task,并执行(消费)。这样的好处是减小了阻塞,同时能够根据Worker的执行结果,配置重试策略(若是执行失败,再次放回到队列中)。但单单依赖Broker作任务分发的话,并不能解决咱们开头的两个问题,所以还须要:
防止消息被重复消费的机制
由于绝大多数的消息队列Broker的传输逻辑都是“保证消息至少被送达一次”,因此颇有可能出现某个Task被多个Worker获取到的现象,若是要确保“每一个任务都只被执行一次”,那么这时候可能须要引入一下上面提到的锁机制来防止重复消费。
不过若是你选择NSQ做为Broker的话,就不用考虑这个问题。NSQ的特性保证了某个消息在同一个channel下,必定只能被一个消费者消费。
任务分发
构建了生产者-消费者模型后,依然很差回答“哪一个Task要在哪一个Worker上运行”,也就是任务分发的机制,本质上仍是依赖于消费者消费动做的随机性,若是要作更精细的调控,大体想一下有两种方案。
一是在放入队列前就根据所需规则计算好映射关系,而后对Task作一下标记,最后Worker能够设置成只对含有特定标记的Task生效,或者根据Task的标记作不一样Topic来分发。
而是在取出队列的时候再进行计算,这样的话可能下游又须要维护一个路由层来作转发,感受有些得不偿失。
就大多数实际状况而言,依赖Broker自己的消息分发机制便可。
参考响应式编程中的背压概念。把Source端推送(Push)任务的过程改成Worker端拉取(Pull)任务,“反客为主”,来实现流速控制和负载均衡。
简单的说,咱们须要Worker(也多是Cluster)可以根据自身的状况来预估本身接下来可以承接的任务量,并将其反馈给Source,而后Source生产Task并传送给Worker(或者Cluster)。
设想一个可行的方案,将Source视为Server,Worker视为Client,那么便造成了一种反向的C/S模式。
其中Worker端的行为是不断重复“请求获取Task -> 运行Task -> 请求获取Task”这个循环。每当Worker评估自身处于“空闲”状态时,就向Source端发送请求,来获取Task并运行。
Source端则相对比较简单,只须要实现一个接口,每当有请求过来时,返回一个Task,并标记该Task被消费便可。
这种思路虽然能够较好的保证每台Worker机器负载处于可控范围,但也存在几个问题。
首先是流速问题,由于整个任务队列的消费速度在此模式下彻底由Worker自己调控,而任务队列的状态(还有多少任务须要处理、哪些任务比较紧急..)对Worker是不可见的,因此有可能致使任务在Source端的堆积。
其次是任务调度的延时问题,由于Source端彻底没法预知下一个Worker的请求会在何时到来,因此对于任何一个被提交的Task,都没法保证其在什么时间被执行。对于后台任务而言这个问题倒不是很大,但对于前台任务就很是致命了。
要解决上面两个问题,须要在Source端引入一个合理的任务分配机制,在极端状况下可能还须要Source端可以强制进行Task的分发。