Kafka学习笔记总结

本文详细探讨了Kafka的实现原理,包括生产者客户端实现、服务端网络层交互、数据读写机制、高可用性保障及消费者客户端拉取消息的流程。涵盖了从消息发送到服务端的整个数据流,以及Kafka集群中的元数据管理、副本机制、负载均衡和消费者分组等核心概念。
摘要由CSDN通过智能技术生成

目录

0 本文主要涉及

1研究的几个点

2基本概念简介

消息队列

Kafka

Kafka相关概念

Topic

Producer

Consumer

Consumer Group

Broker

Partition

Replica

Kafka数据流概览

Kafka本质

Kafka 使用

Kafka为开发者提供了四类API:

3kafka生产者客户端实现

Producer发送消息流程:

发送消息时方法调用流程:

主线程部分一些关键对象和流程说明:

ProducerInterceptor

集群元数据相关对象及更新操作

Partitioner.partition()方法

RecordAccumulator部分关键对象和流程说明:

MemoryRecords

RecordBatch

RecordAccumulator

Sender线程部分关键对象和流程说明:

发送消息的流程:

NetworkClient

相关方法:

InFlightRequests

DefaultMetadataUpdater

ClusterConnectionStates

4kafka服务端网络层实现,如何与客户端交互

工作原理:

SocketServer相关对象和方法:

SocketServer

Acceptor

Processor 

RequestChannel

KafkaRequestHandlerPool

KafkaRequestHandler

KafkaApis

请求数据从生产者发送到服务端的流转过程

5kafka服务端如何读写消息数据,如何维护日志数据

日志文件结构

偏移量offset

日志读写相关对象和方法

FileMessageSet

OffsetIndex

LogSegment

Log

LogManager

高效日志文件读写原理

6kafka服务端如何保证高可用

KafkaController机制

KafkaController相关功能主要对象和方法说明:

ControllerChannelManager

ControllerContext

状态机

PartitionStateMachine

ReplicaStateMachine

ZooKeeper Listener

副本机制

副本机制主要对象和方法:

7kafka消费者客户端拉取消息

Consumer接口中定义KafkaConsumer对外的API:

一些关键对象说明:

ConsumerNetworkClient

SubscriptionState

消息拉取

提交拉取消息的进度

事务性 / 原子性广播

Consumer Group Rebalance

服务端负载均衡以及消息进度保存相关对象和方法:

GroupCoordinator


0 本文主要涉及

主要为Kafka实现原理学习笔记,以及一些基本的使用和维护方面记录。

1研究的几个点

1,kafka生产者客户端实现,如何发送消息
2,kafka服务端网络层实现,如何与客户端交互
3,kafka服务端如何读写消息数据,如何维护日志数据
4,kafka服务端如何保证高可用(KafkaController&Topic分区副本机制)
5,kafka消费者客户端实现,如何获取消息,如何实现负载均衡,如何实现消息传递语义

2基本概念简介

消息队列

指的是消息队列中间件(消息系统),它本质上就是提供逻辑上以队列的方式写入与读取数据的功能模块,
一般在系统架构设计中起到解耦应用、缓冲削峰、异步处理等作用,是异步 RPC 的主要手段之一。

Kafka

基于 zookeeper 协调的具有高性能、高吞吐量、持久化、多副本备份、具有横向扩展能力的大规模分布式集群、同时支持点到点以及发布/订阅的消息队列中间件系统
它主要设计目标:

  • 以时间复杂度为 O(1) 的方式提供消息持久化能力,并保证即使对 TB 级以上数据也能保证常数时间的访问性能
  • 高吞吐率。即使在非常廉价的商用机器上也能做到单机支持每秒 100K 条消息的传输
  • 支持 Kafka Server 间的消息分区,及分布式消息消费,同时保证每个 Partition 内的消息顺序传输
  • 同时支持离线数据处理和实时数据处理

Kafka相关概念

Topic

Kafka中程序按照Topic分类来维护消息,Producer生产者往 Topic 里写消息,Consumer消费者从Topic读消息,可以将一种Topic 的消息理解为一个队列 Queue

Producer

Producer生产者客户端以 push 模式将消息发布 (publish) 消息到 Topic 某个分区Partition的对应服务端

Consumer

Consumer消费者客户端通过订阅 (subscribe)Topic 获取并处理 Topic 中消息,通过提交偏移量确认消费进度

Consumer Group

用于定义可重复消费同一Topic消息不同分类的一组消费者(同分组消费者数最多不能超过分区数),同一 Topic 的一条消息只能被同一个 Consumer Group 内的一个 Consumer 消费,但可以同时被多个不同Consumer Group 消费

Broker

Kafka以集群的方式运行,集群中的每一台服务器称之为一个代理 (broker),消费者向他发布消息,消费者从他拉取数据Controller Broker 
Kafka集群中的其中一个服务器,用来进行和 zookeeper 通信,管理和协调 Kafka 集群:

  • 维护更新集群元数据信息
  • 创建 topic,删除topic
    Replica副本Leader选举,维护副本状态
    Broker加入集群、崩溃等情况处理
    受控关闭
  • Controller Leader选举
  • Partition分区分配,维护分区状态

