中间件面试题

MQ

1、为什么使用消息队列(MQ)?

面试官问你这个问题,期望的⼀个回答是说,你们公司有个什么业务场景,这个业务场景有个什么技术挑战,如果不用 MQ 可能会很麻烦,但是你现在用了 MQ 之后带给了你很多的好处。消息队列的常见使用场景,其实场景有很多,但是比较核心的有 3 个:解耦、异步、削峰

异步

解耦

削峰

每天 0 点到 16 点,A 系统风平浪静,每秒并发请求数量就 100 个。结果每次⼀到 16 点~23 点,每秒并发请求数量突然会暴增到 1 万条。但是系统最⼤的处理能⼒就只能是每秒钟处理 1000 个请求啊。怎么办?需要我们进⾏流量的削峰,让系统可以平缓的处理突增的请求。

2、如何选择合适的消息队列(MQ)?

RabbitMQ

RabbitMQ 于 2007 年发布,是使用Erlang编程语⾔编写的,最早是为电信行业系统之间的可靠通信设计的,也是少数几个支持 AMQP 协议的消息队列之⼀。

RabbitMQ:轻量级、迅捷,它的宣传口号,也很明确地表明了 RabbitMQ 的特点:

Messaging thatjust works,开箱即用的消息队列。也就是说,RabbitMQ 是⼀个相当轻量级的消息队列,非常容易部

署和使用。

RabbitMQ 的客户端⽀持的编程语言大概是所有消息队列中最多的。

RabbitMQ的问题

  • RabbitMQ 对消息堆积的⽀持并不好,当大量消息积压的时候,会导致 RabbitMQ 的性能急剧下降。
  • RabbitMQ 的性能是这几个消息队列中最差的,⼤概每秒钟可以处理几万到十几万条消息。如果应用对消息队列的性能要求非常⾼,那不要选择 RabbitMQ。
  • RabbitMQ 使⽤的编程语⾔ Erlang,扩展和⼆次开发成本高。

Kafka

Apache Kafka是⼀个分布式消息发布订阅系统。它最初由 LinkedIn 公司基于独特的设计实现为⼀个分布式的日志提交系统,之后成为 Apache 项目的⼀部分。

在早期的版本中,为了获得极致的性能,在设计方面做了很多的牺牲,比如不保证消息的可靠性,可能会丢失消息,也不⽀持集群,功能上也⽐较简陋,这些牺牲对于处理海量⽇志这个特定的场景都是可以接受的。

但是,随后几年 Kafka 逐步补齐了这些短板,当下的 Kafka 已经发展为⼀个非常成熟的消息队列产品,无论在数据可靠性、稳定性和功能特性等⽅⾯都可以满足绝大多数场景的需求。

Kafka与周边生态系统的兼容性是最好的没有之⼀,尤其在大数据和流计算领域,几乎所有的相关开源软件系统都会优先⽀持 Kafka。

Kafka 性能高效、可扩展良好并且可持久化。它的分区特性,可复制和可容错都是不错的特性。

Kafka 使⽤ Scala 和 Java 语⾔开发,设计上大量使用了批量和异步的思想,使得 Kafka 能做到超高的性能。Kafka 的性能,尤其是异步收发的性能,是三者中最好的,但与 RocketMQ 并没有量级上的差异,⼤约每秒钟可以处理几⼗万条消息。

在有足够的客户端并发进⾏异步批量发送,并且开启压缩的情况下,Kafka 的极限处理能力可以超过每秒 2000 万条消息。

Kafka的问题

Kafka 异步批量的设计带来的问题是,它的同步收发消息的响应时延比较高,因为当客户端发送⼀条消息的时候,Kafka 并不会立即发送出去,而是要等⼀会儿攒⼀批再发送,在它的 Broker 中,很多地方都会使⽤这种先攒⼀波再⼀起处理的设计。当你的业务场景中,每秒钟消息数量没有那么多的时候,Kafka的时延反而会比较高。所以,Kafka 不太适合在线业务场景。

topic达到上百个时,吞吐量会⼤幅下降。

RocketMQ

RocketMQ 是阿⾥巴巴在 2012 年开源的消息队列产品,⽤ Java 语⾔实现,在设计时参考了 Kafka,并做出了自己的⼀些改进,后来捐赠给 Apache 软件基⾦会,2017 正式毕业,成为 Apache 的顶级项目。RocketMQ 在阿里内部被⼴泛应⽤在订单,交易,充值,流计算,消息推送,日志流式处理,Binglog 分发等场景。经历过多次双双十一考验,它的性能、稳定性和可靠性都是值得信赖的。

