kafka 进阶

 

高性能架构

Kafka的高性能在宏观架构层面和具体实现层面都有体现。从宏观架构层面来讲,主要分两点:

  1. 利用Partition实现并行处理 由于不同Partition可位于不同机器,因此可以充分利用集群优势,实现机器间的并行处理。另一方面,由于Partition在物理上对应一个文件夹,即使多个Partition位于同一个节点,也可通过配置让同一节点上的不同Partition置于不同的磁盘上,从而实现磁盘间的并行处理,充分发挥多磁盘的优势。
  2. ISR实现可用性与数据一致性的动态平衡 Kafka的数据复制是以Partition为单位的。而多个备份间的数据复制,通过Follower向Leader拉取数据完成。从一这点来讲,Kafka的数据复制方案接近于Master-Slave方案。不同的是,Kafka既不是完全的同步复制,也不是完全的异步复制,而是基于ISR的动态复制方案

高效使用磁盘、零拷贝、减少网络开销。

顺序写磁盘

Kafka的整个设计中,Partition相当于一个非常长的数组,而Broker接收到的所有消息顺序写入这个大数组中。同时Consumer通过Offset顺序消费这些数据,并且不删除已经消费的数据,从而避免了随机写磁盘的过程。 由于磁盘有限,不可能保存所有数据,实际上作为消息系统Kafka也没必要保存所有数据,需要删除旧的数据。而这个删除过程,并非通过使用“读-写”模式去修改文件,而是将Partition分为多个Segment,每个Segment对应一个物理文件,通过删除整个文件的方式(直接删除Segment对应的整个log文件和整个index文件而非删除文件中的部分内容)去删除Partition内的数据。这种方式清除旧数据的方 式,也避免了对文件 的随机写操作。

 

Page Cache

首先介绍下Page cache机制:page cache是Linux操作系统的一个特色,系统内核为文件系统文件设置了一个缓存,对文件读写的数据内容都缓存在这里。这个缓存称为page cache(页缓存)。page cache是通过将磁盘中的数据缓存到内存中,从而减少磁盘I/O操作,从而提高性能。此外,还要确保在page cache中的数据更改时能够被同步到磁盘上。 Page cache由内存中的物理page组成,其内容对应磁盘上的block。page cache的大小是动态变化的,可以扩大,也可以在内存不足时缩小。cache缓存的存储设备被称为后备存储(backing store)。 当内核发起一个读请求时,首先会检查请求的数据是否缓存到了page cache中,如果有,那么直接从内存中读取,不需要访问磁盘。若未命中内存,就必须从磁盘中读取数据,然后内核将读取的数据缓存到cache中。 当内核发起一个写请求时,同样是直接往cache中写入,后备存储中的内容不会直接更新。内核会将被写入的page标记为dirty,并将其加入dirty list中。内核会周期性地将dirty list中的page写回到磁盘上,从而使磁盘上的数据和内存中缓存的数据一致。 Cache的回收有LRU和Two-List策略,Linux使用的是后者

使用Page Cache的好处如下: 1)I/O Scheduler会将连续的小块写组装成大块的物理写从而提高性能; 2)I/O Scheduler会尝试将一些写操作重新按顺序排好,从而减少磁盘头的移动时间; 3)充分利用所有空闲内存(非JVM内存)。如果使用应用层Cache(即JVM堆内存),会增加GC负担; 4)读操作可直接在Page Cache内进行。如果消费和生产速度相当,甚至不需要通过物理磁盘(直接通过Page Cache)交换数据; 5)如果进程重启,JVM内的Cache会失效,但Page Cache仍然可用。 Broker收到数据后,写磁盘时只是将数据写入Page Cache,并不保证数据一定完全写入磁盘。从这一点看,可能会造成机器宕机时,Page Cache内的数据未写入磁盘从而造成数据丢失。但是这种丢失只发生在机器断电等造成操作系统不工作的场景,而这种场景完全可以由Kafka层面的Replication机制去解决。如果为了保证这种情况下数据不丢失而强制将Page Cache中的数据Flush到磁盘,反而会降低性能。也正因如此,Kafka虽然提供了flush.messages和flush.ms两个参数将Page Cache中的数据强制Flush到磁盘,但是Kafka并不建议使用。