GroupCoordinator Broker &Group Leader
GroupCoordinator Broker 组协调者 为服务端的一个Broker,Group Leader为一组消费者中的一个(由GroupCoordinator Broker选定,他们合作通过组协调协议(group coordination protocol)实现消费者成员管理、消费分配方案制定 (rebalance) 以及提交位移等

Partition

Kafka集群中每个Topic中的消息会被分为一个或若干个 Partition(用于提高消息的处理效率,遇到瓶颈时,可以通过增加 Partition的数量来进行横向扩容),一个Partition是一个有序的 message 序列,每条 message 都有唯一的offset编号,单个 Partition内是保证消息有序的 

Replica

Partition的副本,保障 Partition的高可用,通过算法均匀分布在多个Broker 上(同一个 partition 的 replica 在不同的 broker,同一个 topic+partition 只有一个 leader)
Replica 副本分为 Leader Replica 和 Follower Replica
Producer和 Consumer 只跟 Leader 交互,实际都是作用在 Leader 副本上,然后再同步到其他的 Follower,
在 leader 挂了之后,Controller 会从符合一定条件的 Follower 中选取一个作为新的 Leader 提供服务ISR(is-sync replica) 
同步副本集合,有资格被选为新的Leader 的副本集合,如果 Follower 延迟过大,会被踢出集合,追赶上数据之后,重新向 leader 申请,加入 ISR 集合

Kafka数据流概览

Kafka的每个Topic(主题)都可以分为多个Partition(分区),每个分区都有多个Replica(副本),实现消息冗余备份。
每个分区中的消息是不同的,这类似于数据库中水平切分的思想,提高了并发读写的能力。
而同一分区的不同副本中保存的是相同的消息,副本之间是一主多从的关系,其中Leader副本负责处理读写请求,Follower副本则只与Leader副本进行消息同步,当Leader副本出现故障时,则从Follower副本中重新选举Leader副本对外提供服务。这样,通过提高分区的数量,就可以实现水平扩展;通过提高副本的数量,就可以提高容灾能力。
 


Producers 往 Brokers 里面的指定 Topic 中写消息,Consumers 从 Brokers 里面拉去指定 Topic 的消息
两种信息队列实现流程如下,
发布 - 订阅消息的工作流程:
1,生产者定期向主题发送消息
2,Kafka Broker服务端接收消息并持久化到多个或一个分区
3,多个分组的消费者分别订阅该特定主题,定期poll轮询拉取数据,每组消费者处理消息后提交新的消费进度offset到服务端,服务端保存到特定的Topic记录,消费者可以随时回退 / 跳到所需的主题偏移量,并阅读所有后续消息。
单一队列消息工作流程:
1,生产者定期向主题发送消息
2,Kafka Broker服务端接收消息并持久化并持久化到一个分区
3,唯一的消费者订阅该特定主题,定期poll轮询拉取数据,唯一分区内的消息可保证消息发送顺序

Kafka本质

Kafka核心本质:一套分部式磁盘文件有序批量读写系统

Kafka 使用

Kafka为开发者提供了四类API:

生产者 API
支持应用程序发布发布消息到1个或多个Topic,并提供了拦截器(interceptor)支持用于实现clients端的定制化控制逻辑
消费者 API
支持应用程序订阅一个或多个Topic,并处理产生的消息(定时提交偏移量到Kafka服务端,记录于内部 topic(consumer_offsets)上),提供了拦截器(interceptor)支持用于实现clients端的定制化控制逻辑
Stream API 
提供了比Producer 和 Consumer 更高级别的抽象,从 1 个或多个 topic 消费输入流,并生产一个输出流到 1 个或多个输出 Topic,将输入流转换为输出流并产生结果,可以实现与 Storm、HBase 和 Spark 的集成,主要处理大数据(最新版本还提供了KSQL)无状态操作,例如信息流的过滤和转换;有状态操作,例如在一个时间窗口内的连接、聚合操作
Connector API
构建或运行可重复使用的生产者或消费者,实现从某些源数据系统持续拉入数据到Kafka或从Kafka输出数据数据到别的系统。
另有:AdminClient API
AdminClient API支持管理和检查Topic、brokers等Kafka中的对象

3kafka生产者客户端实现

Producer生产者主要做的事情就是获取要发送的消息,然后确认发到哪里,然后发送并处理返回结果。
KafkaProducer实现了Producer接口,在Producer接口中定义了对外提供的API:
send()方法:发送消息,实际是将消息放入RecordAccumulator暂存,等待发送,支持同步和异步两种模式的消息发送,send方法返回的是一个Future,基于Future实现同步或异步的消息发送语义:
    同步:调用send返回Future时,需要立即调用get,因为Future.get在没有返回结果时会一直阻塞。
    异步:提供一个回调,调用send后可以继续发送消息而不用等待,当有结果返回时,会自动执行回调函数。
flush()方法:刷新操作,等待RecordAccumulator中所有消息发送完成,在刷新完成之前会阻塞调用的线程。
partitionsFor()方法:在KafkaProducer中维护了一个Metadata对象用于存储Kafka集群的元数据,Metadata中的元数据会定期更新。partitionsFor()方法负责从Metadata中获取指定Topic中的分区信息。
close()方法:关闭此Producer对象,主要操作是设置close标志,等待RecordAccumulator中的消息清空,关闭Sender线程。
metrics()方法:用于记录统计信息

Producer发送消息流程:



① ProducerInterceptors对消息进行拦截处理
② Serializer对消息的key和value进行序列化
③ Partitioner为消息选择合适的Partition
④ RecordAccumulator收集消息,实现批量发送
⑤ Sender从RecordAccumulator获取消息
⑥ 构造ClientRequest
⑦ 将ClientRequest交给NetworkClient,准备发送
⑧ NetworkClient将请求放入KafkaChannel的缓存
⑨ 执行网络I/O,发送请求
⑨ 收到响应,调用ClientRequest的回调函数
⑪ 调用RecordBatch的回调函数,最终调用每个消息上注册的回调函数

发送消息时方法调用流程:

调用ProducerInterceptors.onSend()方法,通过ProducerInterceptor对消息进行拦截或修改
调用waitOnMetadata()方法获取Kafka集群的信息,底层会唤醒Send线程更新Metadata中保存的Kafka集群元数据
调用Serializer.serialize()方法序列化消息的key和value
调用partition()为消息选择合适的分区
调用RecordAccumulator.append()方法,将消息追加到RecordAccumulator中
唤醒Sender线程,由Sender线程将RecordAccumulator中缓存的消息以ProducerRequest格式批量发送出去
Kafka的记录收集器(RecordAccumulator)负责缓存生产者客户端产生的消息,发送线程(Sender)负责读取记录收集器的批量消息,通过网络发送给服务端。

主线程部分一些关键对象和流程说明:

ProducerInterceptor

添加自定义预操作,功能类似于Java Web中的Filter,它可以在消息发送之前对其进行拦截或修改,也可以先于用户的Callback,对ACK响应进行预处理。自定义使用ProducerInterceptor类,只要实现ProducerInterceptor接口,创建其对象并添加到ProducerInterceptors中即可

集群元数据相关对象及更新操作

抽象三个基本类用于描述Kafka集群元数据:
Node 表示集群中的一个节点,Node记录这个节点的host、ip、port等信息。
TopicPartition 表示某Topic的一个分区,其中的topic字段是Topic的名称,partition字段则此分区在Topic中的分区编号(ID)。
PartitionInfo 表示一个分区的详细信息。其中topic字段和partition字段的含义与TopicPartition中的相同,leader字段记录了Leader副本所在节点的id,replica字段记录全部副本所在的节点信息,inSyncReplicas字段记录了ISR集合中所有副本所在的节点信息
而Cluster类中记录了三类数据的关系从而确定实际实际需要发往的服务端相关信息
Metadata对Cluster对象再次封装,并保存Cluster数据的最后更新时间、版本号(version)、是否需要更新等待信息
KafkaProducer.waitOnMetadata()方法负责触发Kafka集群元数据的更新,并阻塞主线程等待更新完毕
主要步骤是:
(1)检测Metadata中是否包含指定Topic的元数据,若不包含,则将Topic添加到topics集合中,下次更
新时会从服务端获取指定Topic的元数据
(2)尝试获取Topic中分区的详细信息,失败后会调用requestUpdate()方法设置Metadata. needUpdate字
段,并得到当前元数据版本号
(3)唤醒Sender线程,由Sender线程更新Metadata中保存的Kafka集群元数据
(4)主线程调用awaitUpdate()方法,等待Sender线程完成更新
(5)从Metadata中获取指定Topic分区的详细信息(即PartitionInfo集合)。若失败,则回到步骤2继续尝试,若等待时间超时,则抛出异常

Partitioner.partition()方法

DefaultPartitioner.partition()方法负责在ProduceRecord中没有明确指定分区编号的时候,为其选择合适的分区:
如果消息没有key,会根据counter与Partition个数取模来确定分区编号,count(AtomicInteger类型保证线程安全)不断递增,确保消息不会都发到同一个Partition里(round-robin);
如果消息有key的话,则对key进行hash(使用的是murmur2这种高效率低碰撞的Hash算法),然后与分区数量取模,来确定key所在的分区达到负载均衡

RecordAccumulator部分关键对象和流程说明:

KafkaProducer有同步和异步两种方式发送消息,其实两者的底层实现相同都是通过异步方式实现的。
主线程调用KafkaProducer.send()方法发送消息的时候,先将消息放到RecordAccumulator中暂存,然后主线程就从send()方法中返回了,此时消息并没有真正地发送给Kafka服务端,而是缓存在了RecordAccumulator中。
之后,业务线程通过KafkaProducer.send()方法不断向RecordAccumulator追加消息,当达到一定的条件,会唤醒Sender线程发送RecordAccumulator中的消息

MemoryRecords

RecordAccumulator中有一个以TopicPartition为key的ConcurrentMap,每个value是ArrayDeque<RecordBatch>(线程不安全的集合),其中缓存了发往对应TopicPartition的消息。每个RecordBatch拥有一个MemoryRecords对象的引用,MemoryRecords是消息最终存放的地方
MemoryRecords表示的是多个消息的集合,其中封装了:
buffer:用于保存消息数据的Java NIO ByteBuffer。
writeLimit:记录buffer字段最多可以写入多少个字节的数据。
compressor:压缩器,对消息数据进行压缩,将压缩后的数据输出到buffer。
writable:此MemoryRecords对象是只读的模式,还是可写模式。在MemoryRecords发送前时,会将其设置成只读模式。
Kafka客户端使用BufferPool来实现ByteBuffer的复用,每个BufferPool对象只针对特定大小(由poolableSize字段指定)的ByteBuffer进行管理,对于其他大小的ByteBuffer并不会缓存进BufferPool
BufferPool的关键字段:
free:是一个ArrayDeque<ByteBuffer>队列,其中缓存了指定大小的ByteBuffer对象。
ReentrantLock:因为有多线程并发分配和回收ByteBuffer,所以使用锁控制并发,保证线程安全。
waiters:记录因申请不到足够空间而阻塞的线程,此队列中实际记录的是阻塞线程对应的
Condition对象。
totalMemory:记录了整个Pool的大小。
availableMemory:记录了可用的空间大小,这个空间是totalMemory减去free列表中全部ByteBuffer
的大小。
BufferPool.allocate()方法负责从缓冲池中申请ByteBuffer,当缓冲池中空间不足时,就会阻塞调用线
程,deallocate()用于释放缓存

RecordBatch

RecordBatch封装MemoryRecords对象,和很多控制信息和统计信息
recordCount:记录了保存的Record的个数
maxRecordSize:最大Record的字节数
attempts:尝试发送当前RecordBatch的次数
lastAttemptMs:最后一次尝试发送的时间戳
records:指向用来存储数据的MemoryRecords对象
topicPartition:当前RecordBatch中缓存的消息都会发送给此TopicPartition
produceFuture:ProduceRequestResult类型,标识RecordBatch状态的Future对象
lastAppendTime:最后一次向RecordBatch追加消息的时间戳
thunks:Thunk对象的集合,消息的回调对象队列,Thunk中的callback字段就指向对应消息的Callback对象
offsetCounter:用来记录某消息在RecordBatch中的偏移量
retry:是否正在重试。如果RecordBatch中的数据发送失败,则会重新尝试发送
RecordBatch.tryAppend()方法尝试将消息添加到当前的RecordBatch中缓存
RecordBatch.done()方法回调RecordBatch中全部消息的Callback回调,处理正常响应、或超时、或关闭生产者等情况

RecordAccumulator

RecordAccumulator中管理了RecordBatch对象数据
batches:TopicPartition与RecordBatch集合的映射关系,类型是CopyOnWriteMap,是线程安全的集合,但其中的Deque是ArrayDeque类型,是非线程安全的集合,追加新消息或发送RecordBatch的时候,需要加锁同步。每个Deque中都保存了发往对应TopicPartition的RecordBatch集合。
batchSize:指定每个RecordBatch底层ByteBuffer的大小
Compression:压缩类型,GZIP、SNAPPY、LZ4等
incomplete:未发送完成的RecordBatch集合,底层通过Set<RecordBatch>集合实现
free:BufferPool对象
drainIndex:使用drain方法批量导出RecordBatch时,为了防止饥饿,使用drainIndex记录上次发送停止时的位置,下次继续从此位置开始发送
KafkaProducer.send()方法最终会调用RecordsAccumulator.append()方法将消息追加到RecordAccumulator中
KafkaProducer.doSend()方法最后一步就是判断此次向RecordAccumulator中追加消息后是否满足唤醒Sender线程条件,这里唤醒Sender线程的条件是消息所在队列的最后一个RecordBatch满了或此队列中不止一个RecordBatch
发送消息到服务端之前会调用RecordAccumulator.ready()方法获取集群中符合发送消息条件的节点集合:
(1)Deque中有多个RecordBatch或是第一个RecordBatch是否满了
(2)是否超时了
(3)是否有其他线程在等待BufferPool释放空间(即BufferPool的空间耗尽了)
(4)是否有线程正在等待flush操作完成
(5)Sender线程准备关闭

Sender线程部分关键对象和流程说明:

网络I/O操作是由Sender线程统一进行
Sender线程调用RecordAccumulator.drain()方法Node集合获取要发送的消息,返回Map<Integer, List<RecordBatch>>集合,key是NodeId,value是待发送的RecordBatch集合
drain()方法的核心逻辑是进行映射的转换:将RecordAccumulator记录的TopicPartition->RecordBatch集合的映射,转换成了NodeId->RecordBatch集合的映射
Sender实现了Runnable接口,并运行在单独的ioThread中

发送消息的流程:

(1)从Metadata获取Kafka集群元数据
(2)调用RecordAccumulator.ready()方法,根据RecordAccumulator的缓存情况,选出可以向哪些Node节点发送消息,返回ReadyCheckResult对象
(3)如果ReadyCheckResult中标识有unknownLeadersExist,则调用Metadata的requestUpdate方法,标记需要更新Kafka的集群信息
(4)针对ReadyCheckResult中readyNodes集合,循环调用NetworkClient.ready()方法,目的是检查网络I/O方面是否符合发送消息的条件,不符合条件的Node将会从readyNodes集合中删除。
(5)针对经过步骤4处理后的readyNodes集合,调用RecordAccumulator.drain()方法,获取待发送的消息集合
(6)调用RecordAccumulator.abortExpiredBatches()方法处理RecordAccumulator中超时的消息。其代码逻辑是,遍历RecordAccumulator中保存的全部RecordBatch,调用RecordBatch.maybeExpire()方法进行处理。如果已超时,则调用RecordBatch.done()方法,其中会触发自定义Callback,并将RecordBatch从队列中移除,释放ByteBuffer空间。
(7)调用Sender.createProduceRequests()方法将待发送的消息封装成ClientRequest(包含RequestSend,RequestCompletionHandler)
(8)调用NetWorkClient.send()方法,将ClientRequest写入KafkaChannel的send字段
(9)调用NetWorkClient.poll()方法,将KafkaChannel.send字段中保存的ClientRequest发送出去,同时还会处理服务端发回的响应、处理超时的请求、调用用户自定义Callback等

NetworkClient

NetworkClient.poll()方法调用KSelector.poll()进行网络I/O,并使用handle*()方法对KSelector.poll()产生的各种数据和队列进行处理
KSelector使用NIO同步非阻塞模式实现网络I/O操作,KSelector使用一个单独的线程可以管理多条网络连接上的连接、读、写等操作,包含字段:
nioSelector:java.nio.channels.Selector类型,用来监听网络I/O事件
channels:HashMap<String, KafkaChannel>类型,维护了NodeId与KafkaChannel之间的映射关系,表示生产者客户端与各个Node之间的网络连接。KafkaChannel是在SocketChannel上的又一层封装,其中Send和NetworkReceive分别表示读和写时用的缓存,底层通过ByteBuffer实现
completedSends:记录已经完全发送出去的请求
completedReceives:记录已经完全接收到的请求
stagedReceives:暂存一次OP_READ事件处理过程中读取到的全部请求。当一次OP_READ事件处理完成之后,会将stagedReceives集合中的请求保存到completeReceives集合中
disconnected、connected:记录一次poll过程中发现的断开的连接和新建立的连接
failedSends:记录向哪些Node发送的请求失败了
channelBuilder:用于创建KafkaChannel的Builder。根据不同配置创建不同的TransportLayer的子类,然后创建KafkaChannel
lruConnections:LinkedHashMap类型,用来记录各个连接的使用情况,并据此关闭空闲时间超过connectionsMaxIdleNanos的连接

相关方法:

KSelector.connect()方法主要负责创建KafkaChannel,并添加到channels集合中保存KSelector.send()方法将之前创建的RequestSend对象缓存到KafkaChannel的send字段中,并开始关注此连接的OP_WRITE事件,并没有发生网络I/O。在下次调用KSelector.poll()时,会将RequestSend对象发送出去
KSelector.poll()方法是真正执行网络I/O的地方,它会调用nioSelector.select()方法等待I/O事件发生。当Channel可写时,发送KafkaChannel.send字段,Channel可读时,读取数据到KafkaChannel.receive,读取一个完整的NetworkReceive后,会将其缓存到stagedReceives中,当一次pollSelectionKeys()完成后会将stagedReceives中的数据转移到completedReceives。最后调用maybeCloseOldestConnection()方法,根据lruConnections记录和connectionsMaxIdleNanos最大空闲时间,关闭
长期空闲的连接
KSelector.pollSelectionKeys()方法是处理I/O操作的核心方法,其中会分别处理OP_ CONNECT、OP_READ、OP_WRITE事件,并且会通过selectionKey.isValid()的返回值以及执行过程中是否抛出异常来判断连接的状态,并将断开的连接收集到disconnected集合,并在后续操作中进行重连

InFlightRequests

主要作用是缓存了已经发出去但没收到响应的ClientRequest。其底层是通过一个Map<String, Deque<ClientRequest>>对象实现

DefaultMetadataUpdater

用于辅助NetworkClient更新的Metadata的类
maybeUpdate()方法是DefaultMetadataUpdater的核心方法,用来判断当前的Metadata中保存的集群元数据是否需要更新。首先检测metadataFetchInProgress字段,如果没发送,满足下面任一条件即可更新:
Metadata.needUpdate字段被设置为true,且退避时间已到
长时间没更新,默认5分钟更新一次

ClusterConnectionStates

NetworkClient中所有连接的状态由ClusterConnectionStates管理,它底层使用Map<String,NodeConnectionState>实现,key是NodeId,value是NodeConnectionState对象,其中使用ConnectionState枚举表示连接状态,还记录了最近一次尝试连接的时间戳

4kafka服务端网络层实现,如何与客户端交互

服务端 (brokers) 和客户端 (producer、consumer) 之间之间通过和开发语言无关的 TCP 协议进行通讯
Kafka服务端网络层使用Reactor模式实现,Reactor是一种基于事件驱动的并发模式
Reactor模式的设计思想,实际上是将连接部分和请求部分用不同的线程来处理,这样请求的处理不会阻塞不断到来的连接。否则,如果服务端为每个客户端都维护一个连接,不仅会耗光服务端的资源,而且会降低服务端的性能。使用Reactor模式并结合选择器管理多个客户端的网络连接,可以减少线程之间的上下文切换和资源的开销。
采用多线程、多个Selector的设计

工作原理:

有助于理解的Java NIO相关概念:
SocketChannel(客户端网络连接通道),底层的字节数据读写都发生在通道上,比如从通道中读取数据、将数据写入通道。通道会和字节缓冲区一起使用,从通道中读取数据时需要构造一个缓冲区,调用channel.read(buffer)就会将通道的数据灌入缓冲区;将数据写入通道时,要先将数据写到缓冲区中,调用channel.write(buffer)可将缓冲区中的每个字节写入通道。
selector(选择器),发生在通道上的事件有读和写,选择器会通过选择键的方式监听读写事件的发生。
Selectionkey(选择键),将通道注册到选择器上,channel.register(selector)返回选择键,这样就将通道和选择器都关联了起来。读写事件发生时,通过选择键可以得到对应的通道,从而进行读写操作。

(1)首先创建ServerSocketChannel对象并在Selector上注册OP_ACCEPT事件,ServerSocketChannel负责监听指定端口上的连接请求。
(2)当客户端发起到服务端的网络连接时,服务端的Selector监听到此OP_ACCEPT事件,会触发Acceptor来处理OP_ACCEPT。
(3)当Acceptor接收到来自客户端的Socket连接请求时会为这个连接创建相应的SocketChannel,将SocketChannel设置为非阻塞模式,并在Selector上注册其关注的I/O事件,例如OP_READ、OP_WRITE等。此时,客户端与服务端之间的Socket连接正式建立完成。
(4)当客户端通过上面建立的Socket连接向服务端发送请求时,服务端的Selector会监听到OP_READ事件,并触发执行相应的处理逻辑(Reader Handler)。当服务端可以向客户端写数据时,服务端的Selector会监听到OP_WRITE事件,并触发执行相应的处理逻辑(Writer Handler)

服务端和网络层相关的组件:一个接收器线程(Acceptor)、多个处理器(Processor)、一个请求通道(Requestchannel)、一个请求队列(requestoueue)、多个响应队列(responseQueue)、一个请求处理线程连接池(KafkaRequestHandlerPool)、多个请求处理线程(KafkaRequestHandler)、一个服务端请求入口(KafkaApis)

SocketServer相关对象和方法:

SocketServer

SocketServer作为Broker对外提供Socket服务的模块,主要用于接收Socket连接的请求,然后产生相应为之服务的SocketChannel对象,通过此对象来和客户端相互通信。



Socketserver是一个NIO服务,它会启动一个接收器线程(Acceptor)和多个处理器(Processor)
每个Acceptor对应多个Processor线程,每个Processor线程拥有自己的Selector,主要用于从连接中读取请求和写回响应。每个Acceptor对应多个Handler线程,主要用于处理请求并将产生响应返回给Processor线程。
Processor线程与Handler线程之间通过RequestChannel进行通信。
SocketServer在初始化时会创建遍历所有的Endpoint,创建与其对应的Acceptor和Processor集合。
整体流程:
Acceptor线程用于监听默认端口号为9092上的Socket链接,然后当有新的Socket链接成功建立时会将对应的SocketChannel以轮询的方式转发给N个Processor线程中的某一个(其中N=num.network.threads,默认为3),并由其处理接下来该SocketChannel上的读写请求。当Processor线程监听来自SocketChannel的请求时,会将请求放置在RequestChannel中的请求队列;KafkaRequestHandler从RequestChannel的请求队列中获取Socket的请求,然后调用KafkaApis完成真正的业务逻辑,最后将响应写回至RequestChannel中的响应队;列当Processor 线程监听到SocketChannel 请求的响应时,会将响应从RequestChannel中的响应队列中取出来并发送给客户端。

Acceptor

Acceptor的主要功能是接收客户端建立连接的请求,创建Socket连接并分配给Processor处理
Acceptor中有两个比较重要的字段:
一个是Java NIO Selector
二是用于接收客户端请求的ServerSocketChannel对象
在创建Acceptor时会初始化上面两个字段,同时还会创建并启动其管理的Processors线程
Acceptor.run()方法调用Acceptor.accept()方法实现了对OP_ACCEPT事件的处理,它会创建SocketChannel并将其交给Processor.accept()方法处理,同时还会增加ConnectionQuotas中记录的连接数

Processor 

Processor主要用于完成读取请求和写回响应的操作,不参与具体业务逻辑的处理
在Acceptor.accept()方法中创建的SocketChannel会通过Processor.accept()方法交给Processor进行处理
Processor.accpet()方法接收到一个新的SocketChannel时会先将其放入newConnections队列中,然后会唤醒Processor线程来处理newConnections队列
Processor.run()方法中实现了从网络连接上读写数据的功能(底层实现通过KSelector.poll()方法,进行网络I/O,读取请求,发送响应)

RequestChannel

Processor线程与Handler线程之间传递数据是通过RequestChannel完成,RequestChannel提供了增删requestQueue队列、responseQueues集合以及responseListeners列表中元素的方法
RequestChannel中包含了一个requestQueue队列和多个responseQueues队列,每个Processor线程对应一个responseQueue。
Processor线程将读取到的请求存入requestQueue中,Handler线程从requestQueue队列中取出请求进行处理;
Handler线程处理请求产生的响应会存放到Processor对应的responseQueue中,Processor线程从其对应的responseQueue中取出响应并发送给客户端

KafkaRequestHandlerPool

KafkaRequestHandlerPool是一个简易版的线程池,管理了所有的KafkaRequestHandler线程,其个数默认为8个,由参数num.io.threads决定

KafkaRequestHandler

KafkaRequestHandler的主要职责是从RequestChannel获取请求并调用KafkaApis. handle()方法处理请求

KafkaApis

KafkaApis是Kafka服务器处理请求的入口类
负责将KafkaRequestHandler传递过来的请求分发到不同的handl*()处理方法中,分发的依据是RequestChannel.Request中的requestId,此字段保存了请求的ApiKeys的值,不同的ApiKeys值表示不同请求的类型。
KafkaApis负责具体的业务逻辑,它主要和Producer、Consumer、Broker Server交互。
KafkaApis 主要依赖以下四个组件来完成具体的业务逻辑:
    LogManager提供针对Kafka的Topic日志的读取和写入功能。
    ReplicaManager 提供针对Topic的分区副本数据的同步功能。
    offsetManager 提供针对提交至Kafka的偏移量的管理功能。
    KafkaScheduler为其他模块提供定时任务的调度和管理功能。(基于ScheduledThreadPoolExecutor实现)
KafkaApis在处理完请求后,会创建响应对象(Requestchannel.Response)并放入请求通道中。响应对象也持有请求对象的引用,因为请求对象中有处理器编号,所以响应对象可以从请求对象中获取处理器编号,确保对请求和响应的处理都是在同一个处理器中完成。
KafkaApis模块中,服务端对外暴露了十一条通信协议,即KafkaApis.handle()方法f负责处理的请求类型包括以下十一类:

ProducerRequest
生产者发送消息的请求。ProducerRequest.requiredAcks的取值是由request.required.acks决定的,在返回响应的时候主要针对request.required.acks的不同取值进行不同的处理:
当request.required.acks=0,生产者不关心Broker Server端消息持久化的执行结果,Broker Server只需要简单的返回即可,但是对于高级消费者发送的提交偏移量的请求还是需要返回具体的执行结果。
当request.required.acks=1,无论是生产者发送的请求还是高级消费者发送的情况,都需要将Broker Server端消息持久化的执行结果立刻返回给对应的客户端。
当request.required.acks=-1,此时不会立刻返回Broker Server端消息持久化的结果,而是需要等待Partition的ISR列表中的Replica完成数据同步,且ISR列表的个数大于min.insync.replicas时才会将响应返回给对应的客户端

FetchRequest
消费者获取消息的请求。当状态为Follower的Replica向状态为Leader的Replica同步数据时或者消费者获取数据时,Replica会发送FetchRequest此种类型的请求,Broker Server接收到此类请求之后,除了将需要的数据返回之外,还会根据不同的情况更新一些元数据信息。

OffsetRequest
获取Topic当前offset的元数据信息的请求。当消费者或者是客户端想要获取Topic某个时间段内的偏移量详情时会发送此种类型的请求,Broker Server接收到此请求之后,会返回指定修改时间之前的LogSegment的baseOffset。

TopicMetadataRequest
获取Topic元数据信息的请求。当生产者或者消费者想要获取Topic的元数据信息时会发送此种类型的请求,Broker Server接收到此请求之后,会把当前在线的所有Broker Server和Topic的所有分区信息返回,后者包括分区索引、Leader Replica、Assign Replicas、In-Sync Replicas等。

LeaderAndlsrRequest
Topic的元数据信息发生变化的请求。如果某Broker Server由于异常导致宕机,则原先Leader Replica在该Broker Server上的Topic 会发生Replica的Leader切换以及In-Sync Replicas列表的缩减;如果由于网络延迟,导致原先位于In-Sync Replicas列表中的Replica没有长时间向Leader Replica同步数据,最后导致落后太多的数据,此时需要将该Replica从ISR列表中剔除;如果用户手动重新指定Topic的分布情况,则可能发生Topic的Replica Leader切换以及ISR列表的变化等。当发生以上几种情况时,Leader 状态的KafkaController所在的Broker Server会向相关Broker Server下发Topic的Leader或者ISR列表发生变化的请求,Broker Server接收到此请求之后,会根据具体的内容对发生变化的Replica进行相应的处理。
针对Leader和ISR的变化,大致分两种情况:
当某个Replica成为Leader:1)暂停Fetch线程;2)添加进Assigned Replica列表;3)添加进In-Sync Replica列表;4)删除已经不存在的Assigned Replica;5)初始化Leader Replica的High Watermark。
当某个Replica成为Follower:1)暂停旧的Fetch线程;2)截断数据至High Watermark以下;3)开启新的Fetch线程;4)添加进Asigned Replica列表;5)删除已经不存在的Assigned Replica。

