消息队列原理学习:kafka、RabbitMQ

1、消息队列

      MQ全称为Message Queue消息队列,生产者不断的往消息队列中不断写入消息,消费者则可以订阅队列中的消息,MQ是遵循了AMQP的具体实现。

     AMQP,即Advanced Message Queuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。组件之间的解耦,消息的发送者无需知道消息使用者的存在,基于此协议的客户端与消息中间件可传递消息。

消息队列应用场景

  • 异步处理、应用解耦:
  • 流量削峰:服务器在接收到用户请求后,首先写入消息队列。这时如果消息队列中消息数量超过最大数量,则直接拒绝用户请求或返回跳转到错误页面(服务器熔断限流)
  • 秒杀业务(缓存)根据秒杀规则读取消息队列中的请求信息,进行后续处理

2、RabbitMQ

RabbitMQ是MQ产品的典型代表。RabbitMQ 最初起源于金融系统,是一个由erlang开发的基于AMQP协议的开源实现。用于在分布式系统中存储转发消息,是当前最主流的消息中间件之一。

 

基本概念

1)producer指的是消息生产者,consumer消息的消费者。

2) Queue消息队列,提供了FIFO的处理机制,具有缓存消息的能力,队列消息可以设置为持久化,临时或者自动删除,决定数据是否在服务器磁盘上保留。

3) 核心概念Exchange交换机

那么为什么我们需要 Exchange 而不是直接将消息发送至队列呢?

AMQP 协议中的核心思想就是生产者和消费者的解耦,生产者从不直接将消息发送给队列。生产者通常不知道是否一个消息会被发送到队列中,只是将消息发送到一个交换机。先由 Exchange 来接收,然后Exchange 按照特定的策略转发到 Queue将各个消息分发到相应的队列中。在实际应用中我们只需要定义好 Exchange 的路由策略,而生产者则不需要关心消息会发送到哪个 Queue或被哪些Consumer消费。和Queue一样,Exchange也可设置为持久化,临时或者自动删除。

不同类型的Exchange转发消息的策略有所区别:

  • Direct直接交换器,Exchange会将消息发送完全匹配的队列
  • 广播式交换器,不管消息的ROUTING_KEY设置为什么,Exchange都会将消息转发给所有绑定的Queue。
  • topic主题交换器,工作方式类似于组播,Exchange会将消息转发和ROUTING_KEY匹配模式相同的所有队列

4) Binding

所谓绑定就是将一个特定的 Exchange 和一个特定的 Queue 绑定起来。Exchange 和Queue的绑定可以是多对多的关系

5) virtual host

相当于物理的server,可以为不同应用提供边界隔离,使得应用安全的运行在不同的vhost实例上,相互之间不会干扰。producer和consumer连接rabbit server需要指定一个vhost。


*如何保证消息100%投递的方案

  1. 生产端可靠性投递
  2. 保障消息的成功发出
  3. 保障MQ节点的成功接收
  4. 发送端收到MQ节点(Broker) 确认应答
  5. 完善的消息补偿机制

如果想保障消息100%投递成功,只做到前三步不一定能够保障。有些极端情况,比如生产端在投递消息时可能失败了,或者说生产端投递了消息,MQ Broker也收到了,MQ Broker在返回确认应答时,由于网络闪断导致生产端没有收到应答,此时这条消息就不知道投递成功了还是失败了,所以针对这些情况需要做一些补偿机制。

互联网大厂的解决方案:

  1. 消息落库,对消息状态进行打标
  2. 消息的延迟投递,做二次确认,回调检查

具体使用哪种要根据业务场景和并发量、数据量大小来决定

方案一:消息信息落库,对消息状态进行打标的方案如下图:

step 1:进行业务数据入库:比如发送一条订单消息,首先把业务数据也就是订单信息进行入库,然后生成一条消息,把消息也进行入库,这条消息应该包含消息状态属性,并设置初始值比如为0,表示消息创建成功正在发送中,这种方式缺陷在于我们要对数据库进行持久化两次。

step 2:首先要保证第一步消息都存储成功了,没有出现任何异常情况,然后生产端再进行消息发送。如果失败了就进行快速失败机制。

step 3:MQ把消息收到的结果应答(confirm)给生产端。

step 4:生产端有一个Confirm Listener,去异步的监听Broker回送的响应,从而判断消息是否投递成功,如果成功,去数据库查询该消息,并将消息状态更新为1,表示消息投递成功。

假设step 2 已经OK了,在第三步回送响应时,网络突然出现了闪断,导致生产端的Listener收不到这条消息的confirm应答,也就是说这条消息的状态一直为0了。