支持多Disk Drive

Broker的log.dirs配置项,允许配置多个文件夹。如果机器上有多个Disk Drive,可将不同的Disk挂载到不同的目录,然后将这些目录都配置到log.dirs里。Kafka会尽可能将不同的Partition分配到不同的目录,也即不同的Disk上,从而充分利用了多Disk的优势。 在$KAFKA_HOME/config/server.properties文件中,可以配置多个Kafka的日志目录:

 

字节零拷贝

为了减少字节拷贝,采用了sendfile系统调用,实现了零拷贝。Kafka中存在大量的网络数据持久化到磁盘(Producer到Broker)和磁盘文件通过网络发送(Broker到Consumer)的过程。这一过程的性能直接影响Kafka的整体吞吐量。 以将磁 盘文件通过 网络发送为 例。传统模 式下的方法 是先将文件 数据读入内 存,然后通 过Socket将 内存中的数 据发送出去。这一过程实际上发生了四次数据拷贝。首先通过系统调用将文件数据读入到内核态Buffer(DMA拷贝),然后应用程序将内存态Buffer数据读入到用户态Buffer(CPU拷贝),接着用户程序通过Socket发送数据时将用户态Buffer数据拷贝到内核态Buffer(CPU拷贝),最后通过DMA拷贝将数据拷贝到NIC Buffer。同时,还伴随着四次上下文切换。
Linux 2.4+内核通过sendfile系统调用,提供了零拷贝。数据通过DMA拷贝到内核态Buffer后,直接通过DMA拷贝到NIC Buffer,无需CPU拷贝。这也是零拷贝这一说法的来源。除了减少数据拷贝外,因为整个读文件-网络发送由一个sendfile调用完成,整个过程只有两次上下文切换,因此大大提高了性能

实际上是否能使用零拷贝与操作系统相关,如果操作系统提供sendfile这样的零拷贝系统调用,则Kafka会通过系统调用充分利用零拷贝的优势,否则并不能实现零拷贝。


批处理

批处理是一种常用的用于提高I/O性能的方式。对Kafka而言,批处理既减少了网络传输的开销,又提高了写磁盘的效率。 在实现上批处理不会立即将消息发送给Broker,而是先存到内部的队列中,直到消息条数达到阈值或者达到指定的Timeout才真正的将消息发送出去,从而实现了消息的批量发送。通过'batch.size'和'linger.ms'两个参数可以控制实际发送批次大小和发送频率。

Kafka从0.7开始,即支持将数据压缩后再传输给Broker。除了可以将每条消息单独压缩然后传输外,Kafka还支持在批量发送时,将整个Batch的消息一起压缩后传输。数据压缩的一个基本原理是,重复数据越多压缩效果越好。因此将整个Batch的数据一起压缩能更大幅度减小数据量,从而更大程度提高网络传输效率。Kafka支持snappy、gzip等多种格式的压缩。 Broker接收消息后,并不直接解压缩,而是直接将消息以压缩后的形式持久化到磁盘。Consumer Fetch到数据后再解压缩。因此Kafka的压缩不仅减少了Producer到Broker的网络传输负载,同时也降低了Broker磁盘操作的负载,也降低了Consumer与Broker间的网络传输量,从而极大得提高了传输效率,提高了吞吐量。 如何区分消息是压缩的还是未压缩的呢?Kafka在消息头部添加了一个描述压缩属性字节,这个字节的后两位表示消息的压缩采用的编码,如果后两位为0,则表示消息未被压缩。

 

序列化方式

