面试消息对列八股笔记
消息对列
为什么使用消息对列(消息对列一般用于什么场景)
解耦,异步,削峰
面对严重耦合的系统,系统自身不仅要负责维护接口,还要考虑接口对应的系统挂掉了之后如何响应,过于复杂
通过消息对列将自身重要的信息发送到消息队列中存储,当别的系统有需要的时候直接去消息对列去取,避免了复杂的接口维护,解耦系统
a负责b,c,d系统的响应,当b,c,d发送请求后,如果不采用消息对列,a就要自己负责维护,到时候就要考虑请求限流和失败重连,做了很多与业务无关的事情,a直接将对应的消息发送到消息对列之后,b,c,d系统直接从对列中取出即可,避免自己维护
一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但是其实这个调用是不需要直接同步调用接口的,如果用 MQ 给它异步化解耦,也是可以的,你就需要去考虑在你的项目里,是不是可以运用这个 MQ 去进行系统的解耦。在简历中体现出来这块东西,用 MQ 作解耦
异步:
如果有些操作不是必须要同步进行,比如数据库的备份。自己读取用户信息之后写入并直接返回,然后将备份指令存储到队列中,其余系统获取消息进行备份,这样可以避免用户长时间不必要的等待
削峰:
当请求具有高频频段的时候,如果一味的放入到系统,系统很难完全响应,甚至有时候会死机。为了避免这种情况,可以将请求存储到队列中,系统按照自己能够处理的量每次从队列中取得对应的量,保证系统不会被高峰请求崩溃掉,而当高峰时间过了之后就可以继续操作。
比如用户的mysql请求在10点会达到5k,而一般的mysql最多抗住2k的请求。通过消息对列存储,每次系统只取2k,避免数据库一下子被打满。
消息队列的优点和缺点
优点就是在特定场景下,可以解耦,异步,削峰,即解耦合减少系统设计的复杂性,提升系统的响应速度和系统的稳定性
缺点就是要消息对列本身必须可靠,如果消息对列因为自身原因导致消息丢失,那用户或者说系统自身是感知不到的,因为消息都存储在消息对列,你必须读取到对应的返回消息才能判断是什么呢情况
即系统的可用性降低(如何保证消息对列高可用),系统的复杂度提高(如何处理重复消费和消息丢失),系统的数据一致性问题(如何保证请求的一致性)
kafka,ActivateMQ, RabbitMQ, RocketMQ的区别是什么,适合那些场景
kafka:单机吞吐达到10w级,一般配合大数据机型实时计算和日志采集;topic数量从10-100的时候,吞吐量波动很大,延迟低在ms级别,高可用采用分布式存储,消息可靠性与RocketMQ一致,功效简单,支持简单的MQ功能
RocketMQ:单机吞吐达到10w级,topic可以达到100-1000级别,吞吐量小时有较小优势,ms级延迟,高可用,采用分布式架构,经过优化可以做到消息0丢失,功能完善,分布式
RabbitMQ:单机吞吐达到1w级别,微秒级别响应,基于主从架构,基本不丢失消息,基于erlang开发,并发能力强,性能极好
ActivateMQ:单机吞吐也是1w级别,基于主从架构可实现高可用,较低的丢包概率,MQ领域功能和完善
如何选择:
中小型公司,技术实力较为一般(对elang语言不熟悉的),技术挑战不是特别高,用 RabbitMQ 是不错的选择
大型公司,基础架构研发实力较强,用 RocketMQ 是很好的选择。
大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的
如何保证消息队列的高可用
MQ 的高可用性怎么保证?这样就是你用过哪个 MQ,你就说说你对那个 MQ 的高可用性的理解
RabbitMQ 的高可用性:RabbitMQ 是比较有代表性的,是基于主从(非分布式)做高可用性的
RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式
单机模式:本地节点,存信息和元信息,不保证高可用
普通集群模式:多台机器上启动多个 RabbitMQ 实例,每台机器启动一个。你创建的 queue,只会放在一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据(元数据可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)
queue实例存储消息元信息,服务器连接queue实例后获取消息元信息,告诉实例去存储的地方拉去消息来消费(主要用于提高吞吐量)
镜像集群模式下,你创建的 queue,无论是元数据还是 queue 里的消息都会存在于多个实例上,就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。(优点保证了高可用以及数据一致性,缺点:数据的复制和一致性维护压力大,无法线性扩展)已废弃:RabbitMQ 4.0 版本起
Quorum Queues(仲裁队列推荐使用):
数据一致性:基于 Raft 算法,保证消息在 quorum 多数节点确认后才算写入成功
自动故障转移:leader 节点宕机后,自动重新选举,无需手动干预
线性扩展:可以独立扩展副本数量,不受单节点容量限制
持久化优化:支持段式存储,滚动刷新时性能更优
Kafka 的高可用性
kafka:由多个 broker 组成,每个 broker 是一个节点;你创建一个 topic,这个 topic 可以划分为多个 partition,每个 partition 可以存在于不同的 broker 上,每个 partition 就放一部分数据,天然的分布式消息队列,就是说一个 topic 的数据,是分散放在多个机器上的,每个机器就放一部分数据。
Kafka 0.8 以后,提供了 HA 机制,就是 replica(复制品) 副本机制。每个 partition 的数据都会同步到其它机器上,形成自己的多个 replica 副本。所有 replica 会选举一个 leader 出来,那么生产和消费都跟这个 leader 打交道,然后其他 replica 就是 follower。写的时候,leader 会负责把数据同步到所有 follower 上去,读的时候就直接读 leader 上的数据即可。只能读写 leader?很简单,要是你可以随意读写每个 follower,那么就要 care 数据一致性的问题,系统复杂度太高,很容易出问题。Kafka 会均匀地将一个 partition 的所有 replica 分布在不同的机器上,这样才可以提高容错性
如何保证消息的可靠性传输/如何处理消息丢失问题
消息可能在哪里丢失:生产者,MQ(消息对列),消费者
生产者:生产后发送到MQ时由于网络抖动未送达:
可以选择用 RabbitMQ 提供的事务功能,就是生产者发送数据之前开启 RabbitMQ 事务 channel.txSelect() ,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务 channel.txRollback() ,然后重试发送消息;如果收到了消息,那么可以提交事务 channel.txCommit() ;优点:消息完全不会丢失,缺点:同步阻塞,增加吞吐量降低性能
使用confim机制,在生产者那里设置开启 confirm 模式之后,你每次写的消息都会分配一个唯一的 id,然后如果写入了 RabbitMQ 中,RabbitMQ 会给你回传一个 ack 消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你的一个 nack 接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。
优点:异步发送,不用阻塞主进程,提升了吞吐量,但是增加了通信成本
cofirm机制:
普通 confirm 模式:每发送一条消息后,调用 waitForConfirms() 方法,等待服务器端 confirm,如果服务端返回 false 或者在一段时间内都没返回,客户端可以进行消息重发。
批量 confirm 模式:每发送一批消息后,调用 waitForConfirms() 方法,等待服务端 confirm。
异步 confirm 模式:提供一个回调方法,服务端 confirm 了一条或者多条消息后客户端会回调这个方法。
MQ自身丢失消息:开启MQ信息持久化,将其存储在磁盘中
持久化步骤:
创建 queue 的时候将其设置为持久化。这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue 里的数据的
第二个是发送消息的时候将消息的 deliveryMode 设置为 2。就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去
消费端丢失数据:
用 RabbitMQ 提供的 ack 机制,简单来说,就是你必须关闭 RabbitMQ 的自动 ack ,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里 ack 一把。这样的话,如果你还没处理完,不就没有 ack 了?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。
综合:为了避免消息的可靠性传输,MQ提供了消息确认机制,一种是同步的事务性,发送消息后确认事务执行完成才返回,一种是异步的消息confirm机制,通过发送ack请求来确认自己是否实现接收到了消息
RocketMQ
消息丢失的场景
生产者发送消息到 MQ 有可能丢失消息
MQ 收到消息后写入硬盘可能丢失消息
消息写入硬盘后,硬盘坏了丢失消息
消费者消费 MQ 也可能丢失消息
整个 MQ 节点挂了丢失消息
生产者发送消息时如何保证不丢失?
解决发送时消息丢失的问题可以采用 RocketMQ 自带的事务消息机制
事务消息原理:首先生产者会发送一个half 消息(对原始消息的封装),该消息对消费者不可见,MQ 通过 ACK 机制返回消息接受状态, 生产者执行本地事务并且返回给 MQ 一个状态(Commit、RollBack 等),如果是 Commit 的话 MQ 就会把消息给到下游, RollBack 的话就会丢弃该消息,状态如果为 UnKnow 的话会过一段时间回查本地事务状态,默认回查 15 次,一直是 UnKnow 状态的话就会丢弃此消息。
为什么先发一个 half 消息,作用就是先判断下 MQ 有没有问题,服务正不正常。
MQ 收到消息后写入硬盘如何保证不丢失?
数据存盘绕过缓存,改为同步刷盘,这一步需要修改 Broker 的配置文件,将 flushDiskType 改为 SYNC_FLUSH 同步刷盘策略,默认的是 ASYNC_FLUSH 异步刷盘,一旦同步刷盘返回成功,那么就一定保证消息已经持久化到磁盘中了。
消息写入硬盘后,硬盘坏了如何保证不丢失?
为了保证磁盘损坏导致丢失数据,RocketMQ 采用主从机构,集群部署,Leader 中的数据在多个 Follower 中都存有备份,防止单点故障导致数据丢失。
Master 节点挂了怎么办?Master 节点挂了之后 DLedger 登场
接管 MQ 的 commitLog
选举从节点
文件复制 uncommited 状态 多半从节点收到之后改为 commited
消费者消费 MQ 如何保证不丢失?
如果是网络问题导致的消费失败可以进行重试机制,默认每条消息重试 16 次
多线程异步消费失败,MQ 认为已经消费成功但是实际上对于业务逻辑来说消息是没有落地的,解决方案就是按照 mq 官方推荐的先执行本地事务再返回成功状态。
整个 MQ 节点挂了如何保证不丢失?
这种极端情况可以消息发送失败之后先存入本地,例如放到缓存中,另外启动一个线程扫描缓存的消息去重试发送。
如何保证消息不被重复消费
重复消费会导致什么
重复消费不可怕,可怕的是你没考虑到重复消费之后,怎么保证幂等性,数据可能出现不一致性
什么时候会被重复消费?
针对RMQ:消费端发送消息到MQ,MQ突然断电然后重启,消费端并没有接收到对应的ack然后又再次发了一个消息,但是本地已经保存了一个对应的ack,此时消息对列有两个同样的消息,使得消费者重复消费了两次
Kafka 实际上有个 offset 的概念,就是每个消息写进去,都有一个 offset,代表消息的序号,然后 consumer 消费了数据之后,每隔一段时间(定时定期),会把自己消费过的消息的 offset 提交一下,表示“我已经消费过了,下次我要是重启啥的,你就让我继续从上次消费到的 offset 来继续消费吧”
但是有时候也不能保证会重复消费,此时要根据具体业务具体分析:
数据库唯一约束(最简单可靠)
Redis分布式锁(高并发场景)
状态机控制(有明确状态流转的业务)
消费记录表(通用方案,需定期清理)
如何保证消息的顺序性
重要性:数据的操作必须要有顺序性,否则导致幂等性被破坏
RabbtiMQ:
拆分多个queue,每个queue一个消费者
一个 queue 但是对应一个 consumer,然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理
消费者不直接消费消息,而是将消息根据关键值(比如:订单 id)进行哈希,哈希值相同的消息保存到相同的内存队列里。也就是说,需要保证顺序的消息存到了相同的内存队列,然后由一个唯一的 worker 去处理
KafKa:
一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低,一般不会用
写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。
总结:为了保证消息的顺序性:要求同 key 同队列,同队列同线程,线程内单线程消费
如何解决消息对列的延时以及过期失效问题?
即消费端出现问题,不进行消费了,你应该怎么做?
具体步骤:
修复consumer的问题,确保能够恢复其消费速度,然后将现有的consumer停掉
新建一个topic,partition未原来的10倍,建立好原来的10倍的queue数量
写一个临时的分发数据的consumer程序,部署上去消费积压的数据,消费后不做耗时处理,直接轮询写入建立好的queue中
临时征用10倍的机器部署consumer,每一批consumer消费一个临时的queue的数据(相当于将queue和consumer临时扩大十倍,以正常的速度消费数据)
消费完数据之后,回复原先的部署的架构,重新用原来的consumer机器消费消息
如果是RabbitMQ:
可以设定消息过期时间,但是过期的数据就丢失了,重新扩容consumer和queue并不起作用,只能等到晚上自己慢慢的查日志消息,手动放到消息队列中进行消费
MQ写满了:
直接丢弃,然后晚上补数据,使用第一个的方法太慢了,因为消息对列的移动也很话费时间,尤其是消息对列快满了的情况下
RocketMQ,官方针对消息积压问题,提供了解决方案:
- 提高消费并行度
绝大部分消息消费行为都属于 IO 密集型,即可能是操作数据库,或者调用 RPC,这类消费行为的消费速度在于后端数据库或者外系统的吞吐量,通过增加消费并行度,可以提高总的消费吞吐量,但是并行度增加到一定程度,反而会下降。所以,应用必须要设置合理的并行度。 如下有几种修改消费并行度的方法:
同一个 ConsumerGroup 下,通过增加 Consumer 实例数量来提高并行度(需要注意的是超过订阅队列数的 Consumer 实例无效)。可以通过加机器,或者在已有机器启动多个进程的方式。提高单个 Consumer 的消费并行线程,通过修改参数 consumeThreadMin、consumeThreadMax 实现。 - 批量方式消费
某些业务流程如果支持批量方式消费,则可以很大程度上提高消费吞吐量,例如订单扣款类应用,一次处理一个订单耗时 1 s,一次处理 10 个订单可能也只耗时 2 s,这样即可大幅度提高消费的吞吐量,通过设置 consumer 的 consumeMessageBatchMaxSize 返个参数,默认是 1,即一次只消费一条消息,例如设置为 N,那么每次消费的消息数小于等于 N。 - 跳过非重要消息
发生消息堆积时,如果消费速度一直追不上发送速度,如果业务对数据要求不高的话,可以选择丢弃不重要的消息。例如,当某个队列的消息数堆积到 100000 条以上,则尝试丢弃部分或全部消息,这样就可以快速追上发送消息的速度。
优化每条消息消费过程
举例如下,某条消息的消费过程如下:
根据消息从 DB 查询【数据 1】
根据消息从 DB 查询【数据 2】
复杂的业务计算
向 DB 插入【数据 3】
向 DB 插入【数据 4】
这条消息的消费过程中有 4 次与 DB 的 交互,如果按照每次 5ms 计算,那么总共耗时 20ms,假设业务计算耗时 5ms,那么总过耗时 25ms,所以如果能把 4 次 DB 交互优化为 2 次,那么总耗时就可以优化到 15ms,即总体性能提高了 40%。所以应用如果对时延敏感的话,可以把 DB 部署在 SSD 硬盘,相比于 SCSI 磁盘,前者的 RT 会小很多
如何设计一个消息对列
参考现有的kafka
消息对列可伸缩:需要时快速扩容,就可以增加吞吐量。采用设计分布式系统,参考kafka设计 broker -》 topic -》 partition,每个partition存放在一台机器保证高可用,资源不够给topic增加parititon进行分区
消息持久化:避免消息在MQ中丢失,采用顺序读写磁盘,先攒一批,等够了之后采用顺序写的方式落盘,效率更高
高可用:多副本+多基架保证数据不丢失,leader + 多follower保证消息发送稳定
浙公网安备 33010602011771号