一、常用MQ产品
RabbitMQ | RocketMQ | Kafka | |
语言 | Erlang | Java | Scala/Java |
单机吞吐量 | 万级 | 十万级 | 十万级 |
时效性 | us级 | ms级 | ms级以内 |
可用性 | 高 | 非常高(分布式) | 非常高(分布式) |
可靠性 | 可做到 0 丢失 | 可做到 0 丢失 | |
topic 数量对吞吐量的影响 | 可以支持大量的 topic, topic达到几百几千的级别,吞吐量会有较小幅度下降 | topic 从几十到几百个的时,吞吐量会大幅度下降,在同等机器数量下,Kafka尽量保证 topic 数量不要过多,如果需要支撑大规模的 topic,需要增加机器资源 | |
特色 | 1.支持非常灵活的路由配置,Producer和Queue之间增加了Exchange模块 | kafka在topic特别多的情况下性能会有大幅度下降而rocketmq依然坚挺 | |
优势 | 1.客户端支持语言比较广泛 2.轻量,开箱即用,容易部署和使用 3.拥有灵活的路由配置 4.性能极好,延时很低 | 1.社区非常活跃,阿里出品经历过多次大促的洗礼验证 2.扩展或者二次开发容易些 | 1.Kafka与周边生态系统的兼容性是最好的没有之一,尤其在大数据和流计算领域,几乎所有的相关开源软件都会优先支持 Kafka 2.设计上大量使用了批量和异步的思想,超高的异步收发性能 |
缺点 | 1.消息挤压支持会导致性能急剧下降 2.性能对比其他消息队列稍差 3.编程语言比较小众,二次开发或扩展比较困难 | 国际上兼容性稍差,正在逐步改善 | 1.异步批量的设计(先攒一波再一起处理)会导致同步收发消息响应的时延比较高,不太适合在线业务场景 |
RabbitMQ:
a、Exchange的作用和交换机也非常相似,可以根据配置的路由规则将生产者发出的消息分发到不同的队列中。路由的规则也非常灵活,可以自定义实现路由规则
b、对消息挤压支持不好,在它的设计理念中消息队列只是一个管道,大量的消息积压会导致RabbitMQ的性能急剧下降,是一种不正常的情况,应尽量避免
kafka:
a、适用于处理海量的消息,像收集日志、监控信息或是前端的埋点这类数据,或是大数据、流计算相关的场景
b、在配置高些的服务器对Kafka进行过压测,在开启压缩的情况下,Kafka 的极限处理能力可以超过每秒2000万条消息。
Pulsar:新兴的开源消息队列,最早是由 Yahoo 开发,目前处于成长期,流行度和成熟度相对没有那么高。与其他消息队列最大的不同是,Pulsar 采用存储和计算分离的设计,可能会引领未来消息队列的一个发展方向
中间件选择考量维度:可靠性,性能,功能,可运维行,可拓展性,是否开源及社区活跃度
二、消息模式
1、队列模型(Queue Pattern)
消息顺序:生产者的发送顺序
消费者:存在竞争的关系,每个消费者只能收到队列中的一部分消息(任何一条消息只能被其中的一个消费者收到)
2、发布 - 订阅模型(Publish-Subscribe Pattern)
订阅者:在接收消息之前需要先“订阅主题”。“订阅”可以认为是主题在被消费时的一个逻辑副本,每份订阅中,订阅者都可以接收到主题的所有消息
在发布 - 订阅模型中,如果只有一个订阅者,它和队列模型基本是一样的。即:发布 - 订阅模型在功能层面上是可以兼容队列模型的
rabbitMq中的订阅模式实现
RabbitMQ中通过Exchange + Queue 实现订阅模式,Exchange 位于生产者和队列之间,生产者只需要将消息发送给 Exchange,由 Exchange 上配置的策略来决定将消息投递到哪些队列中,同一份消息如果需要被多个消费者来消费,需要配置 Exchange 将消息发送到多个队列,每个队列中都存放一份完整的消息数据,可以为一个消费者提供消费服务。
RabbitMQ 还提供了“死信队列”的功能,它会自动把反复消费都失败的消息丢到这个特殊的死信队列中,避免一条消息卡主队列的情况。
三、消息队列的应用场景
异步处理(流程异步化,提高响应速度)、流量控制(消峰填谷,根据下游处理能力自动调节流量)、服务解藕、流计算
mq的引入可能会带来:延迟问题、数据不一致、复杂度增加(调用链环节增加导致总响应时延变长)等问题
1、分布式事务实现最终一致性
半消息:消息在事务提交之前,对于消费者不可见
问题:如果在第4步出现失败而2、3部分完成,需要怎么处理?
kafka:提交失败直接抛异常,让用户自行处理
RocketMQ:增加事务反查机制来解决事务消息提交失败的问题
Producer 在提交或回滚事务消息时发生网络异常,Broker没有收到提交或者回滚的请求,会定期去 Producer 上反查这个事务对应的本地事务的状态,然后根据反查结果决定提交或者回滚这个事务。
为了支撑这个事务反查机制,业务代码需要实现一个反查本地事务状态的接口,告知 RocketMQ 本地事务是成功还是失败。
步骤:
1.通过 producer.sendMessageInTransaction(msg, null) 发送半消息
2.在 TransactionListener#executeLocalTransaction中执行本地事物(创建、保存订单等),返回 LocalTransactionState
3. 如果出现异常未获取到LocalTransactionState,则通过TransactionListener#checkLocalTransaction反查本地事物的状态
4.获取到LocalTransactionState则按照commit或rollback进行相关业务流程处理
public static void main(String[] args) throws MQClientException, InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(10); TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name"); producer.setExecutorService(executorService); producer.setTransactionListener(new TransactionListener() { @Override public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { //处理本地事物,如创建、保存订单等 return LocalTransactionState.COMMIT_MESSAGE; } @Override public LocalTransactionState checkLocalTransaction(MessageExt msg) { Integer orderStatus = getOrderStatus();//自定义实现 if (null != orderStatus) { switch (orderStatus) { case 0: return LocalTransactionState.UNKNOW; case 1: return LocalTransactionState.COMMIT_MESSAGE; case 2: return LocalTransactionState.ROLLBACK_MESSAGE; } } return LocalTransactionState.COMMIT_MESSAGE; } }); producer.start(); for (int i = 0; i < 10; i++) { try { Message msg = new Message("topic1","hello MQ".getBytes(StandardCharsets.UTF_8)); SendResult sendResult = producer.sendMessageInTransaction(msg, null); System.out.printf("%s%n", sendResult); Thread.sleep(10); } catch (MQClientException e) { e.printStackTrace(); } } for (int i = 0; i < 100000; i++) { Thread.sleep(1000); } producer.shutdown(); }
四、常见问题
1、MQ队列是如何实现 “请求 - 确认”的 ?
首先请求确认机制的出现是为了确保消息不会在传递过程中因为网络或服务器故障而丢失,具体做法:
如图:生产者发送消息后,broker会响应写入成功,如果没有收到写入成功或写入失败,则重新发送消息,消费者在收到消息并完成业务操作后响应broker消费成功,否则broker会认为消费失败,重新该消息直到消费者消费成功。
问题:该机制虽然保证了消息传递过程中的可靠性,但为了确保消息的有序性,在某一条消息被成功消费之前,下一条消息是不能被消费的,否则就会出现消息空洞(如:1(2)3 ,2没有收到,那2就是一个空洞消息)违背了有序性这个原则。即:在该机制下每个主题在任意时刻,只能有一个消费者实例进行消费,没法通过水平扩展消费者数量提升消费性能
解决:RocketMQ中每个主题包含多个队列,通过多个队列来实现多实例并行生产和消费,做到队列上的消息有序
a、每个消费组可消费主题中的完整消息,不同组之间不受影响,即:一条消息被 goup1 消费过,可再次被group2 消费
b、消费组中存在多个消费者,同一个组内的消费者是竞争消费的关系,每个消费者负责消费组内的一部分消息。如果一条消息被消费者 Consumer1 消费了,那同组的其他消费者就不会再收到这条消息。
c、Topic的消费过程中,由于消息需要被不同的组进行多次消费,所以消费完的消息并不会立即被删除,而是通过消费偏移量(Consumer Offset)进行标记,位置之前的消息表示被消费过的,之后的消息是没有被消费的,每成功消费一条消息,消费位置就加一。(丢消息的原因大多是由于消费位置处理不当导致的)
PS:相同的消息 producer 只会往某个队列里面发送一次,在消费组中,每个队列上只能串行消费(同一时刻只能被一个consumer占用),多个队列之间可并行消费,并行度就是队列数量,数量越多并行度越大,所以水平扩展可以提升消费性能。
2、如何确保消息不丢失?
消息是否丢失校验:利用消息的拦截器在发送消息前将有序递增的序号添加到消息中,消费端进行序号的有序行检测进行校验(注意:Topic 上的消息不是有序的,可以增加分区标识,在每个分区内判断是否有序)
那些环节可能会丢消息? 应怎么做才能不丢消息?
生产阶段:
使用请求确认机制保证消息的可靠传递,只要 Producer收到Broker的确认响应,就可以保证消息在生产阶段不丢失。有些消息队列在长时间没收到发送确认响应后,会自动重试,如果重试再失败,会以返回值或者异常的方式告知用户,正确处理返回值或者捕获异常,就可以保证这个阶段的消息不会丢失,MQ异步发送时,需要在回调方法里进行检查
存储阶段:Broker出现故障(进程卡死或者服务器宕机)可能会导致丢失消息,可以通过配置 Broker 参数来避免因为宕机丢消息(写入副本后再响应)
消费阶段:不要在收到消息后就立即发送消费确认,而是应该在执行完所有消费业务逻辑之后,再发送消费确认(注意服务幂等问题)
3、消息积压处理方式
原因:消费端的消费性能 < 生产端的发送性能(消费端网络、系统故障、生产端扩容 等等)
分析:90%的错误基本上都是由业务代码本身导致,遇事首先分析自身系统问题
1、分析系统日志,观察是否有消费或生产相关的异常情况
2、通过MQ的监控功能分析监控数据,确定流量是否出现生产或消费的明显波动
3、定位上游生产者的生产性能是否增大,中游消息队列存储层是否异常,下游消费速度是否变慢
4、分析网络或硬件环境是否有波动或异常
应对消息积压常见处理方式:
生产端 消费端 处理方式 1、服务降级,适当关闭非核心业务,减少消息发送量
2、降低发送端并发及批量大小(一般不使用)
1、服务临时水平扩容
2、增加并发进行批量消费,提高并行能力(增加并发需要同步扩容分区数量,否则是起不到效果)
缺点 1、降级服务的数据需要手动或后续补偿处理
2、需改动程序重新上线,另外如果降低发送端处理能力,可能会导致集群无法及时响应外部请求,反而得不偿失
2、某一条数据消费失败,可能会引起很多消息重试,另外多线程消费时,需要考虑消息的顺序(无序)
4、不要求严格顺序,如何实现单个队列的并行消费?
原则上一个队列同一时刻只能被一个消费组中的consumer占用,如果想实现在单队列上进行并行消费就需要避免因为顺序性而导致的消息空洞问题(如:1(2)3 ,2没有收到,那2就是一个空洞消息)
场景:某一队列中现有消息 1、2、3、4,有2个 consumer A 、B 同时进行消费,
举例:A 处理1,A无影响,B处理2,B响应成功,此时先将消息1复制到重试队列,然后更新offset=2,如B再次拉取时,从重试队列中下发消息1
思路:为防止消息offset更新而导致出现消息空洞,在更新offset时先判断是否存在 <offset 未响应的消息,如果存在则将该消息复制到一个特殊的重试队列中,然后再更新偏移量,下次消费时优先从此队列中返回消息。这种实现方式的并行消费开销比较大,不应该作为一个常规提升消费并发的手段
5、如何实现消息的顺序消费 ?
rocketMQ在主题层面无法保证消息的顺序,如果想要实现消息的顺序消费,需采取指定队列发送来实现
- 通过消息的唯一ID(如订单ID)进行一致性hash算法计算出要发送到topic中那个Qid
- 按照获取的QID进行消息发送,保证ID的消息总被发送到同一个队列上来实现顺序消费
缺点:
- 主从复制方式架构的RocketMQ集群,主从关系通过配置进行设置,不支持动态切换。
- 主节点宕机,生产者就不能再生产消息了,但是不会影响消费端,消费者可以自动切换到从节点继续进行消费。
- 在这种复制模式下,严格顺序和高可用只能选择一个
解决:RocketMQ新复制方式引入Dledger,
- Dledger 在写入消息的时候,要求至少消息复制到半数以上的节点之后,才给客户端返回写入成功,
- 支持选举方式动态切换主节点,Dledger在选举时,总会把数据和主节点一样的从节点选为新的主节点,来保证数据的一致性,既不会丢消息,还可以保证严格顺序。
Dledger 复制方式缺点:
- 选举过程中不能提供服务
- 最少需要 3 个节点才能保证数据一致性(半数以上原则)
- 由于要求写入需半数以上的节点才返回写入成功,性能不如主从异步复制
持续完善中.................
PS:
RocketMQ 中文:https://gitee.com/apache/rocketmq/tree/master/docs/cn#apache-rocketmq开发者指南