Kafka消息的Key和Payload(或者说Value)的类型可自定义,只需同时提供相应的序列化器和反序列化器即可。因此用户可以通过使用快速且紧凑的序列化-反序列化方式(如Avro,ProtoBuffer)来减少实际网络传输和磁盘存储的数据规模,从而提高吞吐率。这里要注意,如果使用的序列化方法太慢,即使压缩比非常高,最终的效率也不一定高。

 

消费者组

consumer group是kafka提供的可扩展且具有容错性的消费者机制。既然是一个组,那么组内必然可以有多个消费者或消费者实例(consumer instance),它们共享一个公共的ID,即group ID。组内的所有消费者协调在一起来消费订阅主题(subscribed topics)的所有分区(partition)。当然,每个分区只能由同一个消费组内的一个consumer来消费。 consumer group有以下三个特性: 1)consumer group下可以有一个或多个consumer,consumer(可以是一个进程,也可以是一个线程; 2)group.id是一个字符串,唯一标识一个consumer group; 3)consumer group下订阅的topic下的每个分区只能分配给某个group下的一个consumer(当然该分区还可以被分配给其他group)。

对于属于不同consumer group的consumers,可以消费同1个partition,从而实现发布/订阅模式。但是在一个group内部,是不允许多个consumer消费同一个partition的,这也就意味着,对于1个topic,1个group来说,其partition数目 >= consumer个数。比如:对于1个topic,有4个partition,那么在一个group内部,最多只能有4个consumer。加入更多的consumer,它们也不会分配到partition。 简单来说,一方面这样做就没办法保证同1个partition中消息的时序;另一方面,Kafka的服务器,是每个topic的每个partition的每个consumer group对应1个offset,即(topic, partition, consumer_group_id) <-–> offset。如果多个consumer并行消费同1个partition,那offset的confirm就会出问题。

 

消费者位置

消费者在消费的过程中需要记录自己消费了多少数据,即消费位置信息。在Kafka中这个位置信息有个专门的术语:位移(offset)。很多消息引擎都把这部分信息保存在服务器端(broker端)。这样做的好处当然是实现简单,但会有三个主要的问题:

  1. broker从此变成有状态的,会影响伸缩性;
  2. 需要引入应答机制(acknowledgement)来确认消费成功;
  3. 由于要保存很多consumer的offset信息,必然引入复杂的数据结构,造成资源浪费。

而Kafka选择了不同的方式:每个consumer group保存自己的位移信息,那么只需要简单的一个整数表示位置就够了;同时可以引入checkpoint机制定期持久化,简化了应答机制的实现。

位移管理:

Kafka默认是定期帮你自动提交位移的(enable.auto.commit = true),当然你可以选择手动提交位移实现自己控制。kafka会定期把group消费情况保存起来。

 

位移提交:

老版本的位移是提交到zookeeper中的。但是zookeeper其实并不适合进行大批量的读写操作,尤其是写操作。

因此新版本的kafka提供了另一种解决方案:增加__consumer_offsets topic,将offset信息写入这个内置的topic中,

摆脱offset对zookeeper的依赖,__consumer_offsets默认有50个分区。

__consumer_offsets中的消息保存了每个consumer group某一时刻提交的offset信息。

__consumer_offsets格式为:组id  topic+分区 偏移 |group.id topic+partition offset| 

例如: |test-group topicA-0 8| |test-group topicA-1 6|

__consumers_offsets topic配置了compact策略,使得它总是能够保存最新的位移信息,既控制了该topic总体的日志容量,

 

两种日志清除策略

kafka log的清理策略有两种:delete,compact,默认是delete
这个对应了kafka中每个topic对于record的管理模式

delete:一般是使用按照时间保留的策略,当不活跃的segment的时间戳是大于设置的时间的时候,当前segment就会被删除
compact: 日志不会被删除,会被去重清理,这种模式要求每个record都必须有key,然后kafka会按照一定的时机清理segment中的key,对于同一个key只保留最新的那个key.