StopReplicaRequest
停止拷贝副本数据的请求。当Topic的某个分区被删除或者被强制下线的时候,处于Leader状态的KafkaController会发送此请求至相关的Broker Server,Broker Server 接收到此请求之后会针对Topic分区被删除的情况做相应的处理。

UpdateMetadataRequest
更新Topic元数据信息的请求。当Topic的元数据发生改变时,例如TopicAndPartition的Leader Replica,In-Sync Replicas,Assigned Replicas等等,处于Leader状态的KafkaController会发送此请求至相关的Broker Server,Broker Server接收到此请求之后会把相关的Topic的元数据持久化至内存,并且仅仅是持久化至内存。

BrokerControlledShutdownRequest
Broker Server下线的请求。当Broker Server准备下线的时候,会向处于Leader状态的KafkaController发送此请求,Leader 状态的KafkaController收到此请求之后会针对原先分配在该Broker Server上的TopicAndPartition 做相关处理。

OffsetCommitRequest
消费者保存偏移量的请求。当高级消费者间隔一定时间将不同Consumer Group的消费情况提交至Kafka集群时会发送此请求,Broker Server收到此请求之后会把详细的偏移量信息保存起来。

OffsetFetchRequest
获取消费者获取消费详情的请求。当高级消费者想要查询不同Consumer Group的消费情况时会发送此请求,Broker Server收到此请求之后会把详细的偏移量信息返回回来。

