如何在MQ中实现支持任意延迟的消息?
什么是定时消息和延迟消息?
- 定时消息:Producer 将消息发送到 MQ 服务端,但并不期望这条消息立马投递,而是推迟到在当前时间点之后的某一个时间投递到 Consumer 进行消费,该消息即定时消息。
- 延迟消息:Producer 将消息发送到 MQ 服务端,但并不期望这条消息立马投递,而是延迟一定时间后才投递到 Consumer 进行消费,该消息即延时消息。
定时消息与延迟消息在代码配置上存在一些差异,但是最终达到的效果相同:消息在发送到 MQ 服务端后并不会立马投递,而是根据消息中的属性延迟固定时间后才投递给消费者。
目前业界MQ对定时消息和延迟消息的支持情况
上图是阿里云上对业界MQ功能的对比,其中开源产品中只有阿里的RocketMQ支持延迟消息,且是固定的18个Level。
固定Level的含义是延迟是特定级别的,比如支持3秒、5秒的Level,那么用户只能发送3秒延迟或者5秒延迟,不能发送8秒延迟的消息。
消息队列RocketMQ的阿里云版本(收费版本)才支持到精确到秒级别的延迟消息(没有特定Level的限制)。
上图是CMQ中对MQ功能的对比,其中标明腾讯的CMQ支持延迟消息,但是没有具体写明支持到什么精度,支持任意时间还是特定的Level。
通过腾讯云上CMQ的API文档可以看到有一个秒级别的delaySeconds,应该是支持任意级别的延迟,即和收费版本的RocketMQ一致。
总结
- 开源版本中,只有RocketMQ支持延迟消息,且只支持18个特定级别的延迟
- 付费版本中,阿里云和腾讯云上的MQ产品都支持精度为秒级别的延迟消息
(真是有钱能使鬼推磨啊,有钱就能发任意延迟的消息了,没钱最多只能发特定Level了)
任意延迟的消息难点在哪里?
开源版本没有支持任意延迟的消息,我想可能有以下几个原因:
- 任意延迟的消息的需求不强烈
- 可能是一个比较有技术含量的点,不愿意开源
需求不强
对支持任意延迟的需求确实不强,因为:
- 延迟并不是MQ场景的核心功能,业务单独做一个替代方案的成本不大
- 业务上一般对延迟的需求都是固定的,比如下单后半小时check是否付款,发货后7天check是否收货
在我司,MQ上线一年多后才有业务方希望我能支持延迟消息,且不要求任意延迟,只要求和RocketMQ开源版本一致,支持一些业务上的级别即可。
不愿意开源
为了差异化(好在云上卖钱),只能降开源版本的功能进行阉割,所以开源版本的RocketMQ变成了只支持特定Level的延迟。
难点在哪里?
既然业务有需求,我们肯定也要去支持。
首先,我们先划清楚定义和边界:
在我们的系统范围内,支持任意延迟的消息指的是:
精度支持到秒级别
最大支持30天的延迟
本着对自己的高要求,我们并不满足于开源RocketMQ的18个Level的方案。那么,如果我们自己要去实现一个支持任意延迟的消息队列,难点在哪里呢?
- 排序
- 消息存储
首先,支持任意延迟意味着消息是需要在服务端进行排序的。
比如用户先发了一条延迟1分钟的消息,一秒后发了一条延迟3秒的消息,显然延迟3秒的消息需要先被投递出去。那么服务端在收到消息后需要对消息进行排序后再投递出去。
在MQ中,为了保证可靠性,消息是需要落盘的,且对性能和延迟的要求,决定了在服务端对消息进行排序是完全不可接受的。
其次,目前MQ的方案中都是基于WAL的方式实现的(RocketMQ、Kafka),日志文件会被过期删除,一般会保留最近一段时间的数据。
支持任意级别的延迟,那么需要保存最近30天的消息。
阿里内部 1000+ 核心应用使用,每天流转几千亿条消息,经过双11交易、商品等核心链路真实场景的验证,稳定可靠。
考虑一下一天几千亿的消息,保存30天的话需要堆多少服务器,显然是无法做到的。
知己知彼
虽然决定自己做,但是依旧需要先了解开源的实现,那么就只能看看RocketMQ开源版本中,支持18个Level是怎么实现的,希望能从中得到一些灵感。
上图是通过RocketMQ源码分析后简化一个实现原理方案示意图。
分为两个部分:
- 消息的写入
- 消息的Schedule
消息写入中:
- 在写入CommitLog之前,如果是延迟消息,替换掉消息的Topic和queueId(被替换为延迟消息特定的Topic,queueId则为延迟级别对应的id)
- 消息写入CommitLog之后,提交dispatchRequest到DispatchService
- 因为在第①步中Topic和QueueId被替换了,所以写入的ConsumeQueue实际上非真正消息应该所属的ConsumeQueue,而是写入到ScheduledConsumeQueue中(这个特定的Queue存放不会被消费)
Schedule过程中:
- 给每个Level设置定时器,从ScheduledConsumeQueue中读取信息
- 如果ScheduledConsumeQueue中的元素已近到时,那么从CommitLog中读取消息内容,恢复成正常的消息内容写入CommitLog
- 写入CommitLog后提交dispatchRequest给DispatchService
- 因为在写入CommitLog前已经恢复了Topic等属性,所以此时DispatchService会将消息投递到正确的ConsumeQueue中
回顾一下这个方案,最大的优点就是没有了排序:
- 先发一条level是5s的消息,再发一条level是3s的消息,因为他们会属于不同的ScheduleQueue所以投递顺序能保持正确
- 如果先后发两条level相同的消息,那么他们的处于同一个ConsumeQueue且保持发送顺序
- 因为level数固定,每个level的有自己独立的定时器,开销也不会很大
- ScheduledConsumeQueue其实是一个普通的ConsumeQueue,所以可靠性等都可以按照原系统的M-S结构等得到保障
但是这个方案也有一些问题:
- 固定了Level,不够灵活,最多只能支持18个Level
- 业务是会变的,但是Level需要提前划分,不支持修改
- 如果要支持30天的延迟,CommitLog的量会很大,这块怎么处理没有看到
站在巨人的肩膀上
总结RocketMQ的方案,通过划分Level的方式,将排序操作转换为了O(1)的ConsumeQueue 的append操作。
我们去支持任意延迟的消息,必然也需要通过类似的方式避免掉排序。
此时我们想到了TimeWheel:《Hashed and Hierarchical Timing Wheels: Data Structures for the Efficient Implementation of a Timer Facility 》
Netty中也是用TimeWheel来优化I/O超时的操作。
TimeWheel
TimeWheel的大致原理如下:
- 箭头按照一定方向固定频率移动(如手表指针),每一次跳动称为一个tick。ticksPerWheel表示一个定时轮上的tick数。
如每次tick为1秒,ticksPerWheel为60,那么这就和现实中的秒针走动完全一致。
TimeWheel应用到延迟消息中
无论定时消息还是延迟消息,最终都是投递后延迟一段时间对用户可见。
假设这个延迟时间为X秒,那么X%(ticksPerWheel * tick)可以计算出X所属的TimeWheel中位置。
这里存在一个问题,以上图为例,TimeWheel的size为8,那么延迟1秒和9秒的消息都处在一个链表中。如果用户先发了延迟9秒的消息再发了延迟1秒的消息,他们在一个链表中所以延迟1秒的消息会需要等待延迟9秒的消息先投递。显然这是不能接受的,那么如何解决这个问题?
排序
显然,如果对TimeWheel一个tick中的元素进行排序显然就解决了上面的问题。但是显而易见的是排序是不可能的。
扩大时间轮
最直观的方式,我们能不能通过扩大时间轮的方式避免延迟9和延迟1落到一个tick位置上?
假设支持30天,精度为1秒,那么ticksPerWheel=30 * 24 * 60 * 60,这样每一个tick上的延迟都是一致的,不存在上述的问题(类似于将RocketMQ的Level提升到了30 * 24 * 60 * 60个)。但是TimeWheel需要被加载到内存操作,这显然是无法接受的。
多级时间轮
单个TimeWheel无法支持,那么能否显示中的时针、分针的形式,构建多级时间轮来解决呢?
多级时间轮解决了上述的问题,但是又引入了新的问题:
- 在整点(tick指向0的位置)需要加载大量的数据会导致延迟,比如第二个时间轮到整点需要加载未来一天的数据
- 时间轮需要载入到内存,这个开销是不可接受的
延迟加载
多级定时轮的问题在于需要加载大量数据到内存,那么能否优化一下将这里的数据延迟加载到内存来解决内存开销的问题呢?
在多级定时轮的方案中,显然对于未来一小时或者未来一天的数据可以不加载到内存,而可以只加载延迟时间临近的消息。
进一步优化,可以将数据按照固定延迟间隔划分,那么每次加载的数据量是大致相同的,不会出tick约大的定时轮需要加载越多的数据,那么方案如下:
基于上述的方案,那么TimeWheel中存储未来30分钟需要投递的消息的索引,索引为一个long型,那么数据量为:30 * 60 * 8 * TPS,相对来说内存开销是可以接受的,比如TPS为1w那么大概开销为200M+。
之后的数据按照每30分钟一个块的形式写入文件,那么每个整点时的操作就是计算一下将30分钟的消息Hash到对应的TimeWheel上,那么排序问题就解决了。
到此为止就只剩下一个问题,如何保存30天的数据?
CommitLog保存超长延迟的数据
CommitLog是有时效性的,比如在我们只保存最近7天的消息,过期数据将被删除。对于延迟消息,可能需要30天之后投递,显然是不能被删除的。
那么我们怎么保存延迟消息呢?
直观的方法就是将延迟消息从CommitLog中剥离出来,独立存储以保存更长的时间。
通过DispatchService将WAL中的延迟消息写入到独立的文件中。这些文件按照延迟时间组成一个链表。
链表长度为最大延迟时间/每个文件保存的时间长度。
那么WAL可以按照正常的策略进行过期删除,Delay Msg File则在一个文件投递完之后进行删除。
唯一的问题是这里会有Delay Msg File带来的随机写问题,但是这个对系统整体性能不会有很大影响,在可接受范围内。
BOUNS
结合TimeWheel和CommitLog保存超长延迟数据的方案,加上一些优化手段,基本就完成了支持任意延迟时间的方案:
- 消息写入WAL
- Dispatcher处理延迟消息
- 延迟消息一定时间的直接写入TimeWheel
- 延迟超过一定时间写入DelayMessageStorage
- DelayMessageStorage对DelayMsgFile构建一层索引,这样在映射到TimeWheel时只需要做一次Hash操作
- 通过TimeWheel将消息投递到ConsumeQueue中完成对Consumer的可见
通过这个方案解决了最初提出来的任意延迟消息的两个难点:
- 消息的排序问题
- 超长延迟消息的存储问题
最后
本文从延迟消息的概念出发,了解业界的支持情况,确定延迟消息的难点和支持边界,最后通过一步步推导完成了一个相对来说从内存开销和性能上都可以满足期望的方案。
对本文有任何问题欢迎通过公公众号留言或添加我的微信交流。
如何在MQ中实现支持任意延迟的消息?的更多相关文章
- 如何在DBGrid中能支持多项记录的选择
When you add [dgMultiSelect] to the Options property of a DBGrid, you give yourself the ability to s ...
- 如何在 Istio 中支持 Dubbo、Thrift、Redis 以及任何七层协议?
赵化冰,腾讯云高级工程师,Istio Member,ServiceMesher管理委员,Istio 项目贡献者, Aerika 项目创建者 ,热衷于开源.网络和云计算.目前主要从事服务网格的开源和研发 ...
- 服务化改造实践 | 如何在 Dubbo 中支持 REST
什么是 REST REST 是 Roy Thomas Fielding [[1]](#fn1) 在 2000 年他的博士论文 [[2]](#fn2) “架构风格以及基于网络的软件架构设计” 中提出来的 ...
- 如何在html中把一个图片或者表格覆盖在一张已有图片上的任意位置
如何在html中把一个图片或者表格覆盖在一张已有图片上的任意位置 <div style="position:relative;"> <img src=&quo ...
- Kafka深度解析(如何在producer中指定partition)(转)
原文链接:Kafka深度解析 背景介绍 Kafka简介 Kafka是一种分布式的,基于发布/订阅的消息系统.主要设计目标如下: 以时间复杂度为O(1)的方式提供消息持久化能力,即使对TB级以上数据也能 ...
- 1.如何在JMeter中使用JUnit
您是否需要在测试过程中使用JUnit? 要回答这个问题,我们先来看看单元测试. 单元测试是软件测试生命周期中测试的最低分辨率. 运行单元测试时,需要在应用程序中使用最小的可测试功能,将其与其他代码隔离 ...
- 我是如何在SQLServer中处理每天四亿三千万记录的
首先声明,我只是个程序员,不是专业的DBA,以下这篇文章是从一个问题的解决过程去写的,而不是一开始就给大家一个正确的结果,如果文中有不对的地方,请各位数据库大牛给予指正,以便我能够更好的处理此次业务. ...
- 【转】我是如何在SQLServer中处理每天四亿三千万记录的
原文转自:http://blog.jobbole.com/80395/ 首先声明,我只是个程序员,不是专业的DBA,以下这篇文章是从一个问题的解决过程去写的,而不是一开始就给大家一个正确的结果,如果文 ...
- 如何在Dreamweaver中使用emmet
by zhangxinxu from http://www.zhangxinxu.com本文地址:http://www.zhangxinxu.com/wordpress/?p=3666 一.emmet ...
随机推荐
- View处理常用方法封装
处理View常用的一些方法:Drawable和Bitmap互相转换,Bitmap改变大小,dp.px互相转换,sp.px互相转换,根据Id查找Drawable,获取屏幕大小等方法. import an ...
- CentOS环境下Docker私有仓库搭建
本文讲述如何搭建docker私有仓库. 有了docker hub,为什么还要搭建docker私有仓库? 1.性能考虑:docker hub的访问要通过互联网,性能太低. 2.安全性:更多的时候,镜像不 ...
- JDK1.7中HashMap底层实现原理
一.数据结构 HashMap中的数据结构是数组+单链表的组合,以键值对(key-value)的形式存储元素的,通过put()和get()方法储存和获取对象. (方块表示Entry对象,横排表示数组ta ...
- java连接VMware虚拟机Oracle数据库问题
最近在电脑上装了虚拟机,为的是在虚拟机上安装Oracle数据库,Oracle实在太占内存,配置低的电脑装个Oracle几乎就瘫了,没办法,搞个虚拟机玩玩.我虚拟机用的是xp系统,顺便怀念下经典.装好O ...
- HDU1171--01背包
Big Event in HDU Time Limit: 10000/5000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others ...
- 38.Linux驱动调试-根据系统时钟定位出错位置
当内核或驱动出现僵死bug,导致系统无法正常运行,怎么找到是哪个函数的位置导致的? 答,通过内核的系统时钟,因为它是由定时器中断产生的,每隔一定时间便会触发一次,所以当CPU一直在某个进程中时,我们便 ...
- Nodejs学习笔记(十七)--- 浮点运算decimal.js
目录 前言 JavaScript加减乘除运算 decimal.js加减乘除运算 前言 开发过程中免不了有浮点运算,JavaScript浮点运算的精度问题会带来一些困扰 JavaScript 只有一种数 ...
- day2-Python基本数据类型介绍
百度云连接 链接:https://pan.baidu.com/s/1hsGQx7m 密码:u07q
- 鼠标悬停,图片放大 CSS实现
因为最近做的项目刚好用到了这个实现,分享出来 class=enlarge 为div标签的class div img 为标签 .enlarge div img:hover{ transform: s ...
- 升级安装php7要考虑的几个方面
php7从推出到现在,前两天官方已经发布到7.2版本,把自己升级和安装php7中遇到的问题和解决方法做一个笔记,内容主要包括:1) mysql2) php3) nginx4) memcache扩展5) ...