位移提交有两种方式:自动或手动。Kafka默认为自动提交,而手动提交又有两种方式:

  1. 在消息pull下来后业务处理前立即提交。此种情况可能出现offset提交成功,但消息处理异常了,导致消息丢失。
  2. 在消息pull下来后业务处理完成再提交。此种情况可能出现因为业务处理时间过长,超过session.out.time,导致消费者假死,或者新消费者加入kafka触发rebalance,从而提交offset失败,又会出现重复消费。

如果消息处理时间过长导致rebalance确实会出现重复消费,目前Kafka无法完美地避免该问题的出现。可以调整'max.poll.interval.ms'来避免这种情况的出现。

注:为了减少__consumer_offsets中的消息数量,最好批量commit,而不是一条消息就commit一次。

消费者每commit一次,就会往__consumer_offsets主题中写一条消息,来记录对应group各分区当前消费到的offset。因此,如果commit次数比较多,__consumer_offsets主题中的消息也会越积越多,这样就会占用很多的磁盘空间,为了管理__consumer_offsets主题中的消息,Kafka提供了'offsets.retention.minutes'配置项,来指定offset的最长保留时间(默认24小时),超过该时间的offset日志都会被清理,清理线程的周期通过'offsets.retention.check.interval.ms'参数配置(默认为10分钟扫描一次)

注:当commit的时候,可以取出一批数据后统一commit,这样__consumer_offsets主题中offset的日志就会少些,如果一条一条的commit,offset日志就会很大。如果分批commit的话,offset日志中的offset并不是连续的。

 

消费者组管理者:coordinator 组管理

Kafka提供了一个角色:coordinator,来执行对于consumer group的管理。最开始的coordinator是依赖ZK来实现组管理的,但是因为zookeeper的“herd”与“split brain”效应,导致一个group里面,不同的consumer拥有了同一个partition,进而会引起消息的消费错乱。为此,在0.9中,不再用zookeeper,而是Kafka集群本身来进行管理。 基于这些潜在的弊端,0.9版本的kafka改进了coordinator的设计,提出了group coordinator——每个consumer group都会被分配一个这样的coordinator用于组管理和位移管理。这个group coordinator比原来承担了更多的责任,比如组成员管理、位移提交保护机制等。当新版本consumer group的第一个consumer启动的时候,它会去和kafka server确定谁是它们组的coordinator。之后该group内的所有成员都会和该coordinator进行协调通信。显而易见,这种coordinator设计不再需要zookeeper了,性能上可以得到很大的提升。

确定组管理是哪个broker 【确定coodinator】

consumer group如何确定自己的coordinator是谁呢?简单来说分为两步: 1、首先确定consumer group位移信息写入__consumers_offsets的哪个分区。 具体计算公式:__consumers_offsets partition = Math.abs(groupId.hashCode() % groupMetadataTopicPartitionCount) 注意:groupMetadataTopicPartitionCount由'offsets.topic.num.partitions'指定,默认是50个分区。 2、该分区leader所在的broker就是被选定的coordinator。

partition分配机制

partition的分配有下面3个步骤:

  1. 对于每一个consumer group,Kafka为其从broker集群中选择一个broker作为其coordinator。因此,第一步就是找到这个coordinator;
  2. 找到coordinator之后,发送JoinGroup请求;
  3. JoinGroup返回之后,发送SyncGroup,得到自己所分配到的partition。

注意,在上面3步中,有一个关键点: partition的分配策略和分配结果其实是由客户端决定的,而不是由coordinator决定的。

 

分区分配为何由客户端决定

所有consumer都往coordinator发送JoinGroup消息之后,coordinator会指定其中一个consumer作为leader,其他consumer作为follower。

然后由这个leader进行分区分配。然后在第3步,leader通过SyncGroup消息,把分配结果发给coordinator,其他consumer也发送SyncGroup消息,获得这个分配结果。

为什么要在consumer中选一个leader出来,进行分配,而不是由coordinator直接分配呢?Kafka的官方文档有详细的分析,其中一个重要原因是为了灵活性:

