如何实现分布式延迟消息

2,221 阅读11分钟

业务场景

业务中很多地方都需要任务调度。
有的任务是周期性执行,比如每隔半小时更新本地缓存数据
有的任务是延迟执行,比如订单15分钟未付款提醒,优惠券到期提醒
这显然要比第一种的难于实现很多:订单的数量/优惠券的数量(一般都是亿级/天)决定了调度任务数量庞大,触发时间分散
精度要求比较高,一般是精确到秒, 支持的最大时间越久,储存的数据量也越大
业务往往还要对任务进行修改和删除
除此以外,可用性、一致性、弹性伸缩、失败重试、监控告警也必须考虑

可见:技术方案还是要根据实际情况来选择,确定好性能指标,再选择

在生产环境的见过的实现主要有以下3类

1.使用mq的延迟消息和定时消息

2.独立部署的任务调度平台

3.快速实践型

  • 典型代表:用redis:zset + score, 或者mysql 扫描表写代码实现

想到的问题

问题1,mq的延迟消息对比独立的任务调度平台有啥优缺点?两者功能有些重合
大厂内部的mq都集成了任意时间延迟消息,比如mafka(美团的mq)/rocketmq阿里云版本 实现上都包含了一个任务调度平台,由任务调度平台延迟发送到目标 topic中,我的想法时,在mq的基础上实现首先方便运维,mq可以提供任务创建和执行时的流量削峰,保护调度平台,调度任务触发也可以使用mq作为事件触发的数据总线,总的来说,像延时触发、定时触发等事件驱动的场景下,离不开一套靠谱的Event-Streaming系统,也就是mq。

前公司 内部是既单独部署了自研任务调度平台(已经开源:链接地址。这个任务调度平台主要依赖jraft提供一致性和系统容错性,用mysql存储任务数据),又部署了rocketMQ社区版,结果就是 开源rocketMQ 由于延迟时间固定大家往往不用,都选择了用任务调度平台,也导致任务调度平台用户很多压力很大,多次宕机。(ps:可见:提供中间件的时候,完整性能测试、多租户隔离都是重要的 )

方案调研和技术选型

Qmq、RocketMQ、xxl-job都是国产的,有着文档可读性高,上手更快的优势,还有国产情怀加持,下面就对比这几种实现


Qmq

  • 实现原理

QMQ的延时/定时消息使用的是两层hash wheel来实现的。第一层位于磁盘上,每个小时为一个刻度(默认为一个小时一个刻度,可以根据实际情况在配置里进行调整),每个刻度会生成一个日志文件(schedule log),因为QMQ支持两年内的延迟消息(默认支持两年内,可以进行配置修改),则最多会生成 2 * 366 * 24 = 17568 个文件(如果需要支持的最大延时时间更短,则生成的文件更少)。第二层在内存中,当消息的投递时间即将到来的时候,会将这个小时的消息索引(索引包括消息在schedule log中的offset和size)从磁盘文件加载到内存中的hash wheel上,内存中的hash wheel则是以500ms为一个刻度。

Step 1: 生产者投递延时消息到messagelog

Step 2: 内部消费者消费消息,判断这个Message是否在当前时间轮范围中,如果不在则来到Step3,如果在的话就直接将消息投递进入时间轮。

Step 3: 找到当前消息所属的scheduleLog,然后写入进去,去哪儿默认划分是一个小时为一段,这里可以根据业务自行调整。

Step 4:时间轮会定时预加载下个时间段的scheduleLog到内存。

Step 6: 到点的消息会还原topic再次投递到MessageLog,如果投递成功这里会记录dispatchLog。记录的原因是因为时间轮是内存的,你不知道已经执行到哪个位置了,如果执行到最后最后1s钟的时候挂了,这段时间轮之前的所有数据又得重新加载,这里是用来过滤已经投递过的消息。

image.png

引用官网的演进历史:Qmq起初选择数据库作为 MQ Server 的消息存储,后来接的业务多了,不得不重新设计。
如果他们用的是分布式数据库,性能上是不是就没问题了呢。

metaserver: 部署两台应用,然后配置nginx作为负载均衡。metaserver依赖mysql,请确保mysql的可用性,但metaserver运行时不强依赖mysql,如果mysql故障,只会影响新上线的消息主题和新上线的消费者,对已有的无影响。
broker:部署两组,每组为一主一从两台,则至少部署4台

我的理解,类似于kafka的数据冗余,broker是有状态服务都需要写磁盘,client从metaserver拉取一个topic下的master服务器,通过rpc的形式调用master服务,master服务收到消息,同步给slave,然后ack client。 如果master宕机,slave可以继续提供读服务,但是不提供写,写请求可以路由到另一个master机器。 消费者也需要实时感知master的地址以及状态,正常情况下rpc到master获取数据,提交offset,master宕机后去slave读取消息。
性能方面没有找到相关数据支撑,但是从架构设计上看,多个broker参与执行,效率应该是最高的

看Qmq的实现用到了时间轮,那我就好奇了,为啥要用时间轮?这种数据结构更加高效吗?

时间轮算法 HashedWheelTimer 这个链接是有赞用内存时间轮来实现,大量连接超时功能的,其中比较了时间轮和轮询扫描的差距,写的蛮好的,可以看下。