ConsumerMetadataRequest
高级消费者获取ConsumerGroup分区信息的请求。当offsets.storage配置为kafka时,其不同Consumer Group的偏移量都是保存在Topic为“consumer offsets”的日志里面的,每个具体的Consumer Group的偏移量保存在特定的分区里面,当客户端想要知道分配给某个具体的Consumer Group所在的特定分区状态时就会发送此请求,Broker Server收到此请求之后会把特定Consumer Group所在的分区的Leader Replica 索引返回回来。

以上十一类请求分类总结:

Producer 和Kafka 集群:Producer 需要利用ProducerRequest和TopicMetadataRequest来完成Topic元数据的查询、消息的发送。
Consumer 和Kafka 集群:Consumer 需要利用TopicMetadataRequest请求、FetchRequest请求、OfsetRequest 请求、OffsetCommitRequest 请求、OffsetFetchRequest请求和ConsumerMetadataRequest请求来完成Topic元数据的查询、消息的订阅、历史偏移量的查询、偏移量的提交、当前偏移量的查询。
KafkaController 状态为Leader的Broker和KafkaController 状态为Standby的Broker:KafkaController状态为Leader的Broker 需要利用LeaderAndlsrRequest 请求、Stop-ReplicaRequest 请求、UpdateMetadataRequest请求来完成对Topic的管理;Kafka-Controller 状态为Standby的Broker 需要利用BrokerControlledShutdownRequest请求来通知KafkaController状态为Leader的Broker自己的下线动作。
Broker和Broker之间:Broker相互之间需要利用FetchRequest请求来同步Topic分区的副本数据,这样才能使Topic分区各副本数据实时保持一致。

请求数据从生产者发送到服务端的流转过程


KafkaProducer线程创建ProducerRecord后,将其缓存进RecordAccumulator。
Sender线程从RecordAccumulator中获取缓存的消息,放入KafkaChannel.send字段中等待发送,同时放入InFlightRequests队
列中等待响应。之后,客户端会通过KSelector将请求发送出去。
在服务端,Processor线程使用KSelector读取请求并暂存到stageReceives队列中,KSelector.poll()方法结束后,请求被移转移到completeReceives队列中。之后,Processor将请求进行一些解析操作后,放入RequestChannel.requestQueue队列。Handler线程会从RequestChannel.requestQueue队列中取出请求进行处理,将处理之后生成的响应放入RequestChannel.responseQueue队列。
Processor线程从其对应的RequestChannel. responseQueue队列中取出响应并放入inflightResponses队列中缓存,当响应发送出去之后会将其从inflightResponse中删除。生产者读取响应的过程与服务端读取请求的过程类似,主要的区别是生产者需要对InFlightRequest中的请求进行确认

5kafka服务端如何读写消息数据,如何维护日志数据

Kafka使用日志文件的方式保存生产者发送的消息

日志文件结构

Kafka通过分段的方式将Log分为多个LogSegment,每个Topic 下有 Partition分区,Partition分区下有 LogSegment分段,
LogSegment是一个逻辑上的概念,一个LogSegment对应磁盘上的一个segment 日志文件和一个索引文件,存储在目录 /${topicName}-{$partitionid}/ 下,其中segment 日志文件用于记录消息,索引文件中保存了消息的索引,文件名以这个 segment 中最小的 offset 偏移量命名,文件扩展名是. log,同一个segment 分区中的消息是顺序写入的,这就避免了随机写入带来的性能问题,提高了写入的性能,每个 segment 文件大小相等(当日志分段累加的消息达到阀值大小(文件大小达到1GB)时,会新创建一个日志分段保存新的消息,而分区的消息总是追加到最新的日志分段中);
对应的索引的文件名字一样,扩展名是. index和.timeindex。 offset index ,用于按 offset 去查 message; time index,用于按照时间去查
总体的组织是这样的:

