一、前言

在之前的两篇文章里,咱们剖析了kafka的工作流程、存储机制、分区策略、数据可靠性、故障解决。从而弄清楚了kafka的整体架构以及生产者生产的数据是怎么存储,保障可靠性以及遇到故障时进行解决的。

深入分析Kafka架构(一):工作流程、存储机制、分区策略

深入分析Kafka架构(二):数据可靠性、故障解决

那么接下来,咱们将剖析kafka架构里的消费者是如何工作的,本文将重点剖析kafka消费者的生产形式,三种分区调配策略(Range调配策略、RoundRobin调配策略、Sticky调配策略) 以及offset的保护。

二、消费者生产形式

先说论断:消费者采纳pull(拉)模式从broker中读取数据。

为什么不采纳push(推,_填鸭式教学_)的模式给消费者数据呢?首先回忆下咱们上学学习不就是各种填鸭式教学吗?不论你三七二十一,就是依照教学进度给你灌输知识,能不能承受是你的事,并美其名曰:优胜略汰!

这种push形式在kafka架构里显然是不合理的,比方一个broker有多个消费者,它们的生产速率不同,一昧的push只会给消费者带来拒绝服务以及网络拥塞等危险。而kafka显然不可能去放弃速率低的消费者,因而kafka采纳了pull的模式,能够依据消费者的生产能力以适当的速率生产broker里的音讯。

当然让消费者去pull数据天然也是有毛病的。同样联想上学的场景,如果把学习主动权全副交给学生,那有些学生想学的货色老师那里没有怎么办?那他不就陷入了一辈子就在那一直求索,然而别的也啥都学的这个死循环的状态了。kafka也是这样,采纳pull模式后,如果kafka没有数据,消费者可能会陷入循环中,始终返回空数据。为了解决这个问题,Kafka消费者在生产数据时会传入一个时长参数timeout,如果以后没有数据可供生产,消费者会期待一段时间之后再返回,这段时长即为timeout。

三、分区调配策略

咱们在第一篇文章里剖析了kafka存储数据的分区策略,这里对于消费者来说,一个consumer group中有多个consumer,一个 topic有多个partition,所以必定会波及到partition的调配问题,即确定每个partition由哪个consumer来生产,这就是分区调配策略(Partition Assignment Strategy)。

3.1、调配分区的前提条件

首先kafka设定了默认的生产逻辑:一个分区只能被同一个生产组(ConsumerGroup)内的一个消费者生产。

在这个生产逻辑设定下,假如目前某生产组内只有一个消费者C0,订阅了一个topic,这个topic蕴含6个分区,也就是说这个消费者C0订阅了6个分区,这时候可能会产生下列三种状况:

  1. 如果这时候消费者组内新增了一个消费者C1,这个时候就须要把之前调配给C0的6个分区拿进去3个调配给C1;
  2. 如果这时候这个topic多了一些分区,就要依照某种策略,把多进去的分区调配给C0和C1;
  3. 如果这时候C1消费者挂掉了或者退出了,不在消费者组里了,那所有的分区须要再次调配给C0。

总结一下,这三种状况其实就是kafka进行分区调配的前提条件:

  1. 同一个 Consumer Group 内新增消费者;
  2. 订阅的主题新增分区;
  3. 消费者来到以后所属的Consumer Group,包含shuts down 或 crashes。

只有满足了这三个条件的任意一个,才会进行分区调配 。分区的所有权从一个消费者移到另一个消费者称为从新均衡(rebalance),如何rebalance就波及到本节提到的分区调配策略。kafka提供了消费者客户端参数partition.assignment.strategy用来设置消费者与订阅主题之间的分区调配策略。默认状况下,此参数的值为:org.apache.kafka.clients.consumer.RangeAssignor,即采纳range调配策略。除此之外,Kafka中还提供了roundrobin调配策略和sticky分区调配策略。消费者客户端参数partition.asssignment.strategy能够配置多个调配策略,把它们以逗号分隔就能够了。

3.2、Range调配策略

Range调配策略是面向每个主题的,首先会对同一个主题外面的分区依照序号进行排序,并把消费者线程依照字母程序进行排序。而后用分区数除以消费者线程数量来判断每个消费者线程生产几个分区。如果除不尽,那么后面几个消费者线程将会多生产一个分区。

