分布式任务调度的几种实现
需求背景
现在有一个计算搜索词热榜的任务,该服务部署在了多个节上,希望只有一个节点在执行这个任务。
常见方案
使用Redis实现分布式锁方案
使用一个分布式锁,确保整个分布式环境下,只有一个节点能够拿到锁。节点先抢占分布式锁,如果抢到了分布式锁的话,就执行计算搜索词热榜的任务,否则的话,就跳过该任务。
使用Redis实现的话,可以使用SexNX命令,SETNX task1_key pod1
表示节点pod1
正在尝试抢任务task1
的分布式锁。设置的时候需要设置过期时间。如果上锁成功的话,说明该节点可以执行任务task1
。
解锁的话,采用lua
脚本完成。这个脚本检查一个键的值是否等于给定的参数值,如果相等,则删除该键;否则,返回0。传入的KEYS[1] = "task1_key",ARGV[1]="pod1"
。
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
使用MySQL实现任务调度
在MySQL的数据库里创建一张表,里面是所有等待运行的定时任务。所有的节点都试着从表里抢占任务,如果成功的话就执行该任务。
这里的抢占,可以使用乐观锁来更新状态实现。先找到符合条件的任务,假设任务执行状态为等待开始,对应的数据库表字段为status=waiting_start
,假设此时的version=1
。然后尝试更新状态为update status = running
。那么对于更新成功的节点来说,就相当于抢占到了该任务。如何保证更新的时候,没有其他节点抢占呢?更新的时候同时判断status=waiting_start,version=1
,就能保证单节点抢占。
这里会存在的一个问题就是,如果已经抢占到任务但是任务还没执行完,该节点就异常退出了,但是其他节点都会认为该节点依旧在执行任务,就会陷入死锁的局面。想了一下解决方案就是,引入续约机制,对于MySQL实现而言,就是该节点需要不断地更新数据库的update_time字段,也就是更新时间,证明自己的状态正常;对于Redis实现而言,相当于延长锁的过期时间。
开源框架 XXL-JOB
XXL-JOB 是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习门槛低、功能强大并且轻量级。它支持自定义任务调度策略,支持多种执行模式,并且支持动态分片。
XXL-JOB主要由调度中心和执行器两部分组成。调度中心负责管理调度信息,按照调度配置发出调度请求,但自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块。执行器则负责接收调度请求并执行任务逻辑。
退一步而言,如果引入XXL-JOB的成本过高,可以考虑起一个单独的服务执行定时任务,运行在单pod上,这样的好处是开发快捷方便,缺点可能在于修改任务需要服务发版。
负载均衡
前两种方案在设计的时候,都是看哪个节点先抢占到任务,没有结合节点的情况考虑,如果一个本身就负载高的节点抢占到了任务,那么可能会影响到其他的业务逻辑,甚至引发节点OOM或崩溃。这种情况下,应该在调度的时候就引入一些负载均衡策略,
- 检查节点的负载情况,超出健康阈值后就不抢占任务,比如内存使用率>=80%或CPU使用率>=80%,就不抢占该定时任务,同时设置一个定时任务未执行的报警,方便监控。
- 每次选取负载最低的节点来进行,可以是内存最低也可以是CPU利用率最低,这就要求有一个控制中枢检测各个节点的负载情况,感觉需要借助中间件实现
- 如果某个节点正在运行该任务,但是运行中的负载较高,就要及时中断并更换节点,做容错处理
- 如果定时任务运行需要的资源较多,可以考虑使用XXL-JOB这种分布式任务调度框架