Kafka消费者详解

Kafka采用消费组保证了“一个分区只可被消费组中的一个消费者所消费” 

消费者组再平衡实现故障容错

一旦有消费者加入或退出消费组,导致消费组成员列表发生变化,消费组中所有的消费者就要执行再平衡(rebalance) 工作。如果订阅主题的分区有变化,所有的消费者也都要再平衡。

由于消费者消费消息的最小单元是分区,因此每个分区都应该记录消费进度,而且消费进度应该面向消费组级别。

消费者除了需要保存消费进度到ZK中,它分配的分区也是从ZK读取的。ZK不仅存储了Kafka的内部元数据,而且记录了消费组的成员列表、分区的消费进度、分区的所有者。

消费者要消费哪些分区的消息由消费组来决定,因为消费组管理所有的消费者,所以它需要知道集群中所有可用的分区和所有存活的消费者,才能执行分区分配算法,而这些信息都需要保存到ZK中。每个消费者都要在ZK的消费组节点下注册对应的消费者节点,在分配到不同的分区后,才会开始各自拉取分区的消息。

 

Kafka高级消费者

  1. 自动负载均衡

高阶消费者为了简化编程,封装了一系列API,这套API会均匀地将分区分配给消费者线程,消费者消费哪个分区不由消费者决定,而是由高阶API决定,如果有消费者线程挂掉了,高阶API会检测到,进而进行重新分配。

高阶消费者API下,固定分区个数时如果消费者个数大于分区个数,那么将会有消费者空转,造成资源的浪费。

 

如图所示,同一套程序在两台机器上同时跑,均为group001,那么高级消费者将会将两台机器中的group001视为同一个组,进而将两个broker的6个分区均匀分配到group001的6个线程中。也就是说,高级消费者关注组名,即使是不同机器上的相同组名的消费者,也全部视为一个组的所有消费者。

 

自动提交offset

在高阶消费者中,Offset采用自动提交的方式。

自动提交时,假设1s提交一次offset的更新,设当前offset=10,当消费者消费了0.5s的数据,offset移动了15,由于提交间隔为1s,因此这一offset的更新并不会被提交,这时候我们写的消费者挂掉,重启后,消费者会去ZooKeeper上获取读取位置,获取到的offset仍为10,它就会重复消费,这就是一个典型的重复消费问题

高阶消费者存在一个弊端,即消费者消费到哪里由高阶消费者API进行提交,提交到ZooKeeper,消费者线程不参与offset更新的过程,这就会造成数据丢失(消费者读取完成,高级消费者API的offset已经提交,但是还没有处理完成Spark Streaming挂掉,此时offset已经更新,无法再消费之前丢失的数据),还有可能造成数据重复读取(消费者读取完成,高级消费者API的offset还没有提交,读取数据已经处理完成后Spark Streaming挂掉,此时offset还没有更新,重启后会再次消费之前处理完成的数据)。

 

Kafka低级消费者

对于低阶消费者就不再有分区到消费者之间的API中间层了,由消费者直接找到分区进行消费,即消费者通过ZooKeeper找到指定分区的Leader在哪个broker上。

首先,在ZooKeeper中能够找到Kafka所有topic的分区列表,并且可以找到指定分区的Leader在哪个broker上。

消费者消费一条消息后,可以选择提交或者不提交,offset可以缓存在Redis中,也可以自己存到ZooKeeper上。

当正在读取的分区挂掉了,此时读取会出现异常,由于Kafka存在副本机制,需要从ZooKeeper重新获取元数据,更新列表,从而继续消费分区数据。

KafkaCluster

在使用Kafka低阶消费者时,可以通过KafkaCluster类实现offset向ZooKeeper的提交和获取。