RocketMQ 有着不错的性能,稳定性和可靠性,具备⼀个现代的消息队列应该有的⼏乎全部功能和特性,并且它还在持续的成长中。

RocketMQ 有非常活跃的中⽂社区,大多数问题可以找到中⽂的答案。RocketMQ 使用 Java 语言开发,源代码相对比较容易读懂,容易对 RocketMQ 进行扩展或者⼆次开发。

RocketMQ 对在线业务的响应时延做了很多的优化,⼤多数情况下可以做到毫秒级的响应,如果你的应用场景很在意响应时延,那应该选择使⽤ RocketMQ。

RocketMQ 的性能比 RabbitMQ 要高⼀个数量级,每秒钟大概能处理几⼗万条消息。

RocketMQ的问题

RocketMQ 的劣势是与周边生态系统的集成和兼容程度不够。

3、RabbitMQ如何保证消息不丢失?

  1. 确保消息到MQ
  2. 确保消息路由到正确的队列
  3. 确保消息在队列正确的存储
  4. 确保消息从队列中正确的投递⾄消费者

解决方案

  1. 确保消息到MQ:发送方的确认模式
  2. 确保消息路由到正确的队列:路由失败通知
  3. 确保消息在队列正确的存储:交换器、队列、消息都需要持久化
  4. 确保消息从队列中正确的投递⾄消费者:手动确认->交给消费者来确认

4、什么是MQ中的消息重复?

第⼀类原因

消息发送端应⽤的消息重复发送,有以下几种情况。

  • 消息发送端发送消息给消息中间件,消息中间件收到消息并成功存储,而这时消息中间件出现了问题,导致应⽤端没有收到消息发送成功的返回因而进行重试产生了重复。
  • 消息中间件因为负载⾼响应变慢,成功把消息存储到消息存储中后,返回“成功”这个结果时超时。
  • 消息中间件将消息成功写⼊消息存储,在返回结果时⽹络出现问题,导致应⽤发送端重试,而重试时网络恢复,由此导致重复。
  • 可以看到,通过消息发送端产⽣消息重复的主要原因是消息成功进⼊消息存储后,因为各种原因使得消息发送端没有收到“成功”的返回结果,并且⼜有重试机制,因⽽导致重复。

第二类原因

消息到达了消息存储,由消息中间件进⾏向外的投递时产⽣重复,有以下⼏种情况。

  • 消息被投递到消息接收者应⽤进⾏处理,处理完毕后应⽤出问题了,消息中间件不知道消息处理结果,会再次投递。
  • 消息被投递到消息接收者应⽤进⾏处理,处理完毕后⽹络出现问题了,消息中间件没有收到消息处理结果,会再次投递。
  • 消息被投递到消息接收者应⽤进⾏处理,处理时间⽐较⻓,消息中间件因为消息超时会再次投递。
  • 消息被投递到消息接收者应⽤进⾏处理,处理完毕后消息中间件出问题了,没能收到消息结果并处理,会再次投递。
  • 消息被投递到消息接收者应⽤进⾏处理,处理完毕后消息中间件收到结果但是遇到消息存储故障,没能更新投递状态,会再次投递。

可以看到,在投递过程中产⽣的消息重复接收主要是因为消息接收者成功处理完消息后,消息中间件不能及时更新投递状态造成的。

5、如何解决MQ中的重复消息?

那么有什么办法可以解决呢?主要是要求消息接收者来处理这种重复的情况,也就是要求消息接收者的消息处理是幂等操作。

什么是幂等性?

对于消息接收端的情况,幂等的含义是采⽤同样的输⼊多次调⽤处理函数,得到同样的结果。例如,⼀个SQL 操作

update stat_table set count= 10 where id =1

这个操作多次执行,id 等于 1 的记录中的 count 字段的值都为 10,这个操作就是幂等的,我们不⽤担心这个操作被重复。

再来看另外⼀个 SQL 操作

update stat_table set count= count +1 where id= 1;

这样的 SQL 操作就不是幂等的,⼀旦重复,结果就会产⽣变化。

常见办法

因此应对消息重复的办法是,使消息接收端的处理是⼀个幂等操作。这样的做法降低了消息中间件的整体复杂性,不过也给使⽤消息中间件的消息接收端应⽤带来了⼀定的限制和门槛。