偏移量offset

Kafka 系统中存储的消息没有明确的消息 Id。消息通过日志中的逻辑偏移量offset来公开。这样就避免了维护配套密集寻址,用于映射消息 ID 到实际消息地址的随机存取索引结构的开销。消息 ID 是增量的,但不连续。要计算下一消息的 ID,可以在其逻辑偏移的基础上加上当前消息的长度。
每条消息都有一个offset值来表示它在分区中的偏移量,这个offset值是逻辑值,并不是消息实际存放的物理地址
偏移量是消息最重要的组成部分,每条消息写入底层数据文件,都会有一个递增的偏移量
每个LogSegment都有一个基准偏移量(segmentBaseoffset,或者叫baseoffset),这个基准偏移量是分区级别的绝对偏移量,而且这个值在LogSegment中是固定的。有了这个基准偏移量,就可以计算出每条消息在分区中的绝对偏移量,最后把消息以及对应的绝对偏移量写到日志文件中,每条消息的偏移量是分区级别的绝对偏移量。
而在索引文件中也是以稀疏索引方式(间隔一定数量的消息才保存一条映射关系,为了减少索引文件的大小,降低空间使用,方便直接加载进内存中)保存着相对偏移量(绝对偏移量减去日志分段的基准偏移量)为日志文件中的部分消息建立了索引。
查找 offset 对应的记录时,会先用二分法,找出对应的 offset 在哪个 segment 中,然后使用索引,在定位出 offset 在 segment 中的大概位置,再遍历查找 message。

日志读写相关对象和方法

FileMessageSet

用于管理日志文件,MessageSet具有顺序写入消息和顺序读取的特性
在FileMessageSet对象初始化的过程中,会移动FileChannel的position指针,这是为了实现每次写入的消息都在日志文件的尾部,从而避免重启服务后的写入操作覆盖之前的操作。对于新创建的且进行了预分配空间的日志文件,其end会初始化为0,所以也是从文件起始写入数据的
FileMessageSet.append()方法实现了写日志文件的功能,需要注意的是其参数必须是ByteBufferMessageSet对象
FileMessageSet.searchFor()方法实现了查找指定消息的功能,逻辑是:从指定的startingPosition开始逐条遍历FileMessageSet中的消息,并将每个消息的offset与targetOffset进行比较,直到offset大于等于targetOffset,最后返回查找到的offset。在整个遍历过程中不会将消息的key和value读取到内存,而是只读取LogOverhead(即offset和size)
FileMessageSet.writeTo()方法实现将FileMessageSet中的数据写入指定的其他Channel中(fileChannel.transferTo()方法零拷贝实现,减少了上下文切换和数据复制)
FileMessageSet.read*()方法是从FileMessageSet中读取数据,可以将FileMessageSet中的数据读入到别的ByteBuffer中返回,也可以按照指定位置和长度形成分片的FileMessageSet对象返回。
FileMessageSet.delete()方法是将整个日志文件删除。
FileMessageSet还有一个truncateTo()方法,主要负责将日志文件截断到targetSize大小

OffsetIndex

OffsetIndex对象对应管理磁盘上的一个索引文件
Kafka使用稀疏索引的方式构造消息的索引,它不保证每个消息在索引文件中都有对应的索引项,这算是磁盘空间、内存空间、查找时间等多方面的折中
append()方法,向索引文件中添加索引项的
truncateTo()方法和truncateToEntries()方法,将索引文件截断到某个位置
resize()方法,进行文件扩容
indexSlotFor()和lookup()方法,二分查找

LogSegment

OffsetIndex对象与FileMessageSet共同构成一个LogSegment对象
LogSegment.append()方法实现追加消息功能
LogSegment.read()方法实现读取消息功能
LogSegment.recover()方法实现了根据日志文件重建索引文件,同时验证日志文件中消息的合法性

Log

Log是对多个LogSegment对象的顺序组合,形成一个逻辑的日志(一个日志(Log)有多个日志分段(Logsegment)每个日志分段由数据文件(FileMessageset)和索引文件(offsetIndex)组成)
为了实现快速定位LogSegment,Log使用跳表(SkipList)对LogSegment进行管理
Log.append()方法,实现向Log追加消息的功能
Log.read()方法实,现读取消息的功能,实现的逻辑是:通过segments跳表,快速定位到读取的起始LogSegment并从中读取消息

LogManager

一个Broker上的所有Log都是由LogManager进行管理,
LogManager提供了加载Log、创建Log集合、删除Log集合、查询Log集合等功能,并且启动了3个周期性的后台任务以及Cleaner线程分别是:
log-flusher(日志刷写)任务,根据配置的时长定时对Log进行flush操作,保证数据的持久性
log-retention(日志保留)任务,按照两个条件进行LogSegment的清理工作:一是LogSegment的存活时长,二是整个Log的大小。log-retention任务不仅会将过期的LogSegment删除,还会根据Log的大小决定是否删除最旧的LogSegment,以控制整个Log的大小,周期性地调用LogManager.cleanupLogs()方法完成对符合条件的LogSegment的删除
recovery-point-checkpoint(检查点刷新)任务,定时将每个Log的recoveryPoint写入RecoveryPointCheckpoint文件中,周期性地调用LogManager.checkpointRecoveryPointOffsets()方法完成RecoveryPointCheckpoint文件的更新,RecoveryPointCheckpoint文件的更新操作是在OffsetCheckpoint中实现的,其更新方式是:先将log目录下的所有Log的recoveryPoint写到tmp文件中,然后用tmp文件替换原来的RecoveryPointCheckpoint文件文件
Cleaner线程(日志清理)Cleaner线程在选定需要清理的Log后,首先为dirty部分的消息建立key与其last_offset(此key出现的最大offset)的映射关系。然后重新复制LogSegment,只保留SkimpyOffsetMap中记录的消息,抛弃掉其他消息。经过日志压缩后,日志文件和索引文件会不断减小,Cleaner线程还会对相邻的LogSegment进行合并,避免出现过小的日志文件和索引文件。

高效日志文件读写原理

写入数据:
为了优化写入速度 Kafak 采用了两个技术,顺序写入和 MMFile。

顺序写入
同一个segment 分区中的消息是顺序写入的,收到消息后 Kafka 会把数据追加到文件末尾,这就避免了磁盘随机写入操作带来的性能问题,提高了写入的性能。
缺陷——没有办法删除数据,所以 Kafka 是不会删除数据的,它会把所有的数据都保留下来,每个消费者(Consumer)对每个 Topic 都有一个 offset 用来表示读取到了第几条数据。

Memory Mapped Files
即便是顺序写入硬盘,硬盘的访问速度还是不可能追上内存。所以 Kafka 的数据并不是实时的写入硬盘,它充分利用了现代操作系统分页存储来利用内存提高 I/O 效率。
Memory Mapped Files( 也被翻译成内存映射文件),在 64 位操作系统中一般可以表示 20G 的数据文件,它的工作原理是直接利用操作系统的 Page 来实现文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上(操作系统在适当的时候)。
使用这种方式可以获取很大的 I/O 提升,省去了用户空间到内核空间复制的开销(调用文件的 read 会把数据先放到内核空间的内存中,然后再复制到用户空间的内存中)
也有一个很明显的缺陷——不可靠,写到 mmap 中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用 flush 的时候才把数据真正的写到硬盘。Kafka 提供了一个参数——producer.type 来控制是不是主动 flush,如果 Kafka 写入到 mmap 之后就立即 flush 然后再返回 Producer 叫同步 (sync);写入 mmap 之后立即返回 Producer 不调用 flush 叫异步 (async)。
mmap 其实是 Linux 中的一个函数就是用来实现内存映射的,Java NIO提供了一个 mappedbytebuffer 类可以用来实现内存映射

读取数据:
读取优化主要包括两方面,批量读取和零拷贝优化

批量读取
Kafka 服务端为消费者提供Pull方式拉取数据,每次拉取你需要多少个,我给你多少个,只要网络允许,消费者想要更快,就增大批量大小

零拷贝Zero Copy
现代的 unix 操作系统提供直接用于将数据从页缓存传输到 socket的数据读取方法(在 Linux 中就是 sendfile API)
在Java 的 NIO 中提供了访问 sendfile 系统调用的方法 的API,FileChannel.transferFrom 和 FileChannel.transferTo

6kafka服务端如何保证高可用

KafkaController机制

Kafka集群的多个Broker中,有一个 Broker会被选举为Controller Leader,负责管理整个集群中所有的分区和副本的状态,重新选举新的副本Leader,重新分配Topic分区,通知集群中Broker更其MetadataCache信息
一个Broker被选为Leader之后,其他的Broker都会成为Follower(与副本的Leader/Follower是不同的)

KafkaController选举&高可用:
选举Controller Leader依赖于ZooKeeper实现,Zookeeper本身是一个分布式应用程序协调服务,每个Broker启动时都会创建一个KafkaController对象,但是集群中只能存在一个Controller Leader来对外提供服务
集群启动时,多个Broker上的KafkaController会在指定路径下竞争创建瞬时节点(Ephemeral Node),有且只有第一个成功创建节点的KafkaController才能成为Leader,而其余的KafkaController则成为Follower。当Leader出现故障后,所有的Follower会收到通知,再次竞争在该路径下创建节点从而选出新的Leader,这也是ZooKeeper的一种常见用法

所谓瞬间瞬时节点就是读写Zookeeper模块的客户端会维护和Zookeeper集群的连接,当该连接断开的时候,通过此连接之前创建的瞬时节点都会消失,所以说该节点是瞬时的,不是永久存在的。接着当Leader状态的KafkaController离线的时候,其内部的Zookeeper客户端就会失去和Zookeeper集群的连接,那么此时其他KafkaController观察到数据节点消失之后,就会重新尝试创建节点。