咱们假如有个名为T1的主题,蕴含了7个分区,它有两个消费者(C0和C1),其中C0的num.streams(消费者线程) = 1,C1的num.streams = 2。排序后的分区是:0,1,2,3,4,5,6;消费者线程排序后是:C0-0,C1-0,C1-1;一共有7个分区,3个消费者线程,进行计算7/3=2…1,商为2余数为1,则每个消费者线程生产2个分区,并且后面1个消费者线程多生产一个分区,后果会是这样的:

消费者线程

对应生产的分区序号

C0-0

0,1,2

C1-0

3,4

C1-1

5,6

这样看如同还没什么问题,然而个别在咱们理论生产环境下,会有多个主题,咱们假如有3个主题(T1,T2,T3),都有7个分区,那么依照咱们下面这种Range调配策略调配后的生产后果如下:

消费者线程

对应生产的分区序号

C0-0

T1(0,1,2),T2(0,1,2),T3(0,1,2)

C1-0

T1(3,4),T2(3,4),T3(3,4)

C1-1

T1(5,6),T2(5,6),T3(5,6)

咱们能够发现,在这种状况下,C0-0生产线程要多生产3个分区,这显然是不合理的,其实这就是Range分区调配策略的毛病。

3.3、RoundRobin调配策略

RoundRobin策略的原理是将生产组内所有消费者以及消费者所订阅的所有topic的partition依照字典序排序,而后通过轮询算法一一将分区以此调配给每个消费者。

应用RoundRobin调配策略时会呈现两种状况:

  1. 如果同一生产组内,所有的消费者订阅的音讯都是雷同的,那么 RoundRobin 策略的分区调配会是平均的。
  2. 如果同一消费者组内,所订阅的音讯是不雷同的,那么在执行分区调配的时候,就不是齐全的轮询调配,有可能会导致分区调配的不平均。如果某个消费者没有订阅生产组内的某个 topic,那么在调配分区的时候,此消费者将不会调配到这个 topic 的任何分区。

咱们别离举例说明:

第一种:比方咱们有3个消费者(C0,C1,C2),都订阅了2个主题(T0 和 T1)并且每个主题都有 3 个分区(p0、p1、p2),那么所订阅的所有分区能够标识为T0p0、T0p1、T0p2、T1p0、T1p1、T1p2。此时应用RoundRobin调配策略后,失去的分区调配后果如下:

消费者线程

对应生产的分区序号

C0

T0p0、T1p0

C1

T0p1、T1p1

C2

T0p2、T1p2

能够看到,这时候的分区调配策略是比拟均匀的。

第二种:比方咱们仍然有3个消费者(C0,C1,C2),他们合在一起订阅了 3 个主题:T0、T1 和 T2(C0订阅的是主题T0,消费者C1订阅的是主题T0和T1,消费者C2订阅的是主题T0、T1和T2),这 3 个主题别离有 1、2、3 个分区(即:T0有1个分区(p0),T1有2个分区(p0、p1),T2有3个分区(p0、p1、p2)),即整个消费者所订阅的所有分区能够标识为 T0p0、T1p0、T1p1、T2p0、T2p1、T2p2。此时如果应用RoundRobin调配策略,失去的分区调配后果如下:

消费者线程

对应生产的分区序号

C0

T0p0

C1

T1p0

C2

T1p1、T2p0、T2p1、T2p2

这时候显然调配是不平均的,因而在应用RoundRobin调配策略时,为了保障得平均的分区调配后果,须要满足两个条件:

  1. 同一个消费者组里的每个消费者订阅的主题必须雷同;
  2. 同一个消费者组外面的所有消费者的num.streams必须相等。

如果无奈满足,那最好不要应用RoundRobin调配策略。

3.4、Sticky调配策略

最初介绍一下Sticky调配策略,这种调配策略是在kafka的0.11.X版本才开始引入的,是目前最简单也是最优良的调配策略。

Sticky调配策略的原理比较复杂,它的设计次要实现了两个目标:

  1. 分区的调配要尽可能的平均;
  2. 分区的调配尽可能的与上次调配的放弃雷同。

如果这两个目标产生了抵触,优先实现第一个目标。