1. MVCC:

多版本并发控制,乐观锁的⼀种实现,在⽣产者发送消息时进⾏数据更新时需要带上数据的版本号,消费者去更新时需要去⽐较持有数据的版本号,

版本号不⼀致的操作⽆法成功。例如博客点赞次数⾃动+1 的接⼝:

public boolean addCount(Long id, Long version);
update blogTable set count= count+1,version=version+1 where id=321 and version=123

每⼀个 version 只有⼀次执⾏成功的机会,⼀旦失败了⽣产者必须重新获取数据的最新版本号再次发起更新。

2. 去重表:

利用数据库表单的特性来实现幂等,常用的⼀个思路是在表上构建唯⼀性索引,保证某⼀类数据⼀旦执行完毕,后续同样的请求不再重复处理了(利用⼀张日志表来记录已经处理成功的消息的 ID,如果新到的消息 ID 已经在日志表中,那么就不再处理这条消息。)

以电商平台为例子,电商平台上的订单 id 就是最适合的 token。当用户下单时,会经历多个环节,比如生成订单,减库存,减优惠券等等。每⼀个环节执⾏时都先检测⼀下该订单 id 是否已经执行过这⼀步骤,对未执行的请求,执行操作并缓存结果,而对已经执行过的 id,则直接返回之前的执行结果,不做任何操作。这样可以在最大程度上避免操作的重复执⾏问题,缓存起来的执行结果也能用于事务的控制等。

6、Rabbitmq官方的六种工作模式

  1. 简单模式:一个生产者,一个消费者
  2. work模式:一个生产者,多个消费者,每个消费者获取到的消息唯一。
  3. 订阅模式:一个生产者发送的消息会被多个消费者获取。
  4. Direct路由模式:发送消息到交换机并且要指定路由key ,消费者将队列绑定到交换机时需要指定路由key
  5. topic模式:将路由键和某模式进行匹配,此时队列需要绑定在一个模式上,“#”匹配一个词或多个词,“*”只匹配一个词。

7、解决顺序的核心问题

  1. 相同业务必须要投递同一个队列
  2. 消费者消费必须要同一个消费者消费
  3. Rocket必须要和队列相同,均摊消费

8、队列的投递消息的特征

先进先出(FIIO)

9、RocketMq工作原理

关键词

NameServer :相当于eureka服务注册与发现;把生成者、消费者、topic 要注册到nameserver中

Borker :主要存储消息和转发消息;

Product : 生产者先注册到NameServer服务中,在把消息发送到 borker RocketMq采用多主多从,采用topic :主题包装broker,采用broker存储

RocketMQ以 Topic 来管理不同应用的消息。对于生产者而言,发送消息是,需要指定消息的Topic,对于消费者而言,在启动后,需要订阅相应的Topic,然后可以消费相应的消息。Topic是逻辑上的概念,在物理实现上,一个Topic由多个Queue组成,采用多个Queue的好处是可以将Broker存储分布式化,提高系统性能。[pagebreak][pagebreak]

RocketMQ中,producer将消息发送给Broker时,需要制定发送到哪一个队列中,默认情况下,producer会轮询的将消息发送到每个队列中(所有broker下的Queue合并成一个List去轮询)。

对于consumer而言,会为每个consumer分配固定的队列(如果队列总数没有发生变化),consumer从固定的队列中去拉取没有消费的消息进行处理。

Producer

Producer端(属于client)的逻辑概述:

producer端的逻辑都比较简单,将消息发送到某个Queue中即可,具体发送到那个Queue可以由用户控制(MessageQueueSelector接口),默认情况下,将轮询方式选择Queue。在producer端,会从NameServer将所有Broker的Topic及对应的Queue信息(即:TopicRoute信息)拉取到本地,然后根据<brokerName, queueId>组建成一个List。因此在MessageQueueSelector,可以看到所有的Queue信息。

RocketMQ将topic的消息以多个Queue来管理,使得其较为容易的就可以进行水平扩展,提供系统吞吐力。这样分布带来的问题,就是从全局上不能做到顺序性(很多时候也并不需要全局上的顺序性)。

RocketMQ提到支持顺序消息,实际上是指基于Queue级别的顺序。用户将某些需要满足顺序的一批消息(比如电商某个订单号的一系列后续操作、比如数据库的某个主键的insert、delete、update等操作)发送到固定的某个Queue中,则从这个Queue消费消息的consumer,针对这一批消息是顺序消费。

