目录
一、消息队列
1、定义
消息 + 队列 (Message + Queue) 简称MQ。消息队列本质就是个队列,FIFO先入先出,只不过队列中存放的内容是Message,从而叫消息队列。消息队列的主要用途就是在不同的服务、进程、线程之间进行通信。
2、消息队列的场景
1、消息队列-异步处理
我们在短信通知、终端状态推送、App推送、用户注册等的场景使用消息队列。但是对于大量的消息,我们是采用同步方法还是异步方法呢?
在上面的工作流程中,我们可以看到同步和异步的过程中,是在库存后面不同,异步后面是一个消息队列存储所有的消息,然后分配给不同的线程进行异步处理。而同步方法中是将这三个任务一个一个进行处理。显而易见的就是异步方法是很快的。那么我们就可以更快的返回结果,减少其中的等待,实现并发处理,提升系统总体的性能。
2、消息队列-流量控制(削峰)
我们生活中常见的有秒杀活动,这种活动,会吸引大量的用户参与进来,比如规定好一个时间点,那么在这个时间点内会有大量的访问请求。那么我们的服务器会压力剧增。对于这种情况我们可以使用消息队列隔离网关和后端服务,以达到流量控制和保护后端服务的目的。
比如上面的流程中,我们使用消息队列,设置最大的限制数量,把所有的访问请求按着先后顺序进行存储起来,然后让后端服务只提取需要提取的数据就可以了。这样就保护了后端服务不被压垮。
3、消息队列-服务解耦
在设计程序的时候,我们可以使用消息队列将程序进行解耦,让他们之间的关联程度变小。
如上两幅图所见,第一幅是没有使用,第二幅是使用了消息队列的。对于第一幅图,当我们设置好BCD系统的调用接口之后,如果在中途需要另外添加一个E系统的接口,我们需要添加大量代码,来实现。
但是对于第二幅图使用了消息队列之后,我们想要添加一个E接口的话,只需要写好E系统的程序,直接让它读取消息队列中的数据就好了。不需要添加大量代码。
4、消息队列-发布订阅
在一些游戏中,我们经常能看到,一些玩家对全服进行喊话,或者系统进行播报的操作。这些展现出来的消息就是通过消息队列,按着先进先出的规则播报出来的。
3、基本概念和原理
1、Broker(代理)
消息队列中有个叫Broker,Broker(代理)是消息队列系统中的一个核心组件,扮演着消息中间件的角色。它负责接收、存储、路由和转发消息,确保消息从生产者(Producer)传递到消费者(Consumer)。当然可以包含多个Broker,可以以集群的方式放置。 通俗来讲就是MQ的服务器。
2、生产消费者
这个消息队列的两边肯定是有生产者(Producer)生产数据,传递给消息队列,然后消费者(Consumer)进行消费其中的数据。对于生产者就是生产完数据,主动将数据存放在消息队列中,但是对于消费者来说,是消息队列主动将数据传递给消费者,还是生产者主动去拉取数据呢?
我们假设是主动推送给消费者数据,根据我们上面所讲的,一个消息队列可以连接多个系统(消费者),当系统足够多的时候,我们的消息队列需要向多个系统发送数据,如果说消息队列的处理能力跟不上,那么消费者拿到数据的时间就会变长。但如果是消费者主动拉取消息队列中的数据的话,就没有这个问题。
3、点对点消息队列-线程池
线程池我之前的文章也讲过并且还有详细的代码讲解。生产者生产的消息固定传送给一个消息队列,然后消费者从这个队列中拉取数据,一条消息只能被消费一次,也就是一条消息只能被一个消费者拿到。
4、发布订阅消息-ToPic
发布订阅消息模型中,支持向一个特定的主题Topic发布消息,0个或多个订阅者接收来自这个消息主题的消息。 在这种模型下,发布者和订阅者彼此不知道对方。当发布者向这个主题发布消息,然后所有的订阅者会接收这个消息。
5、消息的ACK确认机制
为了保证消息的不丢失,消息队列中提供了消息的ACknowledge机制,即ACK机制。当消费者确认这个消息已经消费掉了,那么会向消息队列发送一个ACK,消息队列收到后会将这个消息进行删除。但是当系统宕机,消息队列并未收到ACK的话,消息队列会认为这个消息并未被消费掉,便会将这个消息继续发送给其他的消费者重新处理。这样ACK的实时性会牺牲一定的吞吐量。
4、消息队列产品
市面上最常用的消息队列就是RabbitMQ 、RocketMQ 、KafKa 、ZeroMQ。
RabbitMQ:吞吐量达万级,比RocketMQ和KafKa低一个数量级。
RocketMQ:吞吐量十万级,支持高吞吐。
KafKa:吞吐量十万级,高吞吐,一般配合大数据类的系统来进行实时的数据计算、日志采集等场景。
ZeroMQ:吞吐量达100万级别,设计用于股票实时交易系统。
二、KafKa
1、介绍
解耦:允许我们独立的扩展或修改队列两边的处理过程。
可恢复性:即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。
缓冲:有助于解决生产消息和消费消息的处理速度不一致的情况。
灵活性和峰值处理能力:不会因为突发的超负荷的请求而完全崩溃,消息队列能够使关键组件顶住突发的访问压力。
异步通信:消息队列允许用户把消息放入队列但不立即处理它。
2、架构
Producer:消息生产者,向 Kafka Broker 发消息的客户端。
Consumer:消息消费者,从 Kafka Broker 取消息的客户端。
Consumer Group:消费者组(CG),消费者组内每个消费者负责消费不同分区的数据,提高消费能力。一个分区只能由组内一个消费者消费,消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
Broker:一台 Kafka 机器就是一个 Broker。一个集群(kafka C1uster)由多个 Broker 组成。一 个 Broker 可以容纳多个 topic。
topic:可以理解为一个队列,topic 将消息分类,生产者和消费者面向的是同一个 topic。
Partition:为了实现扩展性,提高并发能力,一个非常大的 topic 可以分布到多个 Broker (即服务器)上,一个 topic 可以分为多个 Partition,同一个topic在不同的分区的数据是不重复的,每个 Partition 是一个有序的队列,其表现形式就是一个一个的文件夹。
Replication:每一个分区都有多个副本,副本的作用是做备胎。当主分区(Leader)故障的时候 会选择一个备胎(Follower)上位,成为Leader。在kafka中默认副本的最大数量是10个,且副本的数量不能大于Broker的数量,follower和leader绝对是在不同的机器,同一机器对同一个分区也 只可能存放一个副本(包括自己)。
Message:消息,每一条发送的消息主体。
Leader:每个分区多个副本的"主"副本,生产者发送数据的对象,以及消费者消费数据的对象,都是 Leader。
Follower:每个分区多个副本的"从"副本,实时从 Leader 中同步数据,保持和 Leader 数据的同 步。Leader 发生故障时,某个 Follower 还会成为新的 Leader。
Offset:消费者消费的位置信息,监控数据消费到什么位置,当消费者挂掉再重新恢复的时候,可以从消费位置继续消费。同一主题,不同的分区,他们的offset 是独立的。
ZooKeeper:Kafka 集群能够正常工作,需要依赖于 ZooKeeper,ZooKeeper 帮助 Kafka 存储和管理集群信息。
我们看Topic(主题)A,我们发现它有两个leader,并且分布在不同的Broker中,并且他们的副本也要分布在不同的Broker中,这样做就是为了高可用性。我们看图可以发现,生产者生产的数据只进入到了leader中,对于他们的副本来说,就相当于数据库的主从复制一样,会进行同步数据。当leader挂掉后,会选举新的leader,但是不会在同一个broker中(高可用性)。并且对于消费者读取数据也是从leader中读取数据。
3、分区和主题的关系
一个分区只能属于一个主题
一个主题可以有多个分区
同一主题的不同分区内容不一样,每个分区有自己独立的offset
同一主题不同的分区能够被放置到不同节点的broker
分区规则设置得当可以使得同一主题的消息均匀落在不同的分区
4、生产者
5、分区分配策略
一个消费者可以订阅多个主题,可以去消费多个分区,一个分区不支持多个消费者(同一个消费组)读取。 一个消费者组中有多个 consumer,一个 topic 有多个 partition,所以必然会涉及到 partition 的分 配问题,即确定那个 partition 由哪个 consumer 来消费。当消费者组里面的消费者个数发生改变的时 候,也会触发再平衡。
Kafka 有四种分配策略,Range 、RoundRobin 、Sticky 、CooperativeSticky ,可以通过参数 partition.assignment.strategy 来配置,默认 Range + CooperativeSticky。
Range:针对每个 topic。将 topic 中的分区与消费者排序,通过分区数/消费者数决定每个消费者消费几个分区,若除不尽则前面几个消费者会多消费1个分区。注意,如果有N个 topic,容易产生 数据倾斜。
RoundRobin:针对集群中的所有 topic。把所有分区和所有的消费者都列出来,然后按照 hashcode 进行排序,最后通过轮训算法来分配分区给到各个消费者。
Sticky:粘性分区从 0.11.x 版本开始引入,首先会尽量均衡的放置分区到消费者上面,在出现同一 消费者组内消费者出现问题的时候,会尽量保持原有分配的分 区不变化。
CooperativeSticky: 在不停止消费的情况下进行增量再平衡。这与 Sticky 的逻辑相同,但具有增量支持。这种策略可能会产生不平衡的分配。
1、RangeAssignor
假设n= 分区数/消费者数量,m= 分区数%消费者数量,那么前m个消费者每个分配n+1个分区,后面的(消费者数量-m)个消费者每个分配n个分区。现在我们假设消费组内有2个消费者C0和C1都订阅了主题t0和t1, 并且每个主题都有4个分区,那 么订阅的所有分区可以标识为: t0p0、t0p1、t0p2、t0p3、t1p0、t1p1、t1p2、t1p3。那么消费者拿到的就是。
消费者C0: t0p0、t0p1、t1p0、t1p1
消费者C1: t0p2、t0p3、t1p2、t1p3
但是当2个主题都只有3个分区,那么订阅的所有分区可以标识为:t0p0、 t0p1、t0p2、 t1p0、t1p1、t1p2最终的分配结果为:
消费者C0: t0p0、t0p1、t1p0、t1p1
消费者C1: t0p2、t1p2
可以明显地看到这样的分配并不均匀,如果将类似的情形扩大,则有可能出现部分消费者过载的情况。
2、RoundRobinAssignor
它的分配策略的原理是将消费组内所有消费者及消费者订阅的所有主题的分区按照字典序排序,然后通过轮询方式逐个将分区依次分配给每个消费者。
假设消费组中有2个消费者C0 和C1都订阅了主题 t0和t1, 并且每个主题都有3个分区,那么订阅的 所有分区可以标识为:t0p0、t0p1、t0p2、t1p0、 t1p1、t1p2。最终的分配结果为:
消费者C0: t0p0、t0p2、t1p1
消费者C1: t0p1、t1p0、t1p2
假设消费组内有3个消费者(C0、 C1和C2), 它们共订阅了3个主题(t0、t1、 t2) , 这 3个主题分别有 1、2、3个分区, 即整个消费组订阅了t0p0、 t1p0、 t1p1、 t2p0、 t2p1、 t2p2这6个分区。 具体而言, 消费者 C0 订阅的是主题t0, 消费者C1 订阅的是主题t0和t1, 消费者C2 订阅的是主题t0、t1和t2, 那 么最终的分配结果为:
消费者C0: t0p0
消费者C1: t1p0
消费者C2: t1p1、t2p0、 t2p1、t2p2
可以看到RoundRobinAssignor策略也不是十分完美, 这样分配其实并不是最优解, 因为完全可以将分区t1p1 分配给消费者C1。
3、StickyAssignor
它主要有两个目的: (1)分区的分配要尽可能均匀。 (2)分区的分配尽可能与上次分配的保待相同。 当两者发生冲突时, 第一个目标优先于第二个目标。
假设消费组内有3个消费者(C0、C1和C2),它们都订阅了4个主题(t0、t1、t2、t3),并且每个主题有2个 分区。 也就是说,整个消费组订阅了t0p0、 t0p1、 t1p0、 t1p1、 t2p0、 t2p1、 t3p0、 t3p1这8个分 区。 最终的分配结果如下:
消费者C0: t0p0、t1p1、t3p0
消费者C1: t0p1、t2p0、t3p1
消费者C2: t1p0、t2p1
这样初看上去似乎与采用RoundRobinAssignor分配策略所分配的结果相同, 但事实是否真的如此呢? 再假设此时消费者 C1脱离了消费组, 那么消费组就会执行再均衡操作,进而消费分区会重新分配。 如果采用RoundRobinAssignor分配策略, 那么此时的分配结果如下:
消费者C0: t0p0、t1p0、t2p0、t3p0
消费者C2: t0p1、t1p1、t2p1、t3p1
如分配结果所示,RoundRobinAssignor分配策略会按照消费者C0 和C2进行重新轮询分配。 如果此时使用的是StickyAssignor分配策略,那么分配结果为:
消费者C0: t0p0、t1p1、t3p0、t2p0
消费者C2: t1p0、t2p1、t0p1 、t3p1
可以看到分配结果中保留了上一次分配中对消费者 C0 和C2的所有分配结果,并将原来消费者C1的 " 负 担 " 分配给了剩余的两个消费者 C0 和C2, 最终 C0 和C2的分配还保持了均衡。 如果发生分区重分配,那么对于同一个分区而言,有可能之前的消费者和新指派的消费者不是同一个, 之前消费者进行到一半的处理还要在新指派的消费者中再次复现一遍,这显然很浪费系统资源。 StickyAssignor 分配策略如同其名称中的"st1cky" 一样,让分配策略具备一定 的 " 黏性 " ,尽可能地让 前后两次分配相同,进而减少系统资源的损耗及其他异常情况的发生。
KafKa就讲解到这里!感谢大家的观看。0voice · GitHub