Zookeeper上保存了Kafka集群的元数据信息,KafkaController通过在不同目录注册不同的回调函数来达到监测集群状态的目的,及时响应集群状态的变化,ZooKeeper中与KafkaController相关的路径以及该路径中记录的内容的含义:
/brokers/ids/[id]:记录了集群中可用Broker的id,通过监测这个目录的变化可以及时响应Broker的上下线情况等。
/brokers/topics/[topic]/partitions:记录了一个Topic中所有分区的分配信息以及AR集合信息,通过监测这个目录的变化可以及时响应Topic创建和删除的请求;
/brokers/topics/[topic]/partitions/[partition_id]/state:记录了某Partition的Leader副本所在BrokerId、lead_epoch、ISR集合、ZKVersion等信息。
/controller_epoch:记录了当前Controller Leader的年代信息。
/controller:记录了当前Controller Leader的Id,也用于Controller Leader的选举
/admin/reassign_partitions:记录了需要进行副本重新分配的分区,通过监测这个目录的变化可以及时响应Topic分区变化的请求;
/admin/preferred_replica_election:记录了需要进行“优先副本”选举的分区。“优先副本”是在创建分区时为其指定的第一个副本,通过监测这个目录的变化可以及时响应Topic分区副本变化的请求;
/admin/delete_topics:记录了待删除的Topic。
/isr_change_notification:记录了一段时间内ISR集合发生变化的分区。
/config:记录了一些配置信息


如上图KafkaController组织并封装了其他组件,对外提供API接口:
ZookeeperLeaderElector,主要用于Controller Leader的选举
ControllerContext,KafkaController的上下文信息,缓存了ZooKeeper中记录的整个集群的元信息,例如可用Broker、全部的Topic、分区、副本的信息等。
ControllerChannelManager,维护了Controller Leader与集群中其他Broker之间的网络连接,是管理整个集群的基础
TopicDeletionManager,用于对指定的Topic进行删除
PartitionStateMachine,用于管理集群中所有Partition状态的状态机
ReplicaStateMachine,用于管理集群中所有副本状态的状态机
ControllerBrokerRequestBatch,实现了向Broker批量发送请求的功能
*PartitionLeaderSelector,实现了多种Leader副本选举策略。
*Listener,是ZooKeeper上的监听器,实现了对ZooKeeper上某些节点中的数据、子节点或ZooKeeperSession状态的监听,被触发后调用相应的业务逻辑

KafkaController相关功能主要对象和方法说明:

ControllerChannelManager

用于管理其与集群中各个Broker之间的网络交互
ControllerChannelManager中使用ControllerBrokerStateInfo类表示与一个Broker连接的各种信息
ControllerChannelManager的核心字段是brokerStateInfo(HashMap[Int, ControllerBrokerStateInfo]类型),用于管理集群中每个Broker对应的ControllerBrokerStateInfo对象
ControllerChannelManager.addNewBroker()方法和removeBroker()方法实现了对brokerStateInfo 集合的管理,sendRequest()方法向指定Broker发送请求

ControllerContext

ControllerContext中维护了Controller使用到的上下文信息,可以将ControllerContext看作ZooKeeper数据的缓存
ControllerBrokerRequestBatch
ControllerBrokerRequestBatch组件用于实现Controller Leader向集群中其他Broker批量发送请求的功能

状态机

KafkaController中有维护分区状态和维护副本状态两个状态机
状态机一般用在事件处理中,并且事件会有多种状态,当事件的状态发生变化时,会触发对应的事件处理动作
状态机加载顺序:
因为分区状态机和副本状态机需要分别获取集群的所有分区和所有副本,而初始化控制器上下文会从ZK读取集群的所有分区与副本,所以初始化控制器上下文后,才能启动状态机。
因为分区包含了多个副本,只有集群中所有副本的状态都初始化完毕,才可以初始化分区的状态。所以控制器会先启动副本状态机,然后才启动分区状态机。

PartitionStateMachine

PartitionStateMachine是Controller Leader用于维护分区状态的状态机:
NonExistentPartition 分区从来没有被创建或是分区被创建之后被又删除掉了,这两种场景下的分区都处于此状态
NewPartition 分区被创建后就处于此状态。此时分区可能已经被分配了AR集合,但是还没有指定Leader副本和ISR集合
OfflinePartition 已经成功选举出分区的Leader副本后,但Leader副本发生宕机,则分区转换为此状态。或者,新创建的分区直接转换为此状态
OnlinePartition 分区成功选举出Leader副本之后,分区会转换为此状态
PartitionStateMachine.handleStateChange()方法是管理分区状态的核心方法,该方法控制着PartitionState的转换
PartitionLeaderSelector
PartitionLeaderSelector中实现Leader副本选举、确定ISR集合等功能
具体有多种不同策略实现NoOpLeaderSelector,ReassignedPartitionLeaderSelector,PreferredReplicaPartitionLeaderSelector,ControlledShutdownLeaderSelector,offlinePartitionLeaderSelector

PartitionStateMachine 在路径为/broker/topics的Zookeeper 上注册了TopicChangeListener监听器,该监听器主要用来监听Topic的创建,当监听到有新的Topic创建的时候,会触发TopicChangeListener 内部的 handleChildChange回调函数PartitionStateMachine在路径为/admin/delete_topics的Zookeeper上注册了Delete TopicsListener监听器,该监听器主要用来监听Topic的删除,当监听到有新的Topic删除的时候,会触发Delete TopicsListener内部的handleChildChange回调函数

ReplicaStateMachine

ReplicaStateMachine是Controller Leader用于维护副本状态的状态机:
NewReplica 创建新Topic或进行副本重新分配时,新创建的副本就处于这个状态。处于此状态的副本只能成为Follower副本
OnlineReplica 副本开始正常工作时处于此状态,处在此状态的副本可以成为Leader副本,也可以成为Follower副本
OfflineReplica 副本所在的Broker下线后,会转换为此状态
ReplicaDeletionStarted 刚开始删除副本时,会先将副本转换为此状态,然后开始删除操作
ReplicaDeletionSuccessful 副本被成功删除后,副本状态会处于此状态
ReplicaDeletionIneligible 如果副本删除操作失败,会将副本转换为此状态
NonExistentReplica 副本被成功删除后最终转换为此状态
ReplicaStateMachine的核心方法是handleStateChange()方法,其中控制着ReplicaState的转换

ZooKeeper Listener

KafkaController会通过ZooKeeper监控整个Kafka集群的运行状态,响应管理员指定的相关操作
Listener按照接口的类型可以分为三类:
IZkDataListener 监听指定节点的数据变化
IZkChildListener 监听指定节点的子节点变化
IZkStateListener 监听ZooKeeper连接状态的变化
五个IZkDataListener接口的实现:
LeaderChangeListener 监听“/controller”节点中的数据变化,当“/controller”节点中的数据被删除时会触发handleDataDeleted()方法
PartitionModificationsListener 监听“/brokers/topics/[topic_name]”节点中的数据变化,主要用于监听一个Topic的分区变化
PreferredReplicaElectionListener 监听的ZooKeeper节点是“/admin/preferred_replica_election”,通过PreferredReplicaLeaderElectionCommand命令指定某些分区需要进行“优先副本”选举时会将指定分区的信息写入该节点,从而触发PreferredReplicaElectionListener进行处理,让Leader副本在整个集群中分布得更加均衡
PartitionsReassignedListener 监听的ZooKeeper节点是“/admin/reassign_partitions”,当通过ReassignPartitionsCommand命令指定某些分区需要重新分配副本时,会将指定分区的信息写入该节点,从而触发PartitionsReassignedListener进行处理
ReassignedPartitionsIsrChangeListener 监听的ZooKeeper“/broker/topics/[topic_name]/partitions/[partitionId]/state”节点上监听其数据变化,主要负责处理进行副本重新分配的分区的ISR集合变化
四个IZkChildListener接口的实现:
TopicChangeListener 负责管理Topic的增删,它监听“/brokers/topics”节点的子节点变化
DeleteTopicsListener 监听ZooKeeper中“/admin/delete_topics”节点下的子节点变化,当TopicCommand在该路径下添加需要被删除的Topic时会被触发将该待删除的Topic交由TopicDeletionManager执行Topic删除操作
IsrChangeNotificationListener IsrChangeNotificationListener用于监听此“/isr_change_notification”路径下的子节点变化,当某些分区的ISR集合变化时通知整个集群中的所有Broker
BrokerChangeListener 是ReplicaStateMachine中的ZooKeeper Listener,它会监听“/brokers/ids”节点下的子节点变化,主要负责处理Broker的上线和故障下线。当Broker上线时会在“/brokers/ids”下创建临时节
点,下线时会删除对应的临时节点
一个IZkStateListener接口的实现:
SessionExpirationListener监听KafkaController与ZooKeeper的连接状态。当KafkaController与ZooKeeper的连接超时后创建新连接时会触发SessionExpirationListener.handleNewSession()方法
Partition Rebalance
在KafkaController.onControllerFailover()方法中会启动一个名为“partition-rebalance”的周期性的定时任务,它提供了分区的自动均衡功能。该定时任务会周期性地调用
KafkaController.checkAndTriggerPartitionRebalance()方法对失衡的Broker上相关的分区进行“优先副本”选举,使得相关分区的“优先副本”重新成为Leader副本,整个集群中Leader副本的分布也会重新恢复平衡

副本机制

副本(Replica)机制保证了Kafka集群的高可用性
每个分区可以有多个副本,并且会从其副本集合(Assigned Replica,AR)中选出一个副本作为Leader副本,所有的读写请求都由选举出的Leader副本处理。
剩余的其他副本都作为Follower副本,Follower副本会从Leader副本处获取消息并更新到自己的Log中。
同一分区的多个副本会被均匀地分配到集群中的不同Broker上,当Leader副本的所在的Broker出现故障后,可以重新选举新的Leader副本继续对外提供服务
Leader副本中会维护自身以及所有Follower副本的相关状态
核心机制:
KafkaController在 Zookeeper 的 /brokers/ids 节点上注册 Watch,一旦有 broker 宕机,它就能知道。当 broker 宕机后,KafkaController就会给受到影响的 partition 选出新 leader。
KafkaController从 Zookeeper 的 / brokers/topics/[topic]/partitions/[partition]/state 中,读取对应 partition 的 ISR(in-sync replica 已同步的副本)列表,选一个出来做 leader。
选出 leader 后,更新 Zookeeper ,然后发送 LeaderAndISRRequest 通知受影响的 broker。
同步规则:
数据同步的可靠性是通过生产者 消息中的request.required.acks 参数来确定的:
acks=0  发过去就完事了,不关心 broker 是否处理成功,可能丢数据。
acks=1  当写 Leader 成功后就返回, 其他的 replica 都是通过 fetcher 去同步的, 所以 kafka 是异步写,主备切换可能丢数据。
acks=-1 要等到 ISR集合中里所有机器同步成功,才能返回成功,延时取决于最慢的机器(可以保证不丢数据)

