本文目录
1、前言
在以前的两篇文章里,咱们分析了kafka的工做流程、存储机制、分区策略、数据可靠性、故障处理。从而弄清楚了kafka的总体架构以及生产者生产的数据是怎么存储,保证可靠性以及遇到故障时进行处理的。算法
深刻分析Kafka架构(一):工做流程、存储机制、分区策略apache
那么接下来,咱们将分析kafka架构里的消费者是如何工做的,本文将重点分析kafka消费者的消费方式,三种分区分配策略(Range分配策略、RoundRobin分配策略、Sticky分配策略) 以及offset的维护。架构
2、消费者消费方式
先说结论:消费者采用pull(拉)模式从broker中读取数据。学习
为何不采用push(推,填鸭式教学)的模式给消费者数据呢?首先回想下我们上学学习不就是各类填鸭式教学吗?无论你三七二十一,就是按照教学进度给你灌输知识,能不能接受是你的事,并美其名曰:优胜略汰!spa
这种push方式在kafka架构里显然是不合理的,好比一个broker有多个消费者,它们的消费速率不一样,一昧的push只会给消费者带来拒绝服务以及网络拥塞等风险。而kafka显然不可能去放弃速率低的消费者,所以kafka采用了pull的模式,能够根据消费者的消费能力以适当的速率消费broker里的消息。.net
固然让消费者去pull数据天然也是有缺点的。一样联想上学的场景,若是把学习主动权所有交给学生,那有些学生想学的东西老师那里没有怎么办?那他不就陷入了一生就在那不断求索,然而别的也啥都学的这个死循环的状态了。kafka也是这样,采用pull模式后,若是kafka没有数据,消费者可能会陷入循环中,一直返回空数据。为了解决这个问题,Kafka消费者在消费数据时会传入一个时长参数timeout,若是当前没有数据可供消费,消费者会等待一段时间以后再返回,这段时长即为timeout。线程
3、分区分配策略
咱们在第一篇文章里分析了kafka存储数据的分区策略,这里对于消费者来讲,一个consumer group中有多个consumer,一个 topic有多个partition,因此确定会涉及到partition的分配问题,即肯定每一个partition由哪一个consumer来消费,这就是分区分配策略(Partition Assignment Strategy)。翻译
3.一、分配分区的前提条件
首先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.二、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.三、RoundRobin分配策略
RoundRobin策略的原理是将消费组内全部消费者以及消费者所订阅的全部topic的partition按照字典序排序,而后经过轮询算法逐个将分区以此分配给每一个消费者。
使用RoundRobin分配策略时会出现两种状况:
- 若是同一消费组内,全部的消费者订阅的消息都是相同的,那么 RoundRobin 策略的分区分配会是均匀的。
- 若是同一消费者组内,所订阅的消息是不相同的,那么在执行分区分配的时候,就不是彻底的轮询分配,有可能会致使分区分配的不均匀。若是某个消费者没有订阅消费组内的某个 topic,那么在分配分区的时候,此消费者将不会分配到这个 topic 的任何分区。
咱们分别举例说明:
第一种:好比咱们有3个消费者(C0,C1,C2),都订阅了2个主题(T0 和 T1)而且每一个主题都有 3 个分区(p0、p一、p2),那么所订阅的全部分区能够标识为T0p0、T0p一、T0p二、T1p0、T1p一、T1p2。此时使用RoundRobin分配策略后,获得的分区分配结果以下:
消费者线程 | 对应消费的分区序号 |
---|---|
C0 | T0p0、T1p0 |
C1 | T0p一、T1p1 |
C2 | T0p二、T1p2 |
能够看到,这时候的分区分配策略是比较平均的。
第二种:好比咱们依然有3个消费者(C0,C1,C2),他们合在一块儿订阅了 3 个主题:T0、T1 和 T2(C0订阅的是主题T0,消费者C1订阅的是主题T0和T1,消费者C2订阅的是主题T0、T1和T2),这 3 个主题分别有 一、二、3 个分区(即:T0有1个分区(p0),T1有2个分区(p0、p1),T2有3个分区(p0、p一、p2)),即整个消费者所订阅的全部分区能够标识为 T0p0、T1p0、T1p一、T2p0、T2p一、T2p2。此时若是使用RoundRobin分配策略,获得的分区分配结果以下:
消费者线程 | 对应消费的分区序号 |
---|---|
C0 | T0p0 |
C1 | T1p0 |
C2 | T1p一、T2p0、T2p一、T2p2 |
这时候显然分配是不均匀的,所以在使用RoundRobin分配策略时,为了保证得均匀的分区分配结果,须要知足两个条件:
- 同一个消费者组里的每一个消费者订阅的主题必须相同;
- 同一个消费者组里面的全部消费者的num.streams必须相等。
若是没法知足,那最好不要使用RoundRobin分配策略。
3.四、Sticky分配策略
最后介绍一下Sticky分配策略,这种分配策略是在kafka的0.11.X版本才开始引入的,是目前最复杂也是最优秀的分配策略。
Sticky分配策略的原理比较复杂,它的设计主要实现了两个目的:
- 分区的分配要尽量的均匀;
- 分区的分配尽量的与上次分配的保持相同。
若是这两个目的发生了冲突,优先实现第一个目的。
咱们举例进行分析:好比咱们有3个消费者(C0,C1,C2),都订阅了2个主题(T0 和 T1)而且每一个主题都有 3 个分区(p0、p一、p2),那么所订阅的全部分区能够标识为T0p0、T0p一、T0p二、T1p0、T1p一、T1p2。此时使用Sticky分配策略后,获得的分区分配结果以下:
消费者线程 | 对应消费的分区序号 |
---|---|
C0 | T0p0、T1p0 |
C1 | T0p一、T1p1 |
C2 | T0p二、T1p2 |
哈哈,这里可能会惊呼,怎么和前面RoundRobin分配策略同样,其实底层实现并不同。这里假设C2故障退出了消费者组,而后须要对分区进行再平衡操做,若是使用的是RoundRobin分配策略,它会按照消费者C0和C1进行从新轮询分配,再平衡后的结果以下:
消费者线程 | 对应消费的分区序号 |
---|---|
C0 | T0p0、T0p二、T1p1 |
C1 | T0p一、T1p0、T1p2 |
可是若是使用的是Sticky分配策略,再平衡后的结果会是这样:
消费者线程 | 对应消费的分区序号 |
---|---|
C0 | T0p0、T1p0、T0p2 |
C1 | T0p一、T1p一、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 个主题分别有 一、二、3 个分区(即:T0有1个分区(p0),T1有2个分区(p0、p1),T2有3个分区(p0、p一、p2)),即整个消费者所订阅的全部分区能够标识为 T0p0、T1p0、T1p一、T2p0、T2p一、T2p2。此时若是使用sticky分配策略,获得的分区分配结果以下:
消费者线程 | 对应消费的分区序号 |
---|---|
C0 | T0p0 |
C1 | T1p0、T1p1 |
C2 | T2p0、T2p一、T2p2 |
因为C0消费者没有订阅T1和T2主题,所以如上这样的分配策略已是这个问题的最优解了!
这时候,再补充一个例子,加入C0挂了,发生再平衡后的分配结果,RoundRobin和Sticky又有什么区别呢?
RoundRobin再平衡后的分配状况:
消费者线程 | 对应消费的分区序号 |
---|---|
C1 | T0p0、T1p1 |
C2 | T1p0、T2p0、T2p一、T2p2 |
而若是使用Sticky策略,再平衡后分分配状况:
消费者线程 | 对应消费的分区序号 |
---|---|
C1 | T1p0、T1p一、T0p0 |
C2 | T2p0、T2p一、T2p2 |
这里咱们惊奇的发现sticky只是把以前C0消耗的T0p0分配给了C1,咱们结合资源消耗来看,这相比RoundRobin能节省更多的资源。
所以,强烈建议使用sticky分区分配策略。
4、offset维护
在现实状况下,消费者在消费数据时可能会出现各类会致使宕机的故障问题,这个时候,若是消费者后续恢复了,它就须要从发生故障前的位置开始继续消费,而不是从头开始消费。因此消费者须要实时的记录本身消费到了哪一个offset,便于后续发生故障恢复后继续消费。Kafka 0.9版本以前,consumer默认将offset保存在Zookeeper中,从0.9版本开始,consumer默认将offset保存在Kafka一个内置的topic中,该topic为 __consumer_offsets 。
offset的维护很简单,之因此单独列出来,是由于offset维护针对不一样的kafka版本进行的处理是不一样的,这点须要注意。
5、总结
本文咱们分析了Kafka的消费者消费方式、分区分配策略、offset维护,其中分区分配策略是重点,咱们很是详细的举例对比说明了Range,RoundRobin,Sticky这三种策略的优缺点,相信读完必定会有所收获。
到这里kafka架构已经分析完毕,这三篇文章系统性的从总体工做流程,存储机制,分区策略入手,到生产者生产数据,如何保证数据可靠性,遇到故障如何处理,到最后的消费者如何去消费数据,如何把分区分配给消费者等问题都能找到解释。
因本人能力有限,若是对kafka架构的分析有误,还请您不吝赐教,谢谢。