如果让server分配,一旦需要新的分配策略,server集群要重新部署,这对于已经在线上运行的集群来说,代价是很大的;

而让client分配,server集群就不需要重新部署了,这样做可以有更好的灵活性。

比如这种机制下我可以实现类似于Hadoop那样的机架感知(rack-aware)分配方案,即为consumer挑选同一个机架下的分区数据,减少网络传输的开销。

我们也可以覆盖consumer的参数:'partition.assignment.strategy'来实现自己的分配策略。

 

Rebalance机制

什么是Rebalance?Rebalance本质上是一种协议,规定了一个consumer group下的所有consumer如何达成一致来分配订阅topic的每个分区。

比如某个group下有20个consumer,它订阅了一个具有100个分区的topic。正常情况下,Kafka平均会为每个consumer分配5个分区。这个分配的过程就叫rebalance。

简单来讲所谓rebalance,就是在某些条件下,partition要在同一个group中的consumer中重新分配。

什么时候触发Rebalance?触发条件有以下几种:

    1. 组成员发生变更。该条件又细分以下3种情况:

  • 有新的consumer加入;
  • 旧的consumer崩溃;
  • consumer调用unsubscrible(),取消topic的订阅,主动离开consumer group。

每次group进行rebalance之后,generation号都会加1,表示group进入到了一个新的版本。它表示rebalance之后的一代成员,

主要是用于保护consumer group,隔离无效offset提交的。比如上一代的consumer成员是无法提交位移到新一代的consumer group中。我们有时候可以看到ILLEGAL_GENERATION的错误,就是因为代出了问题。

 

Rebalance协议

Rebalance本质上是一组协议。group与coordinator共同使用它来完成group的rebalance。

目前kafka提供了5个协议来处理与consumer group coordination相关的问题:

  1. Heartbeat请求:consumer需要定期给coordinator发送心跳来表明自己还活着;
  2. LeaveGroup请求:主动告诉coordinator我要离开consumer group;
  3. JoinGroup请求:成员请求加入组;
  4. SyncGroup请求:group leader把分配方案告诉组内所有成员;
  5. DescribeGroup请求:显示组的所有信息,包括成员信息,协议名称,分配方案,订阅信息等。通常该请求是给管理员使用。 Coordinator在rebalance的时候主要用到了前面4种请求。

Heartbeat

consumer如何向coordinator证明自己还活着?通过定时向coordinator发送Heartbeat请求。

如果超过了设定的超时时间,那么coordinator就认为这个consumer已经挂了。

一旦coordinator认为某个consumer挂了,那么它就会开启新一轮rebalance,并且在当前其他consumer的心跳response中添加“REBALANCE_IN_PROGRESS”,

告诉其他consumer:你们需要重新申请加入组! 定期发送如何实现呢?

一个直观的想法就是开一个后台线程,定时发送heartbeat消息,但维护一个后台线程,很显然会增大实现的复杂性。而consumer是单线程程序,

在这里是通过DelayedQueue来实现的。其基本思路是把HeartBeatRequest放入一个DelayedQueue中,

然后在while循环的poll中,每次从DelayedQueue中把请求拿出来发送出去(只有时间到了,Task才能从Queue中拿出来)。

对应的实现类为: class HeartbeatTask implements DelayedTask {...}

 

 

Rebalance过程

Rebalance的前提是coordinator已经确定了。

总体而言,rebalance分为2步:Join和Sync。

  1. Join就是加入组。这一步中,所有成员都向coordinator发送JoinGroup请求,请求入组。一旦所有成员都发送了JoinGroup请求,coordinator会从中选择一个consumer担任leader的角色,并把组成员信息以及订阅信息发给leader。注意leader和coordinator不是一个概念。leader负责消费分配方案的制定。
  2. Sync,这一步leader开始分配消费方案,即哪个consumer负责消费哪些topic的哪些partition。一旦完成分配,leader会将这个方案封装进SyncGroup请求中发给coordinator,非leader也会发SyncGroup请求,只是内容为空。coordinator接收到分配方案之后会把方案放进SyncGroup的response中发给各个consumer。这样组内的所有成员就都知道自己应该消费哪些分区了。

 

