消息队列是怎样设计的

Posted EnjoyMoving

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了消息队列是怎样设计的相关的知识,希望对你有一定的参考价值。

之前阅读过一篇美团的技术文章《消息队列设计精要》,觉得写的不错,可以让人更加了解消息队列

消息队列的设计要点,大致总结整理如下:

  • 最简单的消息队列可以做成一个消息转发器,把一次RPC做成两次RPC。

    • 构建一个整体的数据流,例如producer发送给broker,broker发送给consumer,consumer回复消费确认,broker删除/备份消息等。

    • 利用RPC将数据流串起来(可以使用开源RPC框架,比如Dubbo等)

      • 负载均衡、

      • 服务发现、

      • 通信协议:Thrift,Dubbo

      • 序列化协议

  • 存储子系统:通过存储承载消息堆积,然后在合适的时机投递消息

    • 持久化、非持久化;

    • 从速度来看,文件系统 > 分布式KV(持久化)> 分布式文件系统 > 数据库,而可靠性却截然相反。需要根据具体业务选择

  • 消费关系的保存:

    • 单播:点对点

    • 多播:一点对多点

    • 广播关系的维护,一般由于消息队列本身都是集群,所以都维护在公共存储上,如config server、zookeeper等。

  • 消费确认(任务执行完整性):

    • 把消息的送达和消息的处理分开,这样才真正的实现了消息队列的本质-解耦。

    • 对于没有特殊逻辑的消息,默认Auto Ack也是可以的

    • 但一定要允许消费方主动进行消费确认ack,并与broker约定下次投递时间

  • 高级特性:

    • 可靠投递(最终一致性):

      • 高可用:超时重发与消息重复、消息队列幂等设计(broker多机器共享一个DB或者一个分布式文件/kv系统)、定时任务补偿

      • 不是所有的系统都要求最终一致性或者可靠投递。任何基础组件要服务于业务场景。

      • 消息可能会重复,并且在异常情况下,要接受消息的延迟。

      • 每当要发生不可靠的事情(RPC等)之前,先将消息落地,然后发送。

      • 当消息发送失败或者不知道成功失败(比如超时)时,消息状态是待发送。

        • 对于各种不确定(超时、down机、消息没有送达、送达后数据没落地、数据落地了回复没收到),其实对于发送方来说,都是一件事情,就是消息没有送达。

      • 定时任务不停轮询所有待发送消息,最终一定可以送达。

    • 重复消息:

      • 如何鉴别消息重复,并幂等的处理重复消息:

        • 版本号

        • 状态机

      • 一个消息队列server如何尽量减少重复消息的投递:

        • 鉴别消息重复:broker记录MessageId,直到投递成功后清除,重复的ID到来不做处理。

    • 顺序消息:

      • 顺序消息要求:允许消息丢失;从发送方到服务方到接受者都是单点单线程。

      • pull模式实现比较容易(见下)

      • 一个主流消息队列的设计范式里,应该是不丢消息的前提下,尽量减少重复消息,不保证消息的投递顺序。

    • 事务特性:

      • 在与本地业务的同一个事务中,本地消息落地(落库、需要业务方提供数据库)

      • 消息只要投递到服务端确认后本地才做删除

      • 定时任务扫描本地消息库表进行补偿发送

    • 性能优化

      • 异步:

        • 对于客户端来说,同步与异步主要是拿到一个Result,还是Future(Listenable)的区别。实现方式可以是线程池,NIO或者其他事件机制。

        • 服务端异步需要RPC协议支持。参考servlet 3.0规范,服务端可以吐一个future给客户端,并且在future done的时候通知客户端。

        • 实践:

          • 客户端必须等待服务端消息成功落地,才算是消息发送成功。

          • 我们不希望消息的发送阻塞客户端的主流程,所以可以先使用线程池提交一个发送请求,主流程继续往下走。

          • 服务端是纯异步。客户端的线程池wait在服务端吐回的future上,直到服务端处理完毕,才解除阻塞继续进行。

      • 批量:消费者合适消费消息:通过网络请求小包合并成大包提高性能

    • 消息消费是push还是pull:

      • push:push模型最大的致命伤是慢消费。

        • 如果消费者的速度比发送者的速度慢很多,势必造成消息在broker的堆积。

        • 最致命的是broker给consumer推送一堆consumer无法处理的消息,consumer不是reject就是error,然后来回踢皮球

        • 所以对于建立索引等慢消费,消息量有限且到来的速度不均匀的情况,pull模式比较合适。

      • pull:pull模式存在消息延迟与忙等问题

        • pull模式如果想做到全局顺序消息,就相对容易很多:

          • producer对应partition,并且单线程。

          • consumer对应partition,消费确认(或批量确认),继续消费即可。

        • 所以对于日志push送这种最好全局有序,但允许出现小误差的场景,pull模式非常合适。如果你不想看到通篇乱套的日志~~Anyway,需要顺序消息的场景还是比较有限的而且成本太高,请慎重考虑。



(完)


EnjoyMoving,与你共同成长~

如果觉得有收获,欢迎转发,谢谢~

EnjoyMoving


以上是关于消息队列是怎样设计的的主要内容,如果未能解决你的问题,请参考以下文章

RabbitMQ消息队列怎样做到服务宕机或重启消息不丢失

RabbitMQ中有大批量的消息,此时多个消费者同时访问消息队列是怎样取里面的消息的?

基于netty的消息队列设计

从0到1设计一个MQ消息队列

高并发系统设计(十五):消息队列如何降低消息队列系统中消息的延迟?

消息队列之简要设计