问题1:针对顺序消息的队列,是否可以做到不停服务下的集群动态扩展?

Consumer

consumer逻辑稍微复杂一点。初步思考,consumer端至少需要处理:

1、 消息的获取

2、 offset(消费进度)的管理与存储

3、 集群消费模式下,Queue的分配问题(rebalance)

RocketMQ对外提供了两种不同形式的Consumer:PushConsumer和PullConsumer。顾名思义,对于PullConsumer而言,用户需要主动调用相应的接口去拉取未消费的消息。对于PushConsumer而言,用户提供消息处理的CallBack,有未曾消费的消息时,会主动回调这个CallBack来处理消息。虽从用户角度而言,Consumer存在主动(pull)和被动(push),但RocketMQ本身的broker端仅仅保存所有的消息,并不负责push消息,因此PushConsumer的底层实现也是有一个长连接主动去broker上拉取未消费的消息,然后回调用户的callback逻辑。

ES

1、elasticsearch的倒排索引是什么

面试官:想了解你对基础概念的认知。

通俗解释一下就可以。

传统的我们的检索是通过文章,逐个遍历找到对应关键词的位置。

而倒排索引,是通过分词策略,形成了词和文章的映射关系表,这种词典+映射表即为倒排索引。

有了倒排索引,就能实现O(1)时间复杂度的效率检索文章了,极大的提高了检索效率。

学术的解答方式:

倒排索引,相反于一篇文章包含了哪些词,它从词出发,记载了这个词在哪些文档中出现过,由两部分组成——词典和倒排表。

加分项:倒排索引的底层实现是基于:FST(Finite State Transducer)数据结构。

lucene从4+版本后开始大量使用的数据结构是FST。FST有两个优点:

1、 空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩了存储空间;

2、 查询速度快。O(len(str))的查询时间复杂度。

2、在并发情况下,Elasticsearch 如果保证读写一致?

1、 可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖,由应用层来处理具体的冲突;

2、 另外对于写操作,一致性级别支持 quorum/one/all,默认为 quorum,即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故障,分片将会在一个不同的节点上重建。

3、 对于读操作,可以设置 replication 为 sync(默认),这使得操作在主分片和副本分片都完成后才会返回;如果设置 replication 为 async 时,也可以通过设置搜索请求参数_preference 为 primary 来查询主分片,确保文档是最新版本。

3、详细描述一下Elasticsearch索引文档的过程

面试官:想了解ES的底层原理,不再只关注业务层面了。

这里的索引文档应该理解为文档写入ES,创建索引的过程。

文档写入包含:单文档写入和批量bulk写入,这里只解释一下:单文档写入流程。

记住官方文档中的这个图。

第一步:客户写集群某节点写入数据,发送请求。(如果没有指定路由/协调节点,请求的节点扮演路由节点的角色。)

第二步:节点1接受到请求后,使用文档_id来确定文档属于分片0。请求会被转到另外的节点,假定节点3。因此分片0的主分片分配到节点3上。

第三步:节点3在主分片上执行写操作,如果成功,则将请求并行转发到节点1和节点2的副本分片上,等待结果返回。所有的副本分片都报告成功,节点3将向协调节点(节点1)报告成功,节点1向请求客户端报告写入成功。

如果面试官再问:第二步中的文档获取分片的过程?

回借助路由算法获取,路由算法就是根据路由和文档id计算目标的分片id的过程。

1shard = hash(_routing) % (num_of_primary_shards)

5、elasticsearch 是如何实现 master 选举的

面试官:想了解 ES 集群的底层原理,不再只关注业务层面了。

前置前提:

1、 只有候选主节点(master:true)的节点才能成为主节点。

2、 最小主节点数(min_master_nodes)的目的是防止脑裂。

这个我看了各种网上分析的版本和源码分析的书籍,云里雾里。核对了一下代码,核心入口为 findMaster,选择主节点成功返回对应 Master,否则返回 null。

选举流程大致描述如下:

第一步:确认候选主节点数达标,elasticsearch.yml 设置的值

discovery.zen.minimum_master_nodes;

第二步:比较:先判定是否具备 master 资格,具备候选主节点资格的优先返回;

若两节点都为候选主节点,则 id 小的值会主节点。

注意这里的 id 为 string 类型。

题外话:获取节点 id 的方法。

1GET /_cat/nodes?v&h=ip,port,heapPercent,heapMax,id,name 2ip port heapPercent heapMax id name


  • 3
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值