Kafka学习笔记总结

目录

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.ma

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值