副本机制主要对象和方法:

Replica
表示一个分区的副本对象,包含brokerId,HighWatermark,LogOffsetMetadata对象,Partition对象,Log对象等信息
Partition
表示分区,Partition负责管理每个副本对应的Replica对象,进行Leader副本的切换,ISR集合的管理以及调用日志存储子系统完成写入消息
核心字段:
topic和partitionId:此Partition对象代表的Topic名称和分区编号
localBrokerId:当前Broker的id,可以与replicaId比较,从而判断指定的Replica对是否表示本地副本
logManager:当前Broker上的LogManager对象
zkUtils:操作ZooKeeper的辅助类
leaderEpoch:Leader副本的年代信息。
leaderReplicaIdOpt:该分区的Leader副本的id
inSyncReplicas:Set[Replica]类型,该集合维护了该分区的ISR集合,ISR集合是AR集合的子集
assignedReplicaMap:Pool[Int, Replica]类型,维护了该分区的全部副本的集合(AR集合)的信息
主要方法:
getOrCreateReplica()方法,获取(或创建)Replica
makeLeader(),makeFollower()方法,副本的Leader/Follower角色切换(由KafkaController发送LeaderAndISRRequest请求控制副本的Leader/Follower角色切换)
maybeExpandIsr()方法,扩张ISR集合,处理来自Follower的FetchRequest时触发
maybeShrinkIsr()方法,缩减集合,ReplicaManager周期调用
appendMessagesToLeader()方法,调用日志存储子系统完成消息写入(向Leader副本对应的Log中追加消息的功能)
checkEnoughReplicasReachOffset()方法,检测HW的位置
delete()方法,删除对应分区在此Broker上的Log文件,同时也会清空其ISR、AR等集合

ReplicaManager
一个Broker上可能分布着多个Partition的副本信息,ReplicaManager的主要功能是管理一个Broker范围内的Partition信息
ReplicaManager的实现依赖于前面介绍的日志存储子系统、DelayedOperationPurgatory、KafkaScheduler等组件,底层依赖于Partition和Replica
副本角色切换通过PartitionReplicaManager.becomeLeaderOrFollower()方法实现,主要逻辑是:获取(或创建)指定的Partition对象,根据partitionStates的信息对其切换成Leader/Follower的副本进行分类,并分别调用makeLeader()和makeFollowers()方法完成切换
ReplicaManager.appendMessages()方法,实现消息写入,具体实现为appendToLocalLog()
ReplicaManager.fetchMessages()方法,完成消息读取,具体实现为readFromLocalLog()
updateFollowerLogReadResults()方法,实现Follower副本与Leader副本同步(数据,状态)

ReplicaManager继承AbstractFetcherManager抽象类实现Follower副本同步相关功能包括:
ReplicaManager.addFetcherForPartitions()方法会让Follower副本从指定的offset开始与Leader副本进行同步
ReplicaManager.removeFetcherForPartitions()方法会停止指定Follower副本的同步操作
ReplicaManager.stopReplicas()方法用于关闭副本

ReplicaManager中有三个定时任务
highwatermark-checkpoint任务会周期性地记录每个Replica的HW并保存到其log目录中的replication-offset-checkpoint文件中
isr-expiration任务会周期性地调用maybeShrinkIsr()方法检测每个分区是否需要缩减其ISR集合
isr-change-propagation任务会周期性地将ISR集合发生变化的分区记录到ZooKeeper中

MetadataCache
MetadataCache是Broker用来缓存整个集群中全部分区状态的组件
KafkaController通过向集群中的Broker发送UpdateMetadataRequest来更新其MetadataCache中缓存的数据,每个Broker在收到该请求后会异步更新MetadataCache中的数据

7kafka消费者客户端拉取消息

消费者主要做的事情是是拉取消息,同时还需要提交拉取消息的进度offset(周期性提交至“__consumer_offsets”的内部Topic)以及和服务端一同完成消费者组Consumer Group Rebalance(心跳检测,rebalence策略现实等)
副本有两个重要的位置信息:LEO表示副本的最新偏移量,HW表示副本的最高水位
消费者拉取的最大上限通过最高水位(watermark)控制,生产者最新写入的消息如果还没有达到备份数量,对消费者是不可见的。
消费者用Pull模式因为kafka的堆积能力强,消费者可以按自己的消费能力Pull

消费者API的核心是消息轮询,通过一个简单的轮询向服务器请求数据。一旦消费者订阅了主题,轮询就会处理所有的细节,包括群组协调、分区再均衡、发送心跳和获取数据,开发者只需要使用一组简单的API来处理从分区返回的数据。KafkaConsumer依赖SubscriptionState管理订阅的Topic集合和Partition的消费状态,通过ConsumerCoordinator与服务端的GroupCoordinator交互,完成Rebalance操作并请求最近提交的offset
Fetcher负责从Kafka中拉取消息并进行解析,同时参与position的重置操作,提供获取指定Topic的集群元数据的操作。
上述操作的所有请求都是通过ConsumerNetworkClient缓存并发送的,在ConsumerNetworkClient中还维护了定时任务队列,用来完成HearbeatTask任务和AutoCommitTask任务。NetworkClient在接收到上述请求的响应时会调用相应回调,最终交给其对应的*Handler以及RequestFuture的监听器进行处理。

Consumer接口中定义KafkaConsumer对外的API:

subscribe()方法:订阅指定的Topic,并为消费者自动分配分区。
assign()方法:用户手动订阅指定的Topic,并且指定消费的分区。此方法与subscribe()方法互斥
commit*()方法:提交消费者已经消费完成的offset
seek*()方法:指定消费者起始消费的位置
poll()方法:负责从服务端获取消息
pause()、resume()方法:暂停/继续Consumer,暂停后poll()方法会返回空

一些关键对象说明:

ConsumerNetworkClient

ConsumerNetworkClient在NetworkClient之上进行了封装,提供了更高级的功能和更易用的API
重要属性:
client:NetworkClient对象
delayedTasks:定时心跳任务任务队列,DelayedTaskQueue是Kafka提供的定时任务队列的实现,其底层是使用JDK提供的PriorityQueue实现(非线程安全的、无界的、优先级队列,实现原理是小顶堆,底层是基于数组实现的)
metadata:用于管理Kafka集群元数据
unsent:缓冲队列,Map<Node,List<ClientRequest>>类型,key是Node节点,value是发往此Node的ClientRequest集合
unsentExpiryMs:ClientRequest在unsent中缓存的超时时长
wakeup:由调用KafkaConsumer对象的消费者线程之外的其他线程设置,表示要中断KafkaConsumer线程
wakeupDisabledCount:KafkaConsumer是否正在执行不可中断的方法。每进入一个不可中断的方法时,则增加一,退出不可中断方法时,则减少一。wakeupDisabledCount只会被KafkaConsumer线程修改,其他线程不能修改
核心方法:
ConsumerNetworkClient.poll()方法:ConsumerNetworkClient中最核心的方法,调用NetworkClient.poll()实现网络交互
awaitMetadataUpdate()方法:循环调用poll()方法,直到Metadata版本号增加,实现阻塞等待Metadata更新完成。
awaitPendingRequests()方法:等待unsent和InFightRequests中的请求全部完成(正常收到响应或出现异常)。
put()方法:向unsent中添加请求。
schedule()方法:向delayedTasks队列中添加定时任务。
leastLoadedNode()方法:查找Kafka集群中负载最低的Node

SubscriptionState

Consumer使用SubscriptionState来追踪TopicPartition与offset对应关系
SubscriptionState包含SubscriptionType和TopicPartitionState
SubscriptionType是SubscriptionState的一个内部枚举类型,表示的是订阅Topic的模式
分为四类:
NONE:SubscriptionState.subscriptionType的初始值。
AUTO_TOPICS:按照指定的Topic名字进行订阅,自动分配分区。
AUTO_PATTERN:按照指定的正则表达式匹配Topic进行订阅,自动分配分区。
USER_ASSIGNED:用户手动指定消费者消费的Topic以及分区编号。
TopicPartitionState表示的是TopicPartition的消费状态
其关键字段:
position:记录了下次要从Kafka服务端获取的消息的offset。
committed:记录了最近一次提交的offset。
paused:记录了当前TopicPartition是否处于暂停状态,与Consumer接口的pause()方法相关。
resetStrategy:OffsetResetStrategy枚举类型,重置position的策略。同时,此字段是否为空,也表示了是否需要重置position的值

消息拉取

Fetcher类的主要功能是发送FetchRequest请求,从服务端获取指定的消息集合
Fetcher的核心方法可以分为三类:
fetch消息的相关方法:用于从Kafka获取消息 createFetchRequests() sendFetches()f etchedRecords()
更新offset相关的方法:用于更新TopicPartitionState中的position字段 Fetcher.updateFetchPositions()
获取Metadata信息的方法:用于获取指定Topic的元信息 sendMetadataRequest()、getTopicMetadata()、getAllTopicMetadata()

提交拉取消息的进度