消费者数量对性能的影响

  1. consumer端性能 消费者组内引入多个consumer的初衷大多是为了提升消费性能,即提升消费的吞吐量,组内的每个consumer成员可以同时消费各自负责的partition。如果组内只有一个consumer,那该consumer负责消费所有的partition,这个时候消费端总的TPS也受限于该单consumer的性能。 如果数据生产速度比较快且消费者端处理消息的性能不是瓶颈,那么500个Consumer与50个Consumer的处理速度会差10倍。如果数据生产速度比较慢,50个Consumer足以及时消费并处理完,那换500个Consumer并不会带来消费速度的提升。
  2. rebalance性能 从不好的方面看,某个组内的consumer数越多,通常意味着该group经rebalance后达到稳定状态的时间也就越长。,因而可能需要为'max.poll.interval.ms'设置更大的值。具体的原因就在于coordinator需要等待所有的组成员都发送JoinGroup请求后才会将group置于AwaitingSync状态,然后等待leader成员分配方案并将方案发送给它,之后coordinator下发分配方案给各个成员。若该过程中有组成员加入比较慢,就会造成大家一起等待。很显然,组内成员数越多,rebalance过程的开销就越有可能增大。
  3. broker端性能: 假设有500个broker,每个broker上一个partition(500个partition)。这时候,group内50个Consumer和500个Consumer,对于Broker来说,几乎没有影响。因为Consumer对Topic的消费,是按Partition来的,每个Partition只会被一个Consumer消费,每个Consumer-Partition都意味着一个TCP连接。因此,不管组内有多少个Consumer,broker端都会创建500个Socket连接,对于broker端资源的消耗是相同的。区别在于,50个Consumer时,平均每个Consumer会向Broker发起10个连接;500个Consumer时,平均每个Consumer只会向Broker发起1个连接。 当然,50个Consumer与500个Consumer,同时处理数据的速度不一样,因此向Broker发起的请求数也不一样。从这个角度考虑,Consumer越多,同时能处理的数据越多,能消费的数据越多,对Broker的压力越大。但是,这点只适用于少量Consumer时数据处理速度低于数据生产速度的场景。如果数据生产速度本身比较慢,50个Consumer或者500个Consumer每秒所需处理和消费的数据与数据生产速度相同,此时50还是500个Consumer,对Broker几乎没有区别。

 

Controller介绍

在Kafka早期版本,对于分区和副本的状态的管理依赖于zookeeper的Watcher和队列:每一个broker都会在zookeeper注册Watcher,所以zookeeper就会出现大量的Watcher。如果宕机的broker上的partition很多比较多,会造成多个Watcher触发,造成集群内大规模调整;每一个replica都要去再次zookeeper上注册监视器,当集群规模很大的时候,zookeeper负担很重。这种设计很容易出现'brain split'和'heard effect'以及zookeeper集群过载。

新版本该变了这种设计,使用Kafka Controller。Kafka集群中多个broker,有一个会被选举为controller leader,负责管理整个集群中分区和副本的状态。只有controller leader会向zookeeper上注册Watcher,其他broker几乎不用监听zookeeper的状态变化。

 

Controller选举

当broker启动的时候,都会创建KafkaController对象,但是集群中只能有一个leader对外提供服务,这些每个节点上的KafkaController会在指定的zookeeper路径下创建临时节点,只有第一个成功创建的节点的KafkaController才可以成为leader,其余的都是follower。当leader故障后,所有的follower会收到通知,再次竞争在该路径下创建节点从而选举新的leader。 Kafka中的controller选举的工作依赖于Zookeeper,成功竞选为controller的broker会在Zookeeper中创建/controller这个临时节点,此临时节点的内容参考如下: {"version":1,"brokerid":0,"timestamp":"1529210278988"} 其中version在目前版本中固定为1,brokerid表示称为控制器的broker的id编号,timestamp表示竞选称为控制器时的时间戳。

 