Kafka协议非常简单,只有六个核心客户端请求API:

  1. 元数据(Metadata - 描述当前可用的代理,主机和端口信息,并提供有关哪个代理主机分区的信息。
  2. 发送(Send - 发送消息给经纪人
  3. 获取(Fetch - 从代理获取消息,获取数据,获取集群元数据,获取关于主题的偏移信息的消息。
  4. 偏移量(Offsets - 获取有关给定主题分区的可用偏移量的信息。
  5. 偏移提交(Offset Commit- 为消费者组提供一组偏移量
  6. 偏移获取(Offset Fetch - 为消费者组取得一组偏移量

Kafka高可靠性存储

Kafka的高可靠性的保障来源于其健壮的副本(replication)策略。通过调节其副本相关参数,可以使得Kafka在性能和可靠性之间运转的游刃有余。Kafka从0.8.x版本开始提供partition级别的复制,replication的数量可以在$KAFKA_HOME/config/server.properties中配置(default.replication.refactor)。

1.  Kafka文件存储机制

Kafka中消息是以topic进行分类的,生产者通过topic向Kafka broker发送消息,消费者通过topic读取数据。

为了便于说明问题,假设这里只有一个Kafka集群,且这个集群只有一个Kafka broker,即只有一台物理机。在这个Kafka broker中配置($KAFKA_HOME/config/ server.properties中)log.dirs=/tmp/kafka-logs,以此来设置Kafka消息文件存储目录,与此同时创建一个topic:topic_zzh_test,partition的数量为4($KAFKA_HOME/bin/kafka-topics.sh –create –zookeeper localhost:2181 –partitions 4 –topic topic_zzh_test –replication-factor 4)。那么我们此时可以在/tmp/kafka-logs目录中可以看到生成了4个目录:

drwxr-xr-x 2 root root 4096 Apr 10 16:10 topic_zzh_test-0

drwxr-xr-x 2 root root 4096 Apr 10 16:10 topic_zzh_test-1

drwxr-xr-x 2 root root 4096 Apr 10 16:10 topic_zzh_test-2

drwxr-xr-x 2 root root 4096 Apr 10 16:10 topic_zzh_test-3

Kafka文件存储中,同一个topic下有多个不同的partition,每个partiton为一个目录,partition的名称规则为:topic名称+有序序号,第一个序号从0开始计,最大的序号为partition数量减1,partition是实际物理上的概念,而topic是逻辑上的概念

partition还可以细分为segment,这个segment又是什么?

如果就以partition为最小存储单位,我们可以想象当Kafka producer不断发送消息,必然会引起partition文件的无限扩张,这样对于消息文件的维护以及已经被消费的消息的清理带来严重的影响,所以这里segment为单位又将partition细分。每个partition(目录)相当于一个巨型文件被平均分配到多个大小相等的segment(段)数据文件中(每个segment 文件中消息数量不一定相等),这种特性也方便old segment的删除,即方便已被消费的消息的清理,提高磁盘的利用率。每个partition只需要支持顺序读写就行,segment的文件生命周期由服务端配置参数(log.segment.bytes,log.roll.{ms,hours}等若干参数)决定。

segment文件由两部分组成,分别为“.index”文件和“.log”文件,分别表示为segment索引文件和数据文件(引入索引文件的目的就是便于利用二分查找快速定位message位置)。这两个文件的命令规则为:partition全局的第一个segment0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset,数值大小为64位,20位数字字符长度,没有数字用0填充,如下:

000000000000000000000.index

00000000000000000000.log

00000000000000170410.index

00000000000000170410.log

00000000000000239430.index

00000000000000239430.log

以上面的segment文件为例,展示出segment:00000000000000170410的“.index”文件和“.log”文件的对应的关系,如下图:

如图2-12所示,.index”索引文件存储大量的元数据,“.log”数据文件存储大量的消息,索引文件中的元数据指向对应数据文件中message的物理偏移地址。其中以“.index”索引文件中的元数据[3, 348]为例,在“.log”数据文件表示第3个消息,即在全局partition中表示170410+3=170413个消息,该消息的物理偏移地址为348。

那么如何从partition中通过offset查找message呢?

以上图为例,读取offset=170418的消息,首先查找segment文件,其中00000000000000000000.index为最开始的文件,第二个文件为00000000000000170410.index(起始偏移为170410+1=170411),而第三个文件为00000000000000239430.index(起始偏移为239430+1=239431),所以这个offset=170418就落到了第二个文件之中。其他后续文件可以依次类推,以其偏移量命名并排列这些文件,然后根据二分查找法就可以快速定位到具体文件位置。其次根据00000000000000170410.index文件中的[8,1325]定位到00000000000000170410.log文件中的1325的位置进行读取

要是读取offset=170418的消息,从00000000000000170410.log文件中的1325的位置进行读取,那么怎么知道何时读完本条消息,否则就读到下一条消息的内容了?

这个就需要联系到消息的物理结构了,消息都具有固定的物理结构,包括:offset(8 Bytes)、消息体的大小(4 Bytes)、crc32(4 Bytes)、magic(1 Byte)、attributes(1 Byte)、key length(4 Bytes)、key(K Bytes)、payload(N Bytes)等等字段,可以确定一条消息的大小,即读取到哪里截止。

2.  复制原理和同步方式

Kafka中topic的每个partition有一个预写式的日志文件,虽然partition可以继续细分为若干个segment文件,但是对于上层应用来说可以将partition看成最小的存储单元(一个由多个segment文件拼接的“巨型”文件),每个partition都由一些列有序的、不可变的消息组成,这些消息被连续的追加到partition中。

 

图2-13  HW与LEO

图2-13中有两个新名词:HWLEO。这里先介绍下LEOLogEndOffset的缩写,表示每个partitionlog最后一条Message的位置。HW是HighWatermark的缩写,是指consumer能够看到的此partition的位置,这个涉及到多副本的概念(参见第3.4.3节)。

言归正传,为了提高消息的可靠性,Kafka每个topic的partition有N个副本(replicas),其中N(大于等于1)是topic的复制因子(replica fator)的个数。Kafka通过多副本机制实现故障自动转移,当Kafka集群中一个broker失效情况下仍然保证服务可用。在Kafka中发生复制时确保partition的日志能有序地写到其他节点上,N个replicas中,其中一个replica为leader,其他都为follower,leader处理partition的所有读写请求,与此同时,follower会被动定期地去复制leader上的数据。

如下图所示,Kafka集群中有4个broker, 某topic有3个partition,且复制因子即副本个数也为3:

图2-14  Kafka副本机制

Kafka提供了数据复制算法保证,如果leader发生故障或挂掉,一个新leader被选举并被接受客户端的消息成功写入。Kafka确保从同步副本列表中选举一个副本为leader,或者说follower追赶leader数据。leader负责维护和跟踪ISR(In-Sync Replicas的缩写,表示副本同步队列,具体可参考下节)中所有follower滞后的状态。当producer发送一条消息到broker后,leader写入消息并复制到所有follower。消息提交之后才被成功复制到所有的同步副本。消息复制延迟受最慢的follower限制,重要的是快速检测慢副本,如果follower“落后”太多或者失效,leader将会把它从ISR中删除

3.  ISR(ISR大于等于2,不会丢失数据,但是可能重复数据)

ISRIn-Sync Replicas),副本同步队列。ISR中包括leader和follower。副本数对Kafka的吞吐率是有一定的影响,但极大的增强了可用性。默认情况下Kafka的replica数量为1,即每个partition都有一个唯一的leader,为了确保消息的可靠性,通常应用中将其值(由broker的参数offsets.topic.replication.factor指定)大小设置为大于1,比如3。 所有的副本(replicas)统称为Assigned Replicas,即AR。ISR是AR中的一个子集,由leader维护ISR列表,follower从leader同步数据有一些延迟(包括延迟时间replica.lag.time.max.ms和延迟条数replica.lag.max.messages两个维度, 当前最新的版本0.10.x中只支持replica.lag.time.max.ms这个维度),任意一个超过阈值都会把follower剔除出ISR, 存入OSROutof-Sync Replicas)列表,新加入的follower也会先存放在OSR中。

AR=ISR+OSR

Kafka 0.10.x版本后移除了replica.lag.max.messages参数,只保留了replica.lag.time.max.ms作为ISR中副本管理的参数。为什么这样做呢?

replica.lag.max.messages表示当前某个副本落后leaeder的消息数量超过了这个参数的值,那么leader就会把follower从ISR中删除。假设设置replica.lag.max.messages=4,那么如果producer一次传送至broker的消息数量都小于4条时,因为在leader接受到producer发送的消息之后而follower副本开始拉取这些消息之前,follower落后leader的消息数不会超过4条消息,故此没有follower移出ISR,所以这时候replica.lag.max.message的设置似乎是合理的。但是producer发起瞬时高峰流量,producer一次发送的消息超过4条时,也就是超过replica.lag.max.messages,此时follower都会被认为是与leader副本不同步了,从而被踢出了ISR。但实际上这些follower都是存活状态的且没有性能问题,那么在之后追上leader,并被重新加入了ISR,于是就会出现它们不断地剔出ISR然后重新回归ISR,这无疑增加了无谓的性能损耗。而且这个参数是broker全局的。设置太大了,影响真正“落后”follower的移除;设置的太小了,导致follower的频繁进出。无法给定一个合适的replica.lag.max.messages的值,故此,新版本的Kafka移除了这个参数。

HWHighWatermark的缩写,俗称高水位取一个partition对应的ISR中最小的LEO作为HWconsumer最多只能消费到HW所在的位置。每个replica都有HW,leader和follower各自负责更新自己的HW的状态。对于leader新写入的消息,consumer不能立刻消费,leader会等待该消息被所有ISR中的replicas同步后更新HW,此时消息才能被consumer消费,这样就保证了如果leader所在的broker失效,该消息仍然可以从新选举的leader中获取。对于来自内部broker的读取请求,没有HW的限制。

(一个partition的副本分为LeaderFollowerLeaderFollower都维护了HWLEO,而partition的读写都在Leader完成,LeaderHW是所有ISR列表里副本中最小的那个的LEO,当Leader新写入消息后,LeaderLEO更新到加入新消息后的位置,LeaderHW仍在原位置,当所有的Follower都同步完成后,HW更新到最新位置,此时最新写入的消息才能被Consumer消费)

图2-15详细的说明了当producer生产消息至broker后,ISR以及HW和LEO的流转过程:

图2-15  ISR、HW、LEO流转过程

由此可见,Kafka的复制机制既不是完全的同步复制,也不是单纯的异步复制。事实上,同步复制要求所有能工作的follower都复制完,这条消息才会被commit,这种复制方式极大的影响了吞吐率。而异步复制方式下,follower异步的从leader复制数据,数据只要被leader写入log就被认为已经commit,这种情况下如果follower都还没有复制完,落后于leader时,突然leader宕机,则会丢失数据。而Kafka的这种使用ISR的方式则很好的均衡了确保数据不丢失以及吞吐率。

KafkaISR的管理最终都会反馈到Zookeeper节点上。具体位置为:/brokers/topics/[topic]/partitions/[partition]/state。目前有两个地方会对这个Zookeeper的节点进行维护:

Controller:Kafka集群中的其中一个Broker会被选举为Controller,主要负责Partition管理和副本状态管理,也会执行类似于重分配partition之类的管理任务。在符合某些特定条件下,Controller下的LeaderSelector会选举新的leader,将ISR和新的leader_epoch及controller_epoch写入Zookeeper的相关节点中。同时发起LeaderAndIsrRequest通知所有的replicas。

LeaderLeader有单独的线程定期检测ISRFollower是否脱离ISR,如果发现ISR变化,则会将新的ISR的信息返回到Zookeeper的相关节点中。

4.  数据可靠性和持久性保证

当producer向leader发送数据时,可以通过request.required.acks参数来设置数据可靠性的级别:

1(默认):producer等待broker的ack,partition的leader落盘成功后返回ack,如果在follower同步成功之前leader故障,那么将会丢失数据;

0:producer不等待broker的ack,这一操作提供了一个最低的延迟,broker一接收到还没有写入磁盘就已经返回,当broker故障时有可能丢失数据;

-1:producer等待broker的ack,partition的leader和follower全部落盘成功后才返回ack,数据一般不会丢失,延迟时间长但是可靠性最高。但是这样也不能保证数据不丢失,比如当ISR中只有leader时(前面ISR那一节讲到,ISR中的成员由于某些情况会增加也会减少,最少就只剩一个leader),这样就变成了acks=1的情况;

如果要提高数据的可靠性,在设置request.required.acks=-1的同时,也要min.insync.replicas这个参数(可以在broker或者topic层面进行设置)的配合,这样才能发挥最大的功效。min.insync.replicas这个参数设定ISR中的最小副本数是多少,默认值为1,当且仅当request.required.acks参数设置为-1时,此参数才生效。如果ISR中的副本数少于min.insync.replicas配置的数量时,客户端会返回异常:org.apache.kafka. common.errors. NotEnoughReplicasExceptoin: Messages are rejected since there are fewer in-sync replicas than required。

接下来对ack=1和-1的两种情况进行详细分析:

  1. Request.required.acks = 1

producer发送数据到Leader,Leader写本地日志成功,返回客户端成功;此时ISR中的副本还没有来得及拉取该消息,Leader就宕机了,那么此次发送的消息就会丢失。

图2-16  Request.required.acks = 1

  1. Request.required.acks = -1

同步(Kafka默认为同步,即producer.type=sync)的发送模式,replication.factor>=2min.insync.replicas>=2的情况下,不会丢失数据。

有两种典型情况。acks=-1的情况下(如无特殊说明,以下acks都表示为参数request.required.acks),数据发送到leader,ISR的Follower全部完1成数据同步后,Leader此时挂掉,那么会选举出新的Leader,数据不会丢失。

图2-17  Request.required.acks = -1

acks=-1的情况下,数据发送到leader ,部分ISR的副本同步,leader此时挂掉。比如follower1follower2都有可能变成新的leader, producer端会得到返回异常,producer端会重新发送数据,数据可能会重复。

图2-18  Leader宕机的情况

当然图2-17中所示,如果在leader crash的时候,follower2还没有同步到任何数据,而且follower2被选举为新的leader的话,这样消息就不会重复。

考虑图2-18(即acks=-1,部分ISR副本同步)中的另一种情况,如果在Leader挂掉的时候,follower1同步了消息4,5,follower2同步了消息4,与此同时follower2被选举为leader,那么此时follower1中的多出的消息5该做如何处理呢?

这里就需要HW的协同配合了。如前所述,一个partition中的ISR列表中,leader的HW是所有ISR列表里副本中最小的那个的LEO。类似于木桶原理,水位取决于最低那块短板。

图2-19  HW的作用

如图2-19,某个topic的某partition有三个副本,分别为A、B、C。A作为leader肯定是LEO最高,B紧随其后,C机器由于配置比较低,网络比较差,故而同步最慢。这个时候A机器宕机,这时候如果B成为leader,假如没有HW,在A重新恢复之后会做同步(makeFollower)操作,在宕机时log文件之后直接做追加操作,而假如B的LEO已经达到了A的LEO,会产生数据不一致的情况,所以使用HW来避免这种情况。

A在做同步操作的时候,先将log文件截断到之前自己的HW的位置,即3,之后再从B中拉取消息进行同步。

如果失败的follower恢复过来,它首先将自己的log文件截断到上次checkpointed时刻的HW的位置,之后再从leader中同步消息。leader挂掉会重新选举,新的leader会发送“指令”让其余的follower截断至自身的HW的位置然后再拉取新的消息。

注意:ISR中的个副本的LEO不一致时,如果此时leader挂掉,选举新的leader时并不是按照LEO的高低进行选举,而是按照ISR中的顺序选举,新的leader会发送“指令”让其余的follower截断至自身的HW的位置然后再拉取新的消息。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值