《Java架构师的第一性原理》34分布式计算之分布式消息队列(AcitveMQ、RabbitMQ、RocketMQ、Kafka、ZeroMQ)
1 总起
文章:《究竟什么时候使用MQ》
内容:
1)什么典型场景不应该使用MQ
- 上游实时关注执行结果
2)什么典型场景应该使用MQ
- 1)数据驱动的任务依赖
- 2)上游不关心下游执行结果
- 3)异步返回执行时间长
2 消息可达性
文章:《MQ,如何做到消息必达》
内容:
1)MQ系统架构
- (1)消息落地
- (2)消息超时、重传、确认
2)MQ消息发送过程
MQ消息投递上半场
- (1)MQ-client将消息发送给MQ-server(此时业务方调用的是API:SendMsg)
- (2)MQ-server将消息落地,落地后即为发送成功
- (3)MQ-server将应答发送给MQ-client(此时回调业务方是API:SendCallback)
MQ消息投递下半场
- (1)MQ-server将消息发送给MQ-client(此时回调业务方是API:RecvCallback)
- (2)MQ-client回复应答给MQ-server(此时业务方主动调用API:SendAck)
- (3)MQ-server收到ack,将之前已经落地的消息删除,完成消息的可靠投递
3)如何实现消息必达
如果消息丢了怎么办?
MQ消息投递的上下半场,都可以出现消息丢失,为了降低消息丢失的概率,MQ需要进行超时和重传。
- 上半场的超时与重传
- 下半场的超时与重传:指数退避的策略
4)消息必达带来的副作用
为了保证消息必达,可能收到重复的消息
3 消息幂等性
文章:《MQ,如何做到消息幂等》
内容:
1)消息发送,上半场幂等性
MQ消息发送上半场,即上图中的1-3
- 1,发送端MQ-client将消息发给服务端MQ-server
- 2,服务端MQ-server将消息落地
- 3,服务端MQ-server回ACK给发送端MQ-client
为了避免步骤2落地重复的消息,对每条消息,MQ系统内部必须生成一个inner-msg-id,作为去重和幂等的依据,这个内部消息ID的特性是:
- (1)全局唯一
- (2)MQ生成,具备业务无关性,对消息发送方和消息接收方屏蔽
2)消息发送,下半场幂等性
MQ消息发送下半场,即上图中的4-6
- 4,服务端MQ-server将消息发给接收端MQ-client
- 5,接收端MQ-client回ACK给服务端
- 6,服务端MQ-server将落地消息删除
为了保证业务幂等性,业务消息体中,必须有一个biz-id,作为去重和幂等的依据,这个业务ID的特性是:
- (1)对于同一个业务场景,全局唯一
- (2)由业务消息发送方生成,业务相关,对MQ透明
- (3)由业务消息消费方负责判重,以保证幂等
4 消息延时性
文章:《MQ,如何做到消息延时》
内容:
1)轮询实现延时消息
2)高效实现延时消息
- 环形队列
5 消息削峰填谷
文章:《MQ,如何做到削峰填谷》
内容:
1)MQ推拉模式
- MQ-client提供拉模式,定时或者批量拉取,可以起到削平流量,下游自我保护的作用(MQ需要做的)
2)削峰填谷实现细节
- 要想提升整体吞吐量,需要下游优化,例如批量处理等方式(消息接收方需要做的)
6 MQ夺命连环11问
消息队列作为日常常见的使用中间件,面试也是必问的点之一,一起来看看MQ的面试题。
6.1 你们为什么使用mq?具体的使用场景是什么?
mq的作用很简单,削峰填谷。以电商交易下单的场景来说,正向交易的过程可能涉及到创建订单、扣减库存、扣减活动预算、扣减积分等等。每个接口的耗时如果是100ms,那么理论上整个下单的链路就需要耗费400ms,这个时间显然是太长了。
如果这些操作全部同步处理的话,首先调用链路太长影响接口性能,其次分布式事务的问题很难处理,这时候像扣减预算和积分这种对实时一致性要求没有那么高的请求,完全就可以通过mq异步的方式去处理了。同时,考虑到异步带来的不一致的问题,我们可以通过job去重试保证接口调用成功,而且一般公司都会有核对的平台,比如下单成功但是未扣减积分的这种问题可以通过核对作为兜底的处理方案。
使用mq之后我们的链路变简单了,同时异步发送消息我们的整个系统的抗压能力也上升了。
6.2 那你们使用什么mq?基于什么做的选型?
我们主要调研了几个主流的mq,kafka、rabbitmq、rocketmq、activemq,选型我们主要基于以下几个点去考虑:
- 由于我们系统的qps压力比较大,所以性能是首要考虑的要素。
- 开发语言,由于我们的开发语言是java,主要是为了方便二次开发。
- 对于高并发的业务场景是必须的,所以需要支持分布式架构的设计。
- 功能全面,由于不同的业务场景,可能会用到顺序消息、事务消息等。
基于以上几个考虑,我们最终选择了RocketMQ。
Kafka | RocketMQ | RabbitMQ | ActiveMQ | |
单机吞吐量 | 10万级 | 10万级 | 万级 | 万级 |
开发语言 | Scala | Java | Erlang | Java |
高可用 | 分布式架构 | 分布式架构 | 主从架构 | 主从架构 |
性能 | ms级 | ms级 | us级 | ms级 |
功能 | 只支持主要的MQ功能 | 顺序消息、事务消息等功能完善 | 并发强、性能好、延时低 | 成熟的社区产品、文档丰富 |
6.3 你上面提到异步发送,那消息可靠性怎么保证?
消息丢失可能发生在生产者发送消息、MQ本身丢失消息、消费者丢失消息3个方面。
6.3.1 生产者丢失
生产者丢失消息的可能点在于程序发送失败抛异常了没有重试处理,或者发送的过程成功但是过程中网络闪断MQ没收到,消息就丢失了。
由于同步发送的一般不会出现这样使用方式,所以我们就不考虑同步发送的问题,我们基于异步发送的场景来说。
异步发送分为两个方式:异步有回调和异步无回调,无回调的方式,生产者发送完后不管结果可能就会造成消息丢失,而通过异步发送+回调通知+本地消息表的形式我们就可以做出一个解决方案。
以下单的场景举例。
- 下单后先保存本地数据和MQ消息表,这时候消息的状态是发送中,如果本地事务失败,那么下单失败,事务回滚。
- 下单成功,直接返回客户端成功,异步发送MQ消息
- MQ回调通知消息发送结果,对应更新数据库MQ发送状态
- JOB轮询超过一定时间(时间根据业务配置)还未发送成功的消息去重试
- 在监控平台配置或者JOB程序处理超过一定次数一直发送不成功的消息,告警,人工介入。
一般而言,对于大部分场景来说异步回调的形式就可以了,只有那种需要完全保证不能丢失消息的场景我们做一套完整的解决方案。
6.3.2 MQ丢失
如果生产者保证消息发送到MQ,而MQ收到消息后还在内存中,这时候宕机了又没来得及同步给从节点,就有可能导致消息丢失。
比如RocketMQ:
RocketMQ分为同步刷盘和异步刷盘两种方式,默认的是异步刷盘,就有可能导致消息还未刷到硬盘上就丢失了,可以通过设置为同步刷盘的方式来保证消息可靠性,这样即使MQ挂了,恢复的时候也可以从磁盘中去恢复消息。
比如Kafka也可以通过配置做到:
acks=all 只有参与复制的所有节点全部收到消息,才返回生产者成功。这样的话除非所有的节点都挂了,消息才会丢失。 replication.factor=N,设置大于1的数,这会要求每个partion至少有2个副本 min.insync.replicas=N,设置大于1的数,这会要求leader至少感知到一个follower还保持着连接 retries=N,设置一个非常大的值,让生产者发送失败一直重试
虽然我们可以通过配置的方式来达到MQ本身高可用的目的,但是都对性能有损耗,怎样配置需要根据业务做出权衡。
6.3.3 消费者丢失
消费者丢失消息的场景:消费者刚收到消息,此时服务器宕机,MQ认为消费者已经消费,不会重复发送消息,消息丢失。
RocketMQ默认是需要消费者回复ack确认,而kafka需要手动开启配置关闭自动offset。
消费方不返回ack确认,重发的机制根据MQ类型的不同发送时间间隔、次数都不尽相同,如果重试超过次数之后会进入死信队列,需要手工来处理了。(Kafka没有这些)
6.4 你说到消费者消费失败的问题,那么如果一直消费失败导致消息积压怎么处理?
因为考虑到时消费者消费一直出错的问题,那么我们可以从以下几个角度来考虑:
- 消费者出错,肯定是程序或者其他问题导致的,如果容易修复,先把问题修复,让consumer恢复正常消费
- 如果时间来不及处理很麻烦,做转发处理,写一个临时的consumer消费方案,先把消息消费,然后再转发到一个新的topic和MQ资源,这个新的topic的机器资源单独申请,要能承载住当前积压的消息
- 处理完积压数据后,修复consumer,去消费新的MQ和现有的MQ数据,新MQ消费完成后恢复原状
6.5 那如果消息积压达到磁盘上限,消息被删除了怎么办?
最初,我们发送的消息记录是落库保存了的,而转发发送的数据也保存了,那么我们就可以通过这部分数据来找到丢失的那部分数据,再单独跑个脚本重发就可以了。如果转发的程序没有落库,那就和消费方的记录去做对比,只是过程会更艰难一点。
6.6 说了这么多,那你说说RocketMQ实现原理吧?
RocketMQ由NameServer注册中心集群、Producer生产者集群、Consumer消费者集群和若干Broker(RocketMQ进程)组成,它的架构原理是这样的:
- Broker在启动的时候去向所有的NameServer注册,并保持长连接,每30s发送一次心跳
- Producer在发送消息的时候从NameServer获取Broker服务器地址,根据负载均衡算法选择一台服务器来发送消息
- Conusmer消费消息的时候同样从NameServer获取Broker地址,然后主动拉取消息来消费
6.7 为什么RocketMQ不使用Zookeeper作为注册中心呢?
我认为有以下几个点是不使用zookeeper的原因:
- 根据CAP理论,同时最多只能满足两个点,而zookeeper满足的是CP,也就是说zookeeper并不能保证服务的可用性,zookeeper在进行选举的时候,整个选举的时间太长,期间整个集群都处于不可用的状态,而这对于一个注册中心来说肯定是不能接受的,作为服务发现来说就应该是为可用性而设计。
- 基于性能的考虑,NameServer本身的实现非常轻量,而且可以通过增加机器的方式水平扩展,增加集群的抗压能力,而zookeeper的写是不可扩展的,而zookeeper要解决这个问题只能通过划分领域,划分多个zookeeper集群来解决,首先操作起来太复杂,其次这样还是又违反了CAP中的A的设计,导致服务之间是不连通的。
- 持久化的机制来带的问题,ZooKeeper 的 ZAB 协议对每一个写请求,会在每个 ZooKeeper 节点上保持写一个事务日志,同时再加上定期的将内存数据镜像(Snapshot)到磁盘来保证数据的一致性和持久性,而对于一个简单的服务发现的场景来说,这其实没有太大的必要,这个实现方案太重了。而且本身存储的数据应该是高度定制化的。
- 消息发送应该弱依赖注册中心,而RocketMQ的设计理念也正是基于此,生产者在第一次发送消息的时候从NameServer获取到Broker地址后缓存到本地,如果NameServer整个集群不可用,短时间内对于生产者和消费者并不会产生太大影响。
6.8 那Broker是怎么保存数据的呢?
RocketMQ主要的存储文件包括commitlog文件、consumequeue文件、indexfile文件。
Broker在收到消息之后,会把消息保存到commitlog的文件当中,而同时在分布式的存储当中,每个broker都会保存一部分topic的数据,同时,每个topic对应的messagequeue下都会生成consumequeue文件用于保存commitlog的物理位置偏移量offset,indexfile中会保存key和offset的对应关系。
CommitLog文件保存于${Rocket_Home}/store/commitlog目录中,从图中我们可以明显看出来文件名的偏移量,每个文件默认1G,写满后自动生成一个新的文件。
由于同一个topic的消息并不是连续的存储在commitlog中,消费者如果直接从commitlog获取消息效率非常低,所以通过consumequeue保存commitlog中消息的偏移量的物理地址,这样消费者在消费的时候先从consumequeue中根据偏移量定位到具体的commitlog物理文件,然后根据一定的规则(offset和文件大小取模)在commitlog中快速定位。
6.9 Master和Slave之间是怎么同步数据的呢?
而消息在master和slave之间的同步是根据raft协议来进行的:
- 在broker收到消息后,会被标记为uncommitted状态
- 然后会把消息发送给所有的slave
- slave在收到消息之后返回ack响应给master
- master在收到超过半数的ack之后,把消息标记为committed
- 发送committed消息给所有slave,slave也修改状态为committed
6.10 你知道RocketMQ为什么速度快吗?
是因为使用了顺序存储、Page Cache和异步刷盘。
- 我们在写入commitlog的时候是顺序写入的,这样比随机写入的性能就会提高很多
- 写入commitlog的时候并不是直接写入磁盘,而是先写入操作系统的PageCache
- 最后由操作系统异步将缓存中的数据刷到磁盘
6.11 什么是事务消息、半事务消息?怎么实现的?
事务消息就是MQ提供的类似XA的分布式事务能力,通过事务消息可以达到分布式事务的最终一致性。
半事务消息就是MQ收到了生产者的消息,但是没有收到二次确认,不能投递的消息。
实现原理如下:
- 生产者先发送一条半事务消息到MQ
- MQ收到消息后返回ack确认
- 生产者开始执行本地事务
- 如果事务执行成功发送commit到MQ,失败发送rollback
- 如果MQ长时间未收到生产者的二次确认commit或者rollback,MQ对生产者发起消息回查
- 生产者查询事务执行最终状态
- 根据查询事务状态再次提交二次确认
最终,如果MQ收到二次确认commit,就可以把消息投递给消费者,反之如果是rollback,消息会保存下来并且在3天后被删除。
7 RocketMq经典面试题
01.为什么要用RocketMq?
02.RocketMq的部署架构了解吗?
03.它有哪几种部署类型?分别有什么特点?
04.你自己部署过RocketMq吗?简单说一下你当时部署的过程
05.rocketmq如何保证高可用性?
06.rocketmq的工作流程是怎样的?
07.RocketMq使用哪种方式消费消息,pull还是push?
08.RocketMq如何负载均衡?
09.RocketMq的存储机制了解吗?
10.RocketMq的存储结构是怎样的?
11.RocketMq如何进行消息的去重?
12.RocketMq性能比较高的原因?
7.1 为什么要用RocketMq?
总得来说,RocketMq具有以下几个优势:
- 吞吐量高:单机吞吐量可达十万级
- 可用性高:分布式架构
- 消息可靠性高:经过参数优化配置,消息可以做到0丢失
- 功能支持完善:MQ功能较为完善,还是分布式的,扩展性好
- 支持10亿级别的消息堆积:不会因为堆积导致性能下降
- 源码是java:方便我们查看源码了解它的每个环节的实现逻辑,并针对不同的业务场景进行扩展
- 可靠性高:天生为金融互联网领域而生,对于要求很高的场景,尤其是电商里面的订单扣款,以及业务削峰,在大量交易涌入时,后端可能无法及时处理的情况
- 稳定性高:RoketMQ在上可能更值得信赖,这些业务场景在阿里双11已经经历了多次考验
7.2 RocketMQ的部署架构了解吗?
这个是rocketMq的集群架构图,里面包含了四个主要部分:NameServer集群,Producer集群,Cosumer集群以及Broker集群
- NameServer 担任路由消息的提供者。生产者或消费者能够通过NameServer查找各Topic相应的Broker IP列表分别进行发送消息和消费消息。nameServer由多个无状态的节点构成,节点之间无任何信息同步broker会定期向NameServer以发送心跳包的方式,轮询向所有NameServer注册以下元数据信息:1)broker的基本信息(ip port等)2)主题topic的地址信息3)broker集群信息4)存活的broker信息5)filter 过滤器也就是说,每个NameServer注册的信息都是一样的,而且是当前系统中的所有broker的元数据信息
- Producer负责生产消息,一般由业务系统负责生产消息。一个消息生产者会把业务应用系统里产生的消息发送到broker服务器。RocketMQ提供多种发送方式,同步发送、异步发送、顺序发送、单向发送。同步和异步方式均需要Broker返回确认信息,单向发送不需要
- Broker,消息中转角色,负责存储消息、转发消息。在RocketMQ系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备
- Consumer负责消费消息,一般是后台系统负责异步消费。一个消息消费者会从Broker服务器拉取消息、并将其提供给应用程序。从用户应用的角度而言提供了两种消费形式:拉取式消费、推动式消费
7.3 它有哪几种部署类型?分别有什么特点?
RocketMQ有4种部署类型
1)单Master
单机模式, 即只有一个Broker, 如果Broker宕机了, 会导致RocketMQ服务不可用, 不推荐使用
2)多Master模式
组成一个集群, 集群每个节点都是Master节点, 配置简单, 性能也是最高, 某节点宕机重启不会影响RocketMQ服务 缺点:如果某个节点宕机了, 会导致该节点存在未被消费的消息在节点恢复之前不能被消费
3)多Master多Slave模式,异步复制
每个Master配置一个Slave, 多对Master-Slave, Master与Slave消息采用异步复制方式, 主从消息一致只会有毫秒级的延迟 优点是弥补了多Master模式(无slave)下节点宕机后在恢复前不可订阅的问题。在Master宕机后, 消费者还可以从Slave节点进行消费。采用异步模式复制,提升了一定的吞吐量。总结一句就是,采用
多Master多Slave模式,异步复制模式进行部署,系统将会有较低的延迟和较高的吞吐量
缺点就是如果Master宕机, 磁盘损坏的情况下, 如果没有及时将消息复制到Slave, 会导致有少量消息丢失
4)多Master多Slave模式,同步双写
与多Master多Slave模式,异步复制方式基本一致,唯一不同的是消息复制采用同步方式,只有master和slave都写成功以后,才会向客户端返回成功 优点:数据与服务都无单点,Master宕机情况下,消息无延迟,服务可用性与数据可用性都非常高 缺点就是会降低消息写入的效率,并影响系统的吞吐量 实际部署中,一般会根据业务场景的所需要的 性能 和 消息可靠性 等方面来选择后两种
7.4 你自己部署过RocketMq吗?简单说一下你当时部署的过程
由于我们项目中主要使用rocketMQ做链路跟踪功能,因此需要比较高的性能,并且偶尔丢失几条消息也关系不大,所以我们就选择多Master多Slave模式,异步复制方式进行部署 部署过程简单说一下: 我部署的是双master和双slave模式集群,并部署了两个nameserver节点
1)服务器分配 分配是两台服务器,A和B,其中A服务器部署nameserv1,master1,slave2;B服务器部署nameserv2,master2和slave1节点
2)broker的配置 分别配置rocketmq安装目录下四个配置文件:
master1:/conf/2m-2s-async/broker-a.propertiesslave2:/conf/2m-2s-async/broker-b-s.propertiesmaster2:/conf/2m-2s-async/broker-b.propertiesslave1:/conf/2m-2s-async/broker-a-s.properties
总的思路是:
a.master节点的brokerId为0,slave节点的brokerId为1(大于0即可);
b.同一组broker的broker-Name相同,如master1和slave1都为broker-a;
c.每个borker节点配置相同的NameServer;
d.复制方式配置:master节点配置为ASYNC-MASTER,slave节点配置为SLAVE即可;
e.刷盘方式分为同步刷盘和异步刷盘,为了保证性能而不去考虑少量消息的丢失,因此同意配置为异步刷盘
3)启动集群
a 检查修改参数
启动前分别检查修改runbroker.sh和runserver.sh两个文件中的JVM参数,默认的JAVA_OPT参数的值比较大,若直接启动可能会失败,需要根据实际情况重新配置
b 分别启动两个namerser节点
nohup sh bin/mqnamesrv > /dev/null 2>&1 &
查看日志
tail -f ~/logs/rocketmqlogs/namesrv.log
c 分别启动4个broker节点
maste1 nohup sh bin/mqbroker -c /usr/local/rocketmq/conf/2m-2s-async/broker-a.properties & slave1 nohup sh bin/mqbroker -c /usr/local/rocketmq/conf/2m-2s-async/broker-a-s.properties & maste2 nohup sh bin/mqbroker -c /usr/local/rocketmq/conf/2m-2s-async/broker-b.properties & slave2 nohup sh bin/mqbroker -c /usr/local/rocketmq/conf/2m-2s-async/broker-b-s.properties & 查看日志: tail -f ~/logs/rocketmqlogs/broker.log
总结:集群环境部署,主要就是以上三个步骤,需要注意的是过程中broker配置文件的配置正确性,还需要注意一下启动前对jvm参数的检查
7.5 rocketmq如何保证高可用性?
1)集群化部署NameServer。Broker集群会将所有的broker基本信息、topic信息以及两者之间的映射关系,轮询存储在每个NameServer中(也就是说每个NameServer存储的信息完全一样)。因此,NameServer集群化,不会因为其中的一两台服务器挂掉,而影响整个架构的消息发送与接收;
2)集群化部署多broker。producer发送消息到broker的master,若当前的master挂掉,则会自动切换到其他的master cosumer默认会访问broker的master节点获取消息,那么master节点挂了之后,该怎么办呢?它就会自动切换到同一个broker组的slave节点进行消费 那么你肯定会想到会有这样一个问题:consumer要是直接消费slave节点,那master在宕机前没有来得及把消息同步到slave节点,那这个时候,不就会出现消费者不就取不到消息的情况了? 这样,就引出了下一个措施,来保证消息的高可用性
3)设置同步复制 前面已经提到,消息发送到broker的master节点上,master需要将消息复制到slave节点上,rocketmq提供两种复制方式:同步复制和异步复制 异步复制,就是消息发送到master节点,只要master写成功,就直接向客户端返回成功,后续再异步写入slave节点 同步复制,就是等master和slave都成功写入内存之后,才会向客户端返回成功 那么,要保证高可用性,就需要将复制方式配置成同步复制,这样即使master节点挂了,slave上也有当前master的所有备份数据,那么不仅保证消费者消费到的消息是完整的,并且当master节点恢复之后,也容易恢复消息数据 在master的配置文件中直接配置brokerRole:SYNC_MASTER即可
7.6 RocketMQ的工作流程是怎样的?
RocketMQ的工作流程如下:
- 1)首先启动NameServer。NameServer启动后监听端口,等待Broker、Producer以及Consumer连上来
- 2)启动Broker。启动之后,会跟所有的NameServer建立并保持一个长连接,定时发送心跳包。心跳包中包含当前Broker信息(ip、port等)、Topic信息以及Borker与Topic的映射关系
- 3)创建Topic。创建时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Topic
- 4)Producer发送消息。启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取当前发送的Topic所在的Broker;然后从队列列表中轮询选择一个队列,与队列所在的
- Broker建立长连接,进行消息的发送
- 5)Consumer消费消息。跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,进行消息的消费
7.7 RocketMQ使用哪种方式消费消息,pull还是push?
RocketMq提供两种方式:pull和push进行消息的消费 而RocketMq的push方式,本质上也是采用pull的方式进行实现的。也就是说这两种方式本质上都是采用consumer轮询从broker拉取消息的 push方式里,consumer把轮询过程封装了一层,并注册了MessageListener监听器。当轮询取到消息后,便唤醒MessageListener的consumeMessage()来消费,对用户而言,感觉好像消息是被推送过来的 其实想想,消息统一都发到了broker,而broker又不会主动去push消息,那么消息肯定都是需要消费者主动去拉的喽~
7.8 RocketMQ如何负载均衡?
1)producer发送消息的负载均衡:默认会轮询向Topic的所有queue发送消息,以达到消息平均落到不同的queue上;而由于queue可以落在不同的broker上,就可以发到不同broker上(当然也可以指定发送到某个特定的queue上)
2)consumer订阅消息的负载均衡:假设有5个队列,两个消费者,则第一个消费者消费3个队列,第二个则消费2个队列,以达到平均消费的效果。而需要注意的是,当consumer的数量大于队列的数量的话,根据RocketMQ的机制,多出来的队列不会去消费数据,因此建议consumer的数量小于或者等于queue的数量,避免不必要的浪费
7.9 RocketMQ的存储机制了解吗?
RocketMq采用文件系统进行消息的存储,相对于ActiveMq采用关系型数据库进行存储的方式就更直接,性能更高了 RocketMq与Kafka在写消息与发送消息上,继续沿用了Kafka的这两个方面:顺序写和零拷贝
1)顺序写 我们知道,操作系统每次从磁盘读写数据的时候,都需要找到数据在磁盘上的地址,再进行读写。而如果是机械硬盘,寻址需要的时间往往会比较长 而一般来说,如果把数据存储在内存上面,少了寻址的过程,性能会好很多;但Kafka 的数据存储在磁盘上面,依然性能很好,这是为什么呢? 这是因为,Kafka采用的是顺序写,直接追加数据到末尾。实际上,磁盘顺序写的性能极高,在磁盘个数一定,转数一定的情况下,基本和内存速度一致 因此,磁盘的顺序写这一机制,极大地保证了Kafka本身的性能
2)零拷贝 比如:读取文件,再用socket发送出去这一过程
buffer = File.read
Socket.send(buffer)
传统方式实现:先读取、再发送,实际会经过以下四次复制
1、将磁盘文件,读取到操作系统内核缓冲区Read Buffer
2、将内核缓冲区的数据,复制到应用程序缓冲区Application Buffer
3、将应用程序缓冲区Application Buffer中的数据,复制到socket网络发送缓冲区
4、将Socket buffer的数据,复制到网卡,由网卡进行网络传输 传统方式,读取磁盘文件并进行网络发送,经过的四次数据copy是非常繁琐的。重新思考传统IO方式,会注意到在读取磁盘文件后,不需要做其他处理,直接用网络发送出去的这种场景下,第二次和第三次数据的复制过程,不仅没有任何帮助,反而带来了巨大的开销。那么这里使用了零拷贝,也就是说,直接由内核缓冲区Read Buffer将数据复制到网卡,省去第二步和第三步的复制。
那么采用零拷贝的方式发送消息,必定会大大减少读取的开销,使得RocketMq读取消息的性能有一个质的提。
此外,还需要再提一点,零拷贝技术采用了MappedByteBuffer内存映射技术,采用这种技术有一些限制,其中有一条就是传输的文件不能超过2G,这也就是为什么RocketMQ的存储消息的文件CommitLog的大小规定为1G的原因 小结:RocketMQ采用文件系统存储消息,并采用顺序写写入消息,使用零拷贝发送消息,极大得保证了RocketMq的性能
7.10 RocketMq的存储结构是怎样的?
如图所示,消息生产者发送消息到broker,都是会按照顺序存储在CommitLog文件中,每个commitLog文件的大小为1G
CommitLog-存储所有的消息元数据,包括Topic、QueueId以及message CosumerQueue-消费逻辑队列:存储消息在CommitLog的offset IndexFile-索引文件:存储消息的key和时间戳等信息,使得RocketMq可以采用key和时间区间来查询消息 也就是说,rocketMq将消息均存储在 CommitLog 中,并分别提供了CosumerQueue和IndexFile两个索引,来快速检索消息
7.11 RocketMQ如何进行消息的去重?
我们知道,只要通过网络交换数据,就无法避免因为网络不可靠而造成的消息重复这个问题。比如说RocketMq中,当consumer消费完消息后,因为网络问题未及时发送ack到broker,broker就不会删掉当前已经消费过的消息,那么,该消息将会被重复投递给消费者去消费 虽然rocketMq保证了同一个消费组只能消费一次,但会被不同的消费组重复消费,因此这种重复消费的情况不可避免 RocketMq本身并不保证消息不重复,这样肯定会因为每次的判断,导致性能打折扣,所以它将去重操作直接放在了消费端:
1)消费端处理消息的业务逻辑保持幂等性。那么不管来多少条重复消息,可以实现处理的结果都一样
2)还可以建立一张日志表,使用消息主键作为表的主键,在处理消息前,先insert表,再做消息处理。这样可以避免消息重复消费
7.11 RocketMQ性能比较高的原因?
就是前面在文件存储机制中所提到的:RocketMq采用文件系统存储消息,采用顺序写的方式写入消息,使用零拷贝发送消息,这三者的结合极大地保证了RocketMq的性能
7.12 RocketMQ设置CONSUME_FROM_LAST_OFFSET的问题
consumer在消费时,会设置从哪里开始消费。默认是CONSUME_FROM_LAST_OFFSET设置的值如代码所示。
public enum ConsumeFromWhere { /** * 一个新的订阅组第一次启动从队列的最后位置开始消费<br> * 后续再启动接着上次消费的进度开始消费 */ CONSUME_FROM_LAST_OFFSET, @Deprecated CONSUME_FROM_LAST_OFFSET_AND_FROM_MIN_WHEN_BOOT_FIRST, @Deprecated CONSUME_FROM_MIN_OFFSET, @Deprecated CONSUME_FROM_MAX_OFFSET, /** * 一个新的订阅组第一次启动从队列的最前位置开始消费<br> * 后续再启动接着上次消费的进度开始消费 */ CONSUME_FROM_FIRST_OFFSET, /** * 一个新的订阅组第一次启动从指定时间点开始消费<br> * 后续再启动接着上次消费的进度开始消费<br> * 时间点设置参见DefaultMQPushConsumer.consumeTimestamp参数 */ CONSUME_FROM_TIMESTAMP, }
这里要注意代码注释。这个参数只对一个新的ConsumeGroup第一次启动时有效。
就是说,如果是一个GonsumerGroup重启,他只会从自己上次消费到的offset,继续消费。这个参数是没用的。 而判断是不是一个新的ConsumerGroup是在broker端判断。
要知道,消费到哪个offset最先是存在Consumer本地的,定时和broker同步自己的消费offset。broker在判断是不是一个新的consumergroup,就是查broker端有没有这个consumergroup的offset记录。
另外,对于一个新的queue,这个参数也是没用的,都是从0开始消费。
所以,让我们困惑的一个问题我已经设置了CONSUME_FROM_LAST_OFFSET,为什么还是重复消费了。可能你这不是新的consumergroup,也可能是个新的Queue。
8 从面试角度一文学完 Kafka
Kafka 是一个优秀的分布式消息中间件,许多系统中都会使用到 Kafka 来做消息通信。对分布式消息系统的了解和使用几乎成为一个后台开发人员必备的技能。今天码哥字节
就从常见的 Kafka 面试题入手,和大家聊聊 Kafka 的那些事儿。
8.1 讲一讲分布式消息中间件
问题
- 什么是分布式消息中间件?
- 消息中间件的作用是什么?
- 消息中间件的使用场景是什么?
- 消息中间件选型?
分布式消息是一种通信机制,和 RPC、HTTP、RMI 等不一样,消息中间件采用分布式中间代理的方式进行通信。如图所示,采用了消息中间件之后,上游业务系统发送消息,先存储在消息中间件,然后由消息中间件将消息分发到对应的业务模块应用(分布式生产者 - 消费者模式)。这种异步的方式,减少了服务之间的耦合程度。
定义消息中间件:
- 利用高效可靠的消息传递机制进行平台无关的数据交流
- 基于数据通信,来进行分布式系统的集成
- 通过提供消息传递和消息排队模型,可以在分布式环境下扩展进程间的通信
在系统架构中引用额外的组件,必然提高系统的架构复杂度和运维的难度,那么在系统中使用分布式消息中间件有什么优势呢?消息中间件在系统中起的作用又是什么呢?
- 解耦
- 冗余(存储)
- 扩展性
- 削峰
- 可恢复性
- 顺序保证
- 缓冲
- 异步通信
面试时,面试官经常会关心面试者对开源组件的选型能力,这既可以考验面试者知识的广度,也可以考验面试者对某类系统的知识的认识深度,而且也可以看出面试者对系统整体把握和系统架构设计的能力。开源分布式消息系统有很多,不同的消息系统的特性也不一样,选择怎样的消息系统,不仅需要对各消息系统有一定的了解,也需要对自身系统需求有清晰的认识。
下面是常见的几种分布式消息系统的对比:
答案关键字
- 什么是分布式消息中间件?通信,队列,分布式,生产消费者模式。
- 消息中间件的作用是什么?解耦、峰值处理、异步通信、缓冲。
- 消息中间件的使用场景是什么?异步通信,消息存储处理。
- 消息中间件选型?语言,协议、HA、数据可靠性、性能、事务、生态、简易、推拉模式。
8.2 Kafka实现原理
Kafka的文件存储机制
Kafka中消息是以topic进行分类的,生产者通过topic向Kafka broker发送消息,消费者通过topic读取数据。然而topic在物理层面又能以partition为分组,一个topic可以分成若干个partition。partition还可以细分为segment,一个partition物理上由多个segment组成,segment文件由两部分组成,分别为“.index”文件和“.log”文件,分别表示为segment索引文件和数据文件。这两个文件的命令规则为:partition全局的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset值。
Kafka 如何保证可靠性
如果我们要往 Kafka 对应的主题发送消息,我们需要通过 Producer 完成。前面我们讲过 Kafka 主题对应了多个分区,每个分区下面又对应了多个副本;为了让用户设置数据可靠性, Kafka 在 Producer 里面提供了消息确认机制。也就是说我们可以通过配置来决定消息发送到对应分区的几个副本才算消息发送成功。可以在定义 Producer 时通过 acks 参数指定。这个参数支持以下三种值:
- acks = 0:意味着如果生产者能够通过网络把消息发送出去,那么就认为消息已成功写入 Kafka 。在这种情况下还是有可能发生错误,比如发送的对象无能被序列化或者网卡发生故障,但如果是分区离线或整个集群长时间不可用,那就不会收到任何错误。在 acks=0 模式下的运行速度是非常快的(这就是为什么很多基准测试都是基于这个模式),你可以得到惊人的吞吐量和带宽利用率,不过如果选择了这种模式, 一定会丢失一些消息。
- acks = 1:意味若 Leader 在收到消息并把它写入到分区数据文件(不一定同步到磁盘上)时会返回确认或错误响应。在这个模式下,如果发生正常的 Leader 选举,生产者会在选举时收到一个 LeaderNotAvailableException 异常,如果生产者能恰当地处理这个错误,它会重试发送悄息,最终消息会安全到达新的 Leader 那里。不过在这个模式下仍然有可能丢失数据,比如消息已经成功写入 Leader,但在消息被复制到 follower 副本之前 Leader发生崩溃。
- acks = all(这个和 request.required.acks = -1 含义一样):意味着 Leader 在返回确认或错误响应之前,会等待所有同步副本都收到悄息。如果和min.insync.replicas 参数结合起来,就可以决定在返回确认前至少有多少个副本能够收到悄息,生产者会一直重试直到消息被成功提交。不过这也是最慢的做法,因为生产者在继续发送其他消息之前需要等待所有副本都收到当前的消息。
Kafka消息是采用Pull模式,还是Push模式
Kafka最初考虑的问题是,customer应该从brokes拉取消息还是brokers将消息推送到consumer,也就是pull还push。在这方面,Kafka遵循了一种大部分消息系统共同的传统的设计:producer将消息推送到broker,consumer从broker拉取消息。push模式下,当broker推送的速率远大于consumer消费的速率时,consumer恐怕就要崩溃了。最终Kafka还是选取了传统的pull模式。Pull模式的另外一个好处是consumer可以自主决定是否批量的从broker拉取数据。Pull有个缺点是,如果broker没有可供消费的消息,将导致consumer不断在循环中轮询,直到新消息到t达。为了避免这点,Kafka有个参数可以让consumer阻塞知道新消息到达。
Kafka是如何实现高吞吐率的
- 顺序读写:kafka的消息是不断追加到文件中的,这个特性使kafka可以充分利用磁盘的顺序读写性能
- 零拷贝:跳过“用户缓冲区”的拷贝,建立一个磁盘空间和内存的直接映射,数据不再复制到“用户态缓冲区”
- 文件分段:kafka的队列topic被分为了多个区partition,每个partition又分为多个段segment,所以一个队列中的消息实际上是保存在N多个片段文件中
- 批量发送:Kafka允许进行批量发送消息,先将消息缓存在内存中,然后一次请求批量发送出去
- 数据压缩:Kafka还支持对消息集合进行压缩,Producer可以通过GZIP或Snappy格式对消息集合进行压缩
Kafka判断一个节点还活着的两个条件
- 节点必须可以维护和 ZooKeeper 的连接,Zookeeper 通过心跳机制检查每个节点的连接
- 如果节点是个 follower,他必须能及时的同步 leader 的写操作,延时不能太久
99 直接读这些牛人的原文
17 个方面,综合对比 Kafka、RabbitMQ、RocketMQ、ActiveMQ 四个分布式消息队列
RocketMQ基础概念剖析,并分析一下Producer的底层源码
|
作者:沙漏哟 出处:计算机的未来在于连接 本文版权归作者和博客园共有,欢迎转载,请留下原文链接 微信随缘扩列,聊创业聊产品,偶尔搞搞技术 |