共计 5158 个字符,预计需要花费 13 分钟才能阅读完成。
一、前言
在之前的两篇文章里,咱们剖析了 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 个分区,这时候可能会产生下列三种状况:
- 如果这时候消费者组内 新增 了一个 消费者C1,这个时候就须要把之前调配给 C0 的 6 个分区拿进去 3 个调配给 C1;
- 如果这时候这个 topic多了一些分区,就要依照某种策略,把多进去的分区调配给 C0 和 C1;
- 如果这时候 C1消费者挂掉了或者退出 了,不在消费者组里了,那所有的分区须要再次调配给 C0。
总结一下,这三种状况其实就是 kafka 进行分区调配的前提条件:
- 同一个 Consumer Group 内新增消费者;
- 订阅的主题新增分区;
- 消费者来到以后所属的 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 调配策略时会呈现两种状况:
- 如果同一生产组内,所有的消费者订阅的音讯都是雷同的,那么 RoundRobin 策略的分区调配会是平均的。
- 如果同一消费者组内,所订阅的音讯是不雷同的,那么在执行分区调配的时候,就不是齐全的轮询调配,有可能会导致分区调配的不平均。如果某个消费者没有订阅生产组内的某个 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 调配策略时,为了保障得平均的分区调配后果,须要满足两个条件:
- 同一个消费者组里的每个消费者订阅的主题必须雷同;
- 同一个消费者组外面的所有消费者的 num.streams 必须相等。
如果无奈满足,那最好不要应用 RoundRobin 调配策略。
3.4、Sticky 调配策略
最初介绍一下Sticky 调配策略,这种调配策略是在 kafka 的 0.11.X 版本才开始引入的,是目前最简单也是最优良的调配策略。
Sticky 调配策略的原理比较复杂,它的设计次要实现了两个目标:
- 分区的调配要尽可能的平均;
- 分区的调配尽可能的与上次调配的放弃雷同。
如果这两个目标产生了抵触,优先实现第一个目标。
咱们举例进行剖析:比方咱们有 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 版本进行的解决是不同的,这点须要留神。