涉及到传递保证语义 Delivery guarantee semantic,不同时机提交offset会导致不同的传递保证
At most once:最多一次,消息可能会丢,但绝不会重复传递
At least once:最少一次,消息绝不会丢,但可能会重复传递
Exactly once:每条消息只会被传递一次,消息不丢失不重复
在业务中,常常都是使用 At least once 的模型,如果需要可重入的话,往往是业务自己实现。
At least once
消费者先获取数据,再进行业务处理,业务处理成功后 commit offset
1、生产者生产消息异常,消息是否成功写入不确定,重做,可能写入重复的消息
2、消费者处理消息,业务处理成功后,更新 offset 失败,消费者重启的话,会重复消费
At most once
消费者先获取数据,再 commit offset,最后进行业务处理。
1、生产者生产消息异常,不管,生产下一个消息,消息就丢了
2、消费者处理消息,先更新 offset,再做业务处理,做业务处理失败,消费者重启,消息就丢了
Exactly once
生产者重做导致重复写入消息 ---- 生产保证幂等性
消费者重复消费 --- 消灭重复消费,或者业务接口保证幂等性重复消费
由于业务接口是否幂等,不是 kafka 能保证的,所以 kafka 这里提供的 exactly once 是有限制的,消费者的下游也必须是 kafka。所以一下讨论的,没特殊说明,消费者的下游系统都是 kafka(注: 使用 kafka conector,它对部分系统做了适配,也实现 exactly once)
解决重复消费有两个方法:
1,下游系统保证幂等性,重复消费也不会导致多条记录,把 commit offset 和业务处理绑定成一个事务(我们的数据源可能是多个 topic,处理后输出到多个 topic,这时我们会希望输出时要么全部成功,要么全部失败。这就需要实现事务性。既然要做事务,那么干脆把重复消费的问题从根源上解决,把 commit offset 和输出到其他 topic 绑定成一个事务。),
2,为每个 producer 分配一个 pid,作为该 producer 的唯一标识。producer 会为每一个 <topic,partition> 维护一个单调递增的 seq。类似的,broker 也会为每个 < pid,topic,partition > 记录下最新的 seq。当 req_seq == broker_seq+1 时,broker 才会接受该消息。因为:消息的 seq 比 broker 的 seq 大超过时,说明中间有数据还没写入,即乱序了。消息的 seq 不比 broker 的 seq 小,那么说明该消息已被保存。

事务性 / 原子性广播

场景是这样的:
先从多个源 topic 中获取数据。
做业务处理,写到下游的多个目的 topic。
更新多个源 topic 的 offset。
其中第 2、3 点作为一个事务,要么全成功,要么全失败。这里得益与 offset 实际上是用特殊的 topic 去保存,这两点都归一为写多个 topic 的事务性处理。
基本思路是这样的:
引入 tid(transaction id),和 pid 不同,这个 id 是应用程序提供的,用于标识事务,和 producer 是谁并没关系。就是任何 producer 都可以使用这个 tid 去做事务,这样进行到一半就死掉的事务,可以由另一个 producer 去恢复。
同时为了记录事务的状态,类似对 offset 的处理,引入 transaction coordinator 用于记录 transaction log。在集群中会有多个 transaction coordinator,每个 tid 对应唯一一个 transaction coordinator。
注:transaction log 删除策略是 compact,已完成的事务会标记成 null,compact 后不保留。
做事务时,先标记开启事务,写入数据,全部成功就在 transaction log 中记录为 prepare commit 状态,否则写入 prepare abort 的状态。之后再去给每个相关的 partition 写入一条 marker(commit 或者 abort)消息,标记这个事务的 message 可以被读取或已经废弃。成功后在 transaction log 记录下 commit/abort 状态,至此事务结束。

在SubscriptionState中使用TopicPartitionState记录了每个TopicPartition的消费状况,TopicPartitionState.position字段则记录了消费者下次要从服务端获取的消息的offset。当没有明确指定待提交的offset值时,则将TopicPartitionState.position作为待提交offset
ConsumerCoordinator实现了offset提交和拉取相关方法
KafkaConsumer.commitOffsetsAsync()
KafkaConsumer.commitOffsetsSync()
refreshCommittedOffsetsIfNeeded()

Consumer Group Rebalance


通过GroupCoordinator(KafkaServer中用于管理Consumer Group的组件)以及消费者端的Group Leader以及两阶段(Join Group阶段和Synchronizing Group State阶段)式协议事项
步骤:
1,消费者通过发送ConsumerMetadataRequest(包含其Consumer Group的GroupId)到服务端Broker节点,收到请求的Broker会返回ConsumerMetadataResponse作为响应,其中
包含了管理此Consumer Group的GroupCoordinator的ConsumerMetadataResponse信息,根据ConsumerMetadataResponse中的GroupCoordinator信息,连接到GroupCoordinator并周
期性地发送HeartbeatRequest,表示有效在线
2,消费者在加入或退出Consumer Group,取消订阅,下线或Topic出现分区数量的变化,会GroupCoordinator开始Rebalance操作
3,消费者接受到的如果HeartbeatResponse中带有IllegalGeneration异常,说明GroupCoordinator发起了Rebalance操作,此时消费者发送JoinGroupRequest给服务端GroupCoordinator,通知GroupCoordinator,当前消费者要加入指定的Consumer Group。之后,服务端的GroupCoordinator收到JoinGroupRequest后会暂存消息,收集到全部消费者之后,根据JoinGroupRequest中的信息来确定Consumer Group中可用的消费者,从中选取一个消费者成为Group Leader,还会选取使用的分区分配策略,最后将这些信息封装成JoinGroupResponse返回给消费者
4,每个消费者都会收到JoinGroupResponse,但是只有Group Leader收到的JoinGroupResponse中封装了所有消费者的信息。当消费者确定自己是Group Leader后,会根据消费者的信息以及选定的分区分配策略进
行分区分配
5,接下来进入Synchronizing Group State阶段,每个消费者会发送SyncGroupRequest到GroupCoordinator,但是只有Group Leader的SyncGroupRequest请求包含了分区的分配结果,GroupCoordinator根据Group Leader的分区分配结果,形成SyncGroupResponse返回给所有Consumer,消费者就可以根据SyncGroupResponse中分配的分区开始消费数据。
5,消费者成功成为Consumer Group的成员后,又会会周期性发送HeartbeatRequest。如果HeartbeatResponse包含IllegalGeneration异常,则执行步骤3。
如果找不到对应的GroupCoordinator(HeartbeatResponse包含NotCoordinatorForGroup异常),则周期性地执行步骤1,直至成功。

服务端负载均衡以及消息进度保存相关对象和方法:

GroupCoordinator

在每一个Broker上都会实例化一个GroupCoordinator对象,Kafka按照Consumer Group的名称将其分配给对应的GroupCoordinator进行管理;每个GroupCoordinator只负责管理Consumer Group的一个子集
GroupCoordinator有几项比较重要的功能:
一 负责处理JoinGroupRequest和SyncGroupRequest完成Consumer Group中分区的分配工作;
二 通过GroupMetadataManager和内部Topic“Offsets Topic”维护offset信息,即使出现消费者宕机也可以找回之前提交的offset;
三 记录Consumer Group的相关信息,即使Broker宕机导致Consumer Group由新的GroupCoordinator进行管理,新GroupCoordinator也可以知道Consumer Group中每个消费者负责处理哪个分区等信息;
四 通过心跳消息检测消费者的状态
GroupCoordinator中使用MemberMetadata记录消费者的元数据,GroupMetadata记录了Consumer Group的元数据信息
GroupCoordinator使用GroupTopicPartition维护Consumer Group与分区的消费关系,使用OffsetAndMetadata记录offset的相关信息
GroupMetadataManager
GroupMetadataManager是GroupCoordinator中负责管理Consumer Group元数据以及其对应offset信息的组件。GroupMetadataManager底层使用Offsets Topic,以消息的形式存储Consumer Group的GroupMetadata信息以及其消费的每个分区的offset
GroupMetadataManager同会将Consumer Group的GroupMetadata信息和offset信息在内存中维护一份相同的副本,并进行同步修改
GroupCoordinator
GroupCoordinator为其管理的每个Consumer Group都维护了一个状态机:
PreparingRebalance  Consumer Group当前正在准备进行Rebalance操作
AwaitingSync             Consumer Group当前正在等待Group Leader将分区的分配结果发送到GroupCoordinator
Stable                         标识Consumer Group处于正常状态,这也是Consumer Group的初始状态
Dead                           处于此状态的Consumer Group中已经没有Member存在了
GroupCoordinator.handleHeartbeat()方法首先会进行一系列的检测,保证GroupMetadataManager处于可用状态且是对应Consumer Group的管理者。之后检测Consumer Group状态、MemberId、generationId是否合法。最后,调用completeAndScheduleNext-HeartbeatExpiration()方法,在completeAndScheduleNextHeartbeatExpiration()方法中会更新收到此Member心跳的时间戳,尝试执行其对应的DelayedHeartbeat,并创建新的DelayedHeartbeat对象放入heartbeatPurgatory中等待下次心跳到来或DelayedHeartbeat超时
GroupCoordinator.handleJoinGroup()方法处理JoinGroupRequest,检测,调用GroupCoordinator.doJoinGroup(),调用addMemberAndRebalance()和updateMemberAndRebalance()添加/更新Member信息,并调用prepareRebalance()方法实现GroupMetadata的状态切换
GroupCoordinator.handleSyncGroup()方法处理SyncGroupRequest,检测,调用GroupCoordinator. doSyncGroup()方法,保存分配结果到对应的Offsets Topic分区中,并修改GroupMetadata中的相关缓存记录
GroupCoordinator.handleCommitOffsets()方法处理OffsetCommitRequest,权限验证,过滤掉未知Topic对应的offset信息,调用GroupCoordinator.handleCommitOffsets()方法完成offset保存(通过调用OffsetManager实现)
GroupCoordinator.handleLeaveGroup()方法处理LeaveGroupRequest,将Consumer Group状态转换成Dead,并根据之前的Consumer Group状态进行相应的清理操作

笔记来源相关书目及文档:
官方文档http://kafka.apachecn.org/documentation.html
《Apache Kafka源码剖析》https://book.douban.com/subject/27038473/ 
《Kafka技术内幕:图文详解Kafka源码设计与实现》Kafka技术内幕 (豆瓣)
《Kafka源码解析与实战》Kafka源码解析与实战 (豆瓣)
《Kafka权威指南》Kafka权威指南 (豆瓣)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值