step 5:此时我们需要设置一个规则,比如说消息在入库时候设置一个临界值timeout,5分钟之后如果状态还是0,那就需要把消息抽取出来。这里,使用分布式定时任务,去定时抓取DB中距离消息创建时间超过5分钟的且状态为0的消息。

step 6:把抓取出来的消息进行重新投递(Retry Send),也就是从第二步开始继续往下走。

step 7:当然有些消息可能由于一些实际的问题无法路由到Broker,比如routingKey设置不对,对应的队列被误删除了,这种消息即使重试多次也仍然无法投递成功,所以需要对重试次数做限制,比如限制3次,如果投递次数大于3次,那么就将消息状态更新为2,表示这个消息最终投递失败。

 

对于方案一可靠性投递,在高并发的场景下是否适合?

对于方案一,需要做两次数据库的持久化操作,在高并发场景下数据库将存在性能瓶颈。其实在核心链路中只需要对业务数据进行入库,消息没必要先入库,可以做一个消息的延迟投递,做二次确认,回调检查。

方案二:消息的延迟投递,做二次确认,回调检查,如下图:

Upstream Service上游服务也就是生产端,Downstream service下游服务也就是消费端,Callback service是回调服务。

step1:先将业务消息进行入库,然后生产端将消息发送出去,注意一定是等数据库操作完成:之后再去发送消息。

step 2:在发送消息之后,紧接着生产端再次发送一条延迟消息投递检查,这里需要设置一个延迟时间,比如5分钟之后进行投递。

step 3:消费端去监听指定队列,将收到的消息进行处理。

step 4:处理完成之后,发送一个confirm消息,也就是回送响应,但是这里响应不是正常的ACK,而是重新生成一条消息,投递到MQ中。

step 5:上面的Callback service是一个单独的服务,其实它扮演了方案一的存储消息的DB角色,它通过MQ去监听下游服务发送的confirm消息,如果Callback service收到confirm消息,那么就对消息做持久化存储,即将消息持久化到DB中。

step6:5分钟之后延迟消息发送到MQ了,然后Callback service还是去监听延迟消息所对应的队列,收到Check消息后去检查DB中是否存在消息,如果存在,则不需要做任何处理,如果不存在或者消费失败了,那么Callback service就需要主动发起RPC通信给上游服务,告诉它延迟投递的这条消息没有找到,需要重新发送,生产端收到信息后就会重新查询业务消息然后将消息发送出去。

 

方案二也是互联网大厂更为经典和主流的解决方案:

方案二不一定能保障百分百投递成功,但是基本上可以保障大概99.9%的消息是OK的,有些特别极端的情况只能是人工去做补偿了,或者使用定时任务去做。

方案二主要目的是为了减少数据库操作,提高并发量。 在高并发场景下,最关心的不是消息100%投递成功,而是一定要保证性能,保证能抗得住这么大的并发量。所以能减少数据库的操作就尽量减少,可以异步的进行补偿。

其实在主流程里面是没有这个Callback service的,它属于一个补偿的服务,整个核心链路就是生产端入库业务消息,发送消息到MQ,消费端监听队列,消费消息。其他的步骤都是一个补偿机制

 

KafKa高级消息队列、数据流处理平台