简单来说,最小堆实现的优先级队列(Priority queue represented as a balanced binary heap),插入和取出一个任务时间复杂度是O(logN),redis zadd一个任务时间复杂度也是O(logN)。 相比之下,时间轮上 任务的插入时间复杂度达为O(1),如果任务数量不是很多的话,看不出差距,海量任务的情况下差距就非常明显了,所以还的看实际需求,防止过度设计。

时间轮的结构和优势已经了解了,时间轮依赖的环形数组需要放在内存,系统肯定是要重启/宕机的,那故障转移该怎么做?(也就是执行过程中如果宕机,怎么转移到另一台机器上继续执行)。

学到现在,如果用时间轮可以这样简单实现一下:

  • 假设有三台配置正常的服务器
  • 每台服务器用netty的HashedWheelTimer(现成的实现~)设置一个内存时间轮3600个槽,每个槽代表1秒时间
  • 开启一个mq topic接收延迟消任务,开三个consumer消费,消费到消息可以判断一下是否在时间轮范围内,也就说是不是1个小时内就要执行,如果是,直接加入时间轮,再放入db,status设置为1待执行,提交mq offset。
  • 如果是时间轮范围之外,也就是延时>1小时,则直接加入db,status设置为0初始化。db可以按照执行时间分表。
  • 三台服务器启动时立马启动一个后台进程,每隔 1小时 去数据库中把下一个1小时要执行的任务加入内存时间轮,这里希望是三台服务器平均分配要执行的任务,

sql: update db.table set status =status+ 1
where id % 3 ={这台机器的编号,比如 1 } and status=0
and fire_time >now()+1h and fire_time < now()+2h
拿到update成功的任务的id

  • 时间轮上的任务可以通过 线程池来异步触发,执行完成后,将结果写入db
  • 服务器重启时加载最近1小时的任务即可 这只是初步的想法,还没有实践,性能也不知,而且mysql存的数据量大了以后还是可能成为性能瓶颈,扫描mysql的时间复杂度还是O(logN),是否可以使用mysql按照时间轮的数据结构来储存数据呢?

这里想到一个类似于qmq延迟队列的实现,由于自己实现高可用比较复杂,而且我也没有写过open&append磁盘文件的程序,我认为可以在hadoop通过读写文件来实现 qmq的第一层磁盘时间轮,由hadoop提供数据冗余和高可用保障,或者使用H2内嵌式数据库,服务端则是无状态无中心的,可以水平扩展


看下xxl-job的设计

image.png

文档地址

文档上来看xxl-job的架构跟平时的web项目差不多。

贴个链接 :分布式任务调度系统xxl-job小结
这个小结已经讲的非常详细了,调度中心通过分布式锁保障是单点的,每隔5s去mysql拉取任务,放入时间轮,时间到了执行的时候会发给“执行器”来执行,执行器相等于是另外一个web服务,他们之间走rpc通讯,执行器的性能可以通过加机器来拓展。
调度中心集群只有一个提供服务,其他要等待分布式锁释放,才能成为主节点,提供服务。 性能方面,官方表示大约能支持单机5000任务的同时调度


选型总结

两者都能实现任务调度的功能,最大的区别应该就是中心化和去中心化(分片),qmq-delay-server去中心化(分片)后性能应该会好很多,对机器配置也要求比较高,适合海量任务。缺点是依赖了qmq其他组件,独立部署后需要自行实现高可用。
xxl-job功能齐全,对任务的curd、监控和管理做的比较全面,部署运维定制化开发都比qmq-delay-server容易很多。

powerJob

无意间发现一个云原生解决方案,这似乎是分布式调度的终极解决方案,果然有钱就是能为所欲为。

分布式调度平台 SchedulerX 阿里云的产品,目前(20210419)还在公测阶段。
社区类似的实现 PowerJob

image.png 云服务的依赖只有。。人民币哈哈哈。打算好好研究一下这个框架

这样的大数据任务调度框架在实现任务调度的同时,更侧重实现 工作流、动态加载任务等等 image.png 又和同事聊了一下,他们认为可以通过水平扩展,把不同的租户放在不同的region上来处理,可以简单粗暴的通过加机器的方法来解决任务量大的问题,而每台机器上的存储形式可以采用时间轮的方案,可以用磁盘、mysql、redis作为时间轮结构的实现基础 。

现状和挑战

我认为目前的任务调度系统最酷的就是 平衡策略了,我先介绍一下:

  • 假设我现在有3个mysql,每个mysql 创建3张表,用来给扫描器扫描用,那就是有9张表
  • 在zk 创建9张表的顺序节点。
  • 有三台web服务器,每台服务器启动一个broker线程,注册成为zk的临时节点
  • 每个broker可以通过zk感知到一共有9张表,3个broker
  • 系统启动,broker线程通过负载均衡算法,已最简单hash分配,broker-1 拿到n%3=0 的三张表也就是0,3,6。broker-2拿到1、4、7。broker-3拿到2、4、8
  • 上面的分配过程非常快速,一下子就能计算出来,不需要加锁,几乎没有延时
  • broker继续监听集群中所有broker的数量,当数量发生改变时,stop the world,进行一下重平衡。

这样做的优势:无中心设计,服务端和数据库都可水平扩展,无锁化设计,既满足高可用,理论上性能无上限

写了个Demo版本的实现 github地址

可以参考的文档