选举过程

在任意时刻,集群中有且仅有一个控制器。每个broker启动的时候会去尝试去读取/controller节点的brokerid的值,如果读取到brokerid的值不为-1,则表示已经有其它broker节点成功竞选为控制器,所以当前broker就会放弃竞选;如果Zookeeper中不存在/controller这个节点,或者这个节点中的数据异常,那么就会尝试去创建/controller这个节点,当前broker去创建节点的时候,也有可能其他broker同时去尝试创建这个节点,只有创建成功的那个broker才会成为控制器,而创建失败的broker则表示竞选失败。每个broker都会在内存中保存当前控制器的brokerid值,这个值可以标识为activeControllerId。

 

唯一性保证

Zookeeper中还有一个与控制器有关的/controller_epoch节点,这个节点是持久节点,节点中存放的是一个整型的controller_epoch值。controller_epoch用于记录控制器发生变更的次数,即记录当前的控制器是第几代,我们也可以称之为“纪元”。controller_epoch的初始值为1,即集群中第一个控制器的纪元为1,当控制器发生变更时,每选出一个新的控制器就将该字段值加1。

每个和控制器交互的请求都会携带上controller_epoch这个字段,如果请求的controller_epoch值小于内存中的controller_epoch值,则认为这个请求是向已经过期的控制器所发送的请求,那么这个请求会被认定为无效的请求。如果请求的controller_epoch值大于内存中的controller_epoch值,那么则说明已经有新的控制器当选了。由此可见,Kafka通过controller_epoch来保证控制器的唯一性,进而保证相关操作的一致性。

 

Controller职责

具备控制器身份的broker需要比其他普通的broker多一份职责,具体细节如下:

  1. 监听partition相关的变化(监听ZK的'/admin/reassign_partitions'节点)
  2. 处理ISR集合变更(监听ZK的'/isr_change_notification'节点);
  3. 处理优先副本的选举(监听'/admin/preferred-replica-election'节点);
  4. 监听topic相关的变化。包括:处理topic增减的变化(监听ZK的'/brokers/topics'节点)、处理删除的topic(监听ZK的'/admin/delete_topics'节点);
  5. 监听broker相关的变化,用来处理broker的增减(监听ZK的'/brokers/ids/'节点);
  6. 从Zookeeper中读取获取当前所有与topic、partition以及broker有关的信息并进行相应的管理;
  7. 启动并管理分区状态机和副本状态机;
  8. 更新集群的元数据信息;
  9. 如果参数'auto.leader.rebalance.enable'设置为true,则还会开启一个名为'auto-leader-rebalance-task'的定时任务来负责维护分区的优先副本的均衡

控制器在选举成功之后会读取Zookeeper中各个节点的数据来初始化上下文信息(ControllerContext),并且也需要管理这些上下文信息,比如为某个topic增加了若干个分区,控制器在负责创建这些分区的同时也要更新上下文信息,并且也需要将这些变更信息同步到其他普通的broker节点中。不管是监听器触发的事件,还是定时任务触发的事件,亦或者是其他事件(比如ControlledShutdown)都会读取或者更新控制器中的上下文信息,那么这样就会涉及到多线程间的同步,如果单纯的使用锁机制来实现,那么整体的性能也会大打折扣。针对这一现象,Kafka的控制器使用单线程基于事件队列的模型,将每个事件都做一层封装,然后按照事件发生的先后顺序暂存到LinkedBlockingQueue中,然后使用一个专用的线程(ControllerEventThread)按照FIFO(First Input First Output, 先入先出)的原则顺序处理各个事件,这样可以不需要锁机制就可以在多线程间维护线程安全。

 

posted @ 2022-02-19 21:32  G1733  阅读(104)  评论(0编辑  收藏  举报