Kafka是最初由Linked in公司开发,是一个分布式、支持分区的(partition、多副本的、多订阅者,基于zookeeper协调的分布式日志系统(也可以当做MQ系统),用scala语言编写。2010年贡献给了Apache基金会并成为顶级开源项目。面向于数据流的生产、转换、存储、消费整体的流处理平台就,Kafka不仅仅是一个消息队列

Kafka的特性

  • 可以发布和订阅且记录数据的流,类似于消息队列
  • 数据流存储的平台,具备容错能力,高可用,备份
  • 在数据产生时就可以进行处理,实时数据流处理

Kafka通过Zookeeper管理集群配置,选举Leader,以及在Consumer Group发生变化时进行Rebalance。Producer使用push模式将消息发布到Broker,Consumer使用Pull模式从Broker订阅并消费消息。

kafka对外使用topic的概念,生产者往topic里写消息,消费者读消息。为了做到水平扩展,一个topic实际是由多个partition组成的,遇到瓶颈时,可以通过增加partition的数量来进行横向扩容,单个parition内是保证消息有序

Kafka专用术语:

  • Broker:是Kafka集群的一个节点,多个broker可以组成一个Kafka集群。
  • Topic:Kafka以topic的形式来保存不同类别的消息,Kafka集群能够同时负责多个topic的分发
  • Partition:topic物理上的分组,一个topic可以分为多个partition,每个partition都由一系列有序的、不可变的消息组成,这些消息被连续的追加到partition中。offset:partition中的每个消息都有一个连续的序列号叫做offset,用于partition唯一标识一条消息。

Kafka以分区(partition)日志的形式存储topic

每个分区都是有序的不可变的消息序列,这些消息序列以追加形式写到提交日志上去。每个分区内,每条消息都被分配了一个下标号(offset),这些有序的下标号用以在不同(partition)中唯一确定消息的位置。消息在Kafka上的存储时间是可配置的,在配置时间范围内,消息是可以随时被消费,但是从消息发布时间开始计算,一旦配置的时间过了,为了腾出更多的空间,消息将会被丢弃。

Replication:分区的副本

当集群中有Broker挂掉的情况,系统可以主动地使Replication提供服务。

系统默认设置每一个Topic的Replication系数为1,所有的读和写都从Replication Leader进行,Replication Followers只是作为备份;Replication Followers必须能够及时复制Replication Leader的数据增加容错性与可扩展性

consumer是如何知道自己要消费的消息在那个位置呢

由于每个消息被赋予了在partition中的唯一下标,所以在每个consumer上只需要维护的消息在日志中的下标位置即可。consumer可以通过控制下标来读取不同的消息。consumer的这种轻量设计方便了consumer的扩展,某个consumer的去留不会影响集群。

将日志分区的目的可以归纳如下:

1. 日志分区可以避免太大的日志无法存储的问题,单个服务器上的容量有限。

2. 这样可以拥有任意多的分区,从而不会对topic的大小有限制。

3. 日志分区存储对后期的并行消费和消息的容错有很大的帮助。

**topic的分布式存储和分布式的服务请求

  日志的分区被分布式存储到不同的server上,为了容错,每个partition可以配置一个冗余的份数。对于每一个partition,多份冗余的partition所在的server中只能有一个为leader,其他的都是follower。在读写操作中,都由partition的leader去接受读写请求,而其他的follower被动的去复制leader来保证消息的一致性。这样在以后如果partition的leader的服务如果挂了,这些follower可以被选举为leader继续提供读写服务。集群中的每个server都同时承担着leader和follower的角色,所以在处理请求的负载上也相对均衡。

producer(消息生产者)

producer将消息发布到它指定的topic中,并负责决定发布到topic的哪个分区。

consumer(消息消费者)

  传统的消息发布模式有两种:队列模式和订阅发布模式。队列模式中,consumer池中的consumer从server从读取消息,每条消息被一个consumer读取。订阅发布模式中每条消息被广播到所有的consumer。而Kafka综合了这两者,提供了一种consumer group(消费组)的概念。

  有了消费组的概念,每个consumer可以将自己标记为所属的组,这样,Kafka将会将消息传输到订阅组的一个consumer实例,注意,这里不是将消息传给订阅组的所有consumer实例。

  在这种消费组的概念下,如果每个消费组只有一个consumer实例,那和传统的订阅发布模式一样,如果所有的consumer都在一个组内,则和传统的队列模式一样。更常见的是,每个topic都有若干数量的consumer组,每个组都是一个逻辑上的“订阅者”,为了容错和更好的稳定性,每个组由若干consumer组成。这其实就是一个发布-订阅模式,只不过订阅者是个组而不是单个consumer。消息订阅组和Kafka集群的关系如下图:

  相比传统的消息系统,Kafka在消息有序性上的保障性更强。传统的消息系统在有序性和并发性上不能做到很好的互补兼容,传统的消息系统没有分区的概念,消息在队列中有序存储,但是在多个consumer消费消息时,虽然消息是顺序分发的,但是由于消息的异步传输,最后并不能保证有序性,然而,如果只让一个consumer去消费消息,又失去了并发性。但是Kafka通过分区的概念解决了这个难题,在Kafka中,每个分区只可以被分发到一个消费组中的一个consumer,这样保证了消息消费的有序性,由于一个topic有多个分区,所以并发性上也有保证。注意:在一个消费组中的consumer数量不能超过分区的数量。

Kafka消息系统的几点保障

1. producer往指定topic的一个partition写消息时,消息被提交到partition中的顺序和producer发送的顺序严格一致。

2. consumer实例看到的消息的顺序和其在partition中存储的顺序一致。

Kafka的高级特性

消息事务的原因:保证数据一致性满足,不准确的数据处理的容忍度不断降低

数据传输的事务定义

最多一次:消息不会被重复发送,最多被传输一次,但也有可能一次不传输

最少一次:消息不会被漏发送,最少被传输一次,但也有可能被重复传输

精确的一次:不会漏传输也不会重复传输,每个消息都被传输一次且仅仅被传输一次,这是大家所期望的

 

零拷贝

通过网络传输持久性日志块 使用Java Nio实现,底层使用Linux文件系统调用

文件传输到网络的公共数据路径

第一次拷贝:操作系统将数据从磁盘读入到内核空间的页缓存

第二次拷贝:应用程序将数据从内核空间读入到用户空间缓存中

第三次拷贝:应用程序将数据写回到内核空间到socket缓存中

第四次拷贝:操作系统将数据从socket缓冲区复制到网卡缓冲区,以便将数据经网络发出

零拷贝过程(指内核空间和用户空间的交互拷贝次数为零)

第一次拷贝:操作系统将数据从磁盘读入到内核空间的页缓存

将数据的位置和长度的信息的描述符增加至内核空间(socket缓存区)

第二次拷贝:操作系统将数据从内核拷贝到网卡缓冲区,以便将数据经网络发出

 


相关知识:

Zookeeper的角色

领导者(leader),负责进行投票的发起和决议,更新系统状态

学习者(learner),包括跟随者(follower)和观察者(observer)

follower用于接受客户端请求并想客户端返回结果,在选主过程中参与投票

Observer可以接受客户端连接,将写请求转发给leader,但observer不参加投票过程,

只同步leader的状态,observer的目的是为了扩展系统,提高读取速度

客户端(client),请求发起方

      Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议。Zab协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步)。当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和Server具有相同的系统状态。

     为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上了zxid。实现中zxid是一个64位的数字,它高32位是epoch用来标识 leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch,标识当前属于那个leader的统治时期。低32位用于递增计数。