咱们举例进行剖析:比方咱们有3个消费者(C0,C1,C2),都订阅了2个主题(T0 和 T1)并且每个主题都有 3 个分区(p0、p1、p2),那么所订阅的所有分区能够标识为T0p0、T0p1、T0p2、T1p0、T1p1、T1p2。此时应用Sticky调配策略后,失去的分区调配后果如下:

消费者线程

对应生产的分区序号

C0

T0p0、T1p0

C1

T0p1、T1p1

C2

T0p2、T1p2

哈哈,这里可能会惊呼,怎么和后面RoundRobin调配策略一样,其实底层实现并不一样。这里假如C2故障退出了消费者组,而后须要对分区进行再均衡操作,如果应用的是RoundRobin调配策略,它会依照消费者C0和C1进行从新轮询调配,再均衡后的后果如下:

消费者线程

对应生产的分区序号

C0

T0p0、T0p2、T1p1

C1

T0p1、T1p0、T1p2

然而如果应用的是Sticky调配策略,再均衡后的后果会是这样:

消费者线程

对应生产的分区序号

C0

T0p0、T1p0、T0p2

C1

T0p1、T1p1、T1p2

看出区别了吗?Stiky调配策略保留了再均衡之前的生产调配后果,并将原来消费者C2的调配后果调配给了残余的两个消费者C0和C1,最终C0和C1的调配还放弃了平衡。这时候再领会一下sticky(翻译为:粘粘的)这个词汇的意思,是不是恍然大悟了。

为什么要这么解决呢?

这是因为产生分区重调配后,对于同一个分区而言有可能之前的消费者和新指派的消费者不是同一个,对于之前消费者进行到一半的解决还要在新指派的消费者中再次解决一遍,这时就会节约系统资源。而应用Sticky策略就能够让调配策略具备肯定的“粘性”,尽可能地让前后两次调配雷同,进而能够缩小系统资源的损耗以及其它异常情况的产生。

接下来,再来看一下上一节RoundRobin存在缺点的中央,这种状况下sticky是怎么调配的?

比方咱们仍然有3个消费者(C0,C1,C2),他们合在一起订阅了 3 个主题:T0、T1 和 T2(C0订阅的是主题T0,消费者C1订阅的是主题T0和T1,消费者C2订阅的是主题T0、T1和T2),这 3 个主题别离有 1、2、3 个分区(即:T0有1个分区(p0),T1有2个分区(p0、p1),T2有3个分区(p0、p1、p2)),即整个消费者所订阅的所有分区能够标识为 T0p0、T1p0、T1p1、T2p0、T2p1、T2p2。此时如果应用sticky调配策略,失去的分区调配后果如下:

消费者线程

对应生产的分区序号

C0

T0p0

C1

T1p0、T1p1

C2

T2p0、T2p1、T2p2

因为C0消费者没有订阅T1和T2主题,因而如上这样的调配策略曾经是这个问题的最优解了!

这时候,再补充一个例子,退出C0挂了,产生再均衡后的调配后果,RoundRobin和Sticky又有什么区别呢?

RoundRobin再均衡后的分配情况:

消费者线程

对应生产的分区序号

C1

T0p0、T1p1

C2

T1p0、T2p0、T2p1、T2p2

而如果应用Sticky策略,再均衡后分分配情况:

消费者线程

对应生产的分区序号

C1

T1p0、T1p1、T0p0

C2

T2p0、T2p1、T2p2

这里咱们惊奇的发现sticky只是把之前C0耗费的T0p0调配给了C1,咱们联合资源耗费来看,这相比RoundRobin能节俭更多的资源。

因而,强烈建议应用sticky分区调配策略。

四、offset保护

在现实情况下,消费者在生产数据时可能会呈现各种会导致宕机的故障问题,这个时候,如果消费者后续复原了,它就须要从产生故障前的地位开始持续生产,而不是从头开始生产。所以消费者须要实时的记录本人生产到了哪个offset,便于后续产生故障复原后持续生产。Kafka 0.9版本之前,consumer默认将offset保留在Zookeeper中,从0.9版本开始,consumer默认将offset保留在Kafka一个内置的topic中,该topic为 __consumer_offsets

offset的保护很简略,之所以独自列出来,是因为offset保护针对不同的kafka版本进行的解决是不同的,这点须要留神。