前言
QMQ是一款去哪儿网内部使用多年的mq。不久前(大概1-2年前)已在携程投入生产大规模使用,年前这款mq也开源了出来。关于QMQ的相关设计文章可以看这里。在这里,我假设你已经对QMQ前世今生以及其设计和实现等背景知识已经有了一个较为全面的认识。
主要功能
对于delay-server,官方已经有了一些介绍。记住,官方通常是最卖力的那个"媒婆"。qmq-delay-server其实主要做的是转发工作。所谓转发,就是delay-server做的就是个存储和投递的工作。怎么理解,就是qmq-client会对消息进行一个路由,即实时消息投递到实时server,延迟消息往delay-server投递,多说一句,这个路由的功能是由qmq-meta-server提供。投递到delay-server的消息会存下来,到时间之后再进行投递。现在我们知道了存储
和投递
是delay-server主要的两个功能点。那么我们挨个击破.
存储
假如让我们来设计实现一个delay-server,存储部分我们需要解决什么问题?我觉得主要是要解决到期投递的到期
问题。我们可以用传统db做,但是这个性能肯定是上不去的。我们也可以用基于LSM树的RocksDB。或者,干脆直接用文件存储。QMQ是用文件存储的。而用文件存储是怎么解决到期
问题的呢?delay-server接收到延迟消息,就将消息append到message_log中,然后再通过回放这个message_log得到schedule_log,此外还有一个dispatch _log用于记录投递记录。QMQ还有个跟投递相关的存储设计,即两层HashWheel。第一层位于磁盘上,例如,以一个小时一个刻度一个文件,我们叫delay_message_segment,如延迟时间为2019年02月23日 19:00 至2019年02月23日 20:00为延迟消息将被存储在2019022319。并且这个刻度是可以配置调整的。第二层HashWheel位于内存中。也是以一个时间为刻度,比如500ms,加载进内存中的延迟消息文件会根据延迟时间hash到一个HashWheel中,第二层的wheel涉及更多的是下一小节的投递。貌似存储到这里就结束了,然而还有一个问题,目前当投递的时候我们需要将一个delay_message_segment加载进内存中,而假如我们提前一个刻度加载进一个delay_message_segment到内存中的hashwheel,比如在2019年02月23日 18:00加载2019022319这个segment文件,那么一个hashwheel中就会存在两个delay_message_segment,而这个时候所占内存是非常大的,所以这是完全不可接收的。所以,QMQ引入了一个数据结构,叫schedule_index,即消息索引,存储的内容为消息的索引,我们加载到内存的是这个schedule_index,在真正投递的时候再根据索引查到消息体进行投递。
投递
解决了存储,那么到期的延迟消息如何投递呢?如在上一小节存储中所提到的,内存中的hashwheel会提前一段时间加载delay_schedule_index,这个时间自然也是可以配置的。而在hashwheel中,默认每500ms会tick一次,这个500ms也是可以根据用户需求配置的。而在投递的时候,QMQ根据实时broker进行分组多线程投递,如果某一broker离线不可用,导致投递失败,delay-server会将延迟消息投递在其他存活
的实时broker。其实这里对于实时的broker应该有一个关于投递消息权重的,现在delay-server没有考虑到这一点,不过我看已经有一个pr解决了这个问题,只是官方还没有时间看这个问题。除此之外,QMQ还考虑到了要是当前延迟消息所属的delay_segment已经加载到内存中的hashwheel了,这个时候消息应该是直接投递或也应加载到hashwheel中的。这里需要考虑的情况还是比较多的,比如考虑delay_segment正在加载、已经加载、加载完成等情况,对于这种情况,QMQ用了两个cursor来表示hashwheel加载到哪个delay_segment以及加载到对应segment的什么offset了,这里还是挺复杂的,这里的代码逻辑在WheelTickManager
这个类。