状态

LOOKING:当前Server不知道leader是谁,正在搜寻

LEADING:当前Server即为选举出来的leader

FOLLOWING:leader已经选举出来,当前Server与之同步

全文检索ElasticSearch(ES)

ES是一个高度可扩展的、开源的、基于 Lucene 的全文搜索和分析引擎。它允许您快速,近实时地存储,搜索和分析大量数据,并支持多租户。通过简单的 RESTful API 来隐藏 Lucene 的复杂性,从而让全文搜索变得简单。

ELK(ElasticSearch, logstash, kibana)技术栈的版本统一配合使用,

Kibana是一个开源分析和可视化平台,旨在与ES协同工作。您使用Kibana搜索,查看和与存储在索引中的数据进行交互。您可以轻松地执行高级数据分析,并在各种图表,表格和地图中可视化您的数据。

 

ES底层原理

Lucene 是一个基于 Java 的全文信息检索工具包,目前主流的搜索系统Elasticsearch和solr都是基于lucene的索引和搜索能力进行。想要理解搜索系统的实现原理,就需要深入lucene这一层,看看lucene是如何存储需要检索的数据,以及如何完成高效的数据检索。

倒排索引

倒排索引实际上由于应用中需要根据属性值来查找记录,这种索引表中的每一项都包含一个属性值和具有该属性值的各记录的地址。

由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称之为倒排索引(inverted index),带有倒排索引的文件称之为倒排

关键词代表的是 搜索引擎 给句子分出来的词,下面的文章代表搜索引擎具体在哪篇文章中搜索出来的数据,我们还可以把这个搜索出的结果更加的细化,变成更加具体的定位 ,第二列文章中代表的含义分别是( 该关键词所在的文章 ,<该关键词在文章出出现的索引位置> ,该关键词出现的次数 )

 

这样以来搜索引擎就可以根据把句子进行分词,然后根据对分词的查询在文章中的位置将其按照某种方法进行排序操作,完成搜索引擎的排序功能。在搜索引擎中,关键词会被提取出来并且记录他在文档中出现的位置和次数,得到正向索引

文档1->关键词1:出现次数,位置列表->关键词2:出现次数,位置列表

文档2->关键词1:出现次数,位置列表->关键词2:出现次数,位置列表

当我要去查询关键词[csgo]要扫描索引库中所有文档,找出包含关键词csgo的文档,再根据打分模型打分,排出名次后给用户返回,当数据库文件数量庞大时,这样扫描全部文档的办法肯定无法满足需求。所以搜索引擎会将正向索引重构为倒排索把文件到关键词的映射改为关键词到文件的映射。索引结构变为:

关键词1->文档1->文档2

关键词2->文档1->文档2

实现时,将上面三列保存为词典文件,频率文件,位置文件,当搜索一个词时,先在词典文件找到该词,通过词典文件指针找到位置文件,而词典文件通常非常小,所以整个过程是毫秒级的。当数据非常庞大达到PB级时,普通的索引无法满足快速查询,就会极大地体现出倒排索引的优势

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章