Kafka入门
1、Kafka是什么
官网给的解释是:Apache Kafka is an open-source distributed event streaming platform used by thousands of companies for high-performance data pipelines, streaming analytics, data integration, and mission-critical applications.
百度翻译是:Apache Kafka是一个开源分布式事件流平台,被数千家公司用于高性能数据管道、流分析、数据集成和关键任务应用程序。
流处理平台是专门处理无限数据集(即数据无穷无尽)的数据处理系统或者框架,跟流处理对应的就是批处理。流处理属于延迟低,但是数据不太精确,批处理属于数据精确但是可能存在高延迟,类似于TCP和UDP。
2、Kafka适合什么场景
- 日志收集和分析:收集来自各种系统或应用程序的日志数据,并进行实时处理和分析,适合作为高性能的日志收集和分析平台。
- 实时数据流处理:可以处理各种实时的数据流,比如网站用户的浏览、搜索、点击,通过相应服务器发布到kafka中进行相应的数据分析。
- 消息队列和事件驱动架构:可以作为消息队列和事件驱动架构的核心组件,实现系统或服务之间的异步通信和解耦,作为消息队列,主要作用还是异步、解耦、削峰。
- 分布式系统的监控:可以接收分布式系统的监控数据,然后进行实时分析和处理。
3、Kafka架构的基本概念
- 分区:对Topic进行分区,是为了避免单个文件的大小限制,以此来保证处理无限量的数据。每个分区也会受限于每个节点的限制。一个Topic包含很多分区,因此无法保证整个Topic中消息的顺序,但是单个分区里面消息是有序的。
- Broker Controller:Kafka集群的多个broker中,会有一个被选举为controller,负责管理集群中partition和副本replicas的状态。
- Zookeeper:zk的作用负责维护和协调Broker,负责Broker Controller的选举;Broker Controller是由Zookeeper选举出来的,Partition Leader是由Broker Controller选举出来的。
- 分区的所有副本为 AR(Assigned Replicas),所有与 leader 副本保持一定同步的副本为ISR(In-Sync Replicas),与 leader 同步滞后过多的副本为 OSR(Out-of-Sync Replicas)。
4、Kafka架构图
5、Kafka Producer发布消息实现原理
5.1生产者架构
- 主线程Producer中会经过拦截器、序列化器、分区器,然后将最终的消息发送到RecordAccumulator消息累加器中。
- RecordAccumulator每个分区会对应一个队列,在收到消息后,将消息放到队列中。
- 使用ProducerBatch批量的进行消息发送到Sender线程处理(为了提高发送效率,减少带宽),ProducerBatch中就是最终发送的消息,其中RecordAccumulator的大小可以使用buffer.memory配置,默认为32MB。(当Producer发送的消息数量达到batch.size(默认16KB)或者等待时间超过linger.ms(默认0)时,消息就会发送到Sender线程)
- Sender线程会从队列中读取消息,然后创建Request并缓存请求,然后提交到Selector,Selector发送消息到Kafka集群。(这其中会把<分区,Deque<ProducerBatch>>的形式转变成<Node,List<ProducerBatch>的形式)。
- 对于一些还没收到Kafka集群ACK响应的消息,会将未响应接收消息的请求进行缓存,当收到Kafka集群ACK响应后,会将Request请求在缓存中清除并同时移除RecordAccumulator中的消息。(这个缓存的请求的大小可以进行配置,默认为5个,配置参数为max.in.flight.requests.per.connection)
5.2 写入方式
producer采用 push 模式将消息发布到 broker,每条消息都被 追加 到 分区(patition) 中(顺序写,顺序写效率比随机写效率要高,保障 kafka 的高吞吐量)。
5.3 消息路由算法
消息路由的指的是找到对应的分区patition。主要路由机制为:
- 指定了 patition,则直接写入指定的partition
- 如果没有指定partition,但指定了key,则通过key的hash值与partition数量进行取模,取模结果就是partition的索引。
- patition 和 key 都未指定,使用轮询选出一个 patition。
5.4 消息写入Broker的流程
- Producer向Broker集群请求连接,其访问的任意一个broker都会向其发送Broker Controller的通信URL。
- 当Producer指定了要生产消息的Topic后,其会向Broker Controller发送请求,请求获取当前Topic的所有Partition Leader。
- Broker Controller在接收到请求后,会从Zookeeper服务器中查找指定Topic的所有Partition Leader,并将Partition Leader所在的Broker地址返回给Producer。
- Producer根据路由策略找到对应的Partition Leader,将消息发送该Partition Leader。
- Leader将消息写入log,并通知ISR中的Followers。
- ISR中的Follower从Leader中同步消息后向Leader发送ACK消息。
- Leader收到了所有ISR中的Follower的ACK后,增加HW,表示消费者可以消费到该位置;如果Leader在等待的Follower的ACK超时了,发现还有Follower没有发送ACK,则会将这些没有发送ACK的Follower从ISR中剔除,然后再增加HW。
5.5 生产者发送消息确认机制(ACK)详解
先了解几个概念,ISR、HW和LEO。ISR上面已经解释过了,这里不重复说明了。
HW全称HighWatermark,高水位。表示Consumer可以消费到的最高Partition偏移量。在Broker集群正常运转的情况下,HW保证了Partition的Follower和Leader之间数据的一致性。
LEO全程Log End Offset,日志中最后一条消息在partition中的偏移量。
对于Leader新写入的消息,Consumer不能立刻消费,Leader会等待该消息被所有ISR中的Replicas同步后更新HW, 此时消息才能被Consumer消费。这样就保证了如果Leader挂掉了,该消息仍然可以从新选举的Leader中获取。
ACK = all或者-1时的工作流程:
总结:这种方式保证了数据不丢失,但是会影响性能。
可以通过min.insync.replicas参数配置ISR中最小副本数。默认值为1,保证了至少一个副本数据完整性。
ACK = 1时的工作流程:
总结:这种机制会等到本地数据落盘后就会回复客户端成功,会引起数据丢失,但保证了分区broker和follower的数据一致性。
ACK = 0的情况就更简单了,这种机制不等待本地数据落盘就会直接回复客户端成功,也会导致数据丢失。
6、Kafka Broker的内部原理
6.1 核心总控制器Controller
在Kafka集群中会有一个或者多个broker,其中有一个broker会被选举为控制器(Controller),它负责管理整个集群中所有分区和副本的状态,包含副本 leader 的选举、topic 的创建和删除、副本的迁移、副本数的增加、broker 上下线的管理。
简单列举几个例子,如下:
- 监听Broker的变化。在Zookeeper中的/brokers/ids节点添加监听器BrokerChangeListener,监听Broker的变化。
- 监听Topic的变化。在Zookeeper中的/brokers/topics节点添加监听器TopicChangeListener,监听Topic的变化。在Zookeeper中的/admin/delete_topics节点添加TopicDeletionListener,用来处理删除Topic的动作。
- 从Zookeeper中读取获取当前所有与Topic、Partition和Broker的信息并进行管理。在Zookeeper中/brokers/topics/[topic]节点添加监听器PartitionModificationsListener,用来监听Topic中的分区分配变化。
6.2 Controller的选举机制
初始化流程
集群启动的时候,会自动选举一个Broker节点作为Controller来管理整个集群。集群中每个Broker都会尝试在Zookeeper上创建一个 /controller 临时节点,Zookeeper会保证有且仅有一个Broker能创建成功,这个Broker 就会成为集群的总控制器controller。
宕机恢复
当Broker Controller 宕机了,Zookeeper中的/controller临时节点会消失,集群里其他Broker节点会一直监听这个临时节 点,发现临时节点消失了,就竞争再次创建临时节点,重复上面的选举机制,Zookeeper又会保证有一个Broker 成为新的Controller。选举出新 Controller 的同时, /controller_epoch 中的值也会加 1,这个节点是 Controller的任期机制(主要是利用Zookeeper的watch机制)。
Kafka集群任期机制(脑裂现象)
Controller任期机制的概念:Controller在Kafka中有一个特定的任期号码(epoch),它通过ZooKeeper中的/controller_epoch来进行分布式协调,每次Controller选举完成后,选出的Controller节点会对其任期号码(epoch)加1,如果某个节点成为Controller,并且同时拥有的任期号码最大,那么它就是当前的有效Controller。
主要是为了解决脑裂问题:指的是集群中的节点之间失去了正常通信,导致出现多个子集无法达成一致的情况。当发生集群脑裂时,可能会导致多个部分集群认为自己是有效的Controller,这会导致数据不一致和操作冲突。此时以任期号码(epoch)最大为有效Controller。
6.3 Partition副本选举Leader机制原理
Controller感知到分区Leader所在的broker节点挂了(这种感知实际就是Zookeeper上注册的watch事件),Controller会从每个Parititon的 Followers(Replicas副本列表)中取第一个Broker作为Leader,当然这个Broker也要同时在ISR列表里。
这里有一个Unclean机制,就是这个分区的所有Follower都宕机的情况,可以通过unclean.leader.election.enable的取值来设置leader的选举范围:
false:必须等待ISR列表中由副本活过来才进行新的选举,该策略可靠性有保证,但是可用性低。
true:在ISR中没有副本存活的情况下,可以选择任何一个该Topic的Partition作为新的leader,该策略可用性高,但是不保证可靠性,可能会引发消息丢失。不建议使用
7、Kafka Consumer消费者消费原理
7.1 消费流程
Kafka消费消息采用的是poll模式,主动拉取消息数据,有利于根据消费者的能力去处理数据。具体流程如下:
- Consumer与Broker集群建立连接,其所连接上的任意Broker会向其发送Broker Controller的地址。
- Consumer指定消费的Topic,向Broker Controller发送poll请求。
- Broker Controller会为Consumer分配一个或者多个Partition Leader,并附带发送该Partition当前的offset。
- Consumer根据Broker Controller分配的Partition进行消费。
- 当Consumer消费完该条数据后,消费者会向Broker发送一个已消费的ack,即该消息的offset。
- 当Broker接收到Consumer的offset后,会将其更新到_consumer_offsets中。
7.2 消费者提交机制
Consumer的offset其实是作为一条普通的消息发送到Kafka的,消息的默认主题是_consumer_offsets,这个是在第一次消费者提交的时候才会创建,其默认有50个partition,使用哪个Partition使用的是消费者组ID的hash值与Partition数量取模处理。提交的数据结构为:key是 消费者组ID + Topic + 分区号,value就是当前offset的值。当Consumer消费完消息后,会将消费消息的offset提交给Broker,表示这些消息已经被消费(在Kafka 0.8之后,offset保存在Kafka集群上,在0.8版本之前,是保存在Zookeeper上的)。
Consumer提交消费offset的方式有自动提交、手动提交(手动同步提交,手动异步提交,手动同步异步混合提交)。
自动提交:自动提交需要设置enable.auto.commit为true。其优点就是较大提升消费速度,但是缺点是会产生重复消息。因为自动提交默认的是5秒提交一次(auto_commit_interval_ms),提交的内容是上一次被消费的数据,那么如果在这个时间内出现了Rebalance,在Rebalance后,Consumer需要重新从上一次确认过的offset处消费,就会造成之前三秒的数据再一次被消费。
手动提交:手动同步提交需要使用commitSync(),而手动异步提交需要使用commitAsync(),同步提交如果提交失败的时候一直尝试提交,直到遇到无法重试的情况下才会结束,这样会导致消费阻塞;而异步提交消费者线程不会阻塞,提交失败的时候也不会进行重试,这样可能会因为提交失败导致消费重复消费;可以采用同步和异步组合的方式,就是进行异步提交offset,但是需要监听Broker的响应结果,如果相应结果是提交失败,则再以同步的方式进行提交。
7.3消费者Rebalance机制
Rebalance 实际上就是规定⼀个 Consumer Group 下的所有 Consumer 如何达成⼀致,去分配订阅 Topic 的每个分区。其触发条件包括:
- 组成员数发⽣变更
- 订阅主题数发⽣变更
- 订阅主题的分区数发⽣变更
Kafka是在每一次Rebalance的时候,都会将所有的Partition和Consumer进行处理,原来的消费者消费的Partition也会重新Rebalance。所以如果Consumer集群足够大,就会造成很大的浪费和性能瓶颈,例如有的大集群,Rebalance一次就需要十几分钟,这是不可以忍受的。目前Kafka针对这种情况也没有很好的解决方案。
操作流程
选取组协调器(GroupCoordinator):
每个Consumer Group都会选择一个Broker作为自己的组协调器Coordinator,一般是提交offset到_commit_offsets时计算的分区Partition(这个分区计算方式上面有提到:hash(consumerGroupId) % _consumer_offsets主题的分区数)所对应的Leader节点所在的Broker,负责监控这个消费组里的所有消费者的心跳,以及判断是否宕机,然后开启消费者Rebalance。
加入消费组,并选出消费组协调器:
在找到消费组所对应的 GroupCoordinator 之后,加入消费组(消费者会向 GroupCoordinator 发送 JoinGroupRequest 请求)。
然后GroupCoordinator 从消费者组中选择第一个加入的Consumer作为Leader,可以称为消费组协调器,主要监控topic的变化,通知Coordinator触发Rebalance。
重新分配:
Consumer Leader通过给GroupCoordinator发送同步信息的请求(SyncGroupRequest),接着GroupCoordinator就把新的分区消费方案下发给各个Consumer,他们会根据指定分区的Leader Broker进行消息消费。
7.4 Rebalance的分区策略
有三种Rebalance的策略:Range(基于Topic)、RoundRobin(基于分区)、Sticky。默认是Range策略。
Range策略
Range是基于Topic,首先对一个Topic里面的分区进行排序,并对消费者按照字典顺序进行排序。然后用Partition的数量除以消费者的总数来决定每个消费者消费几个分区。每个消费者消费的分区是连续的,如果除不尽,那么前面几个消费者线程将会多消费一个分区。
如果消费者组订阅多个Topic,可能会导致前面几个消费者压力很大。
比如一个Topic有4个分区:p0,p1,p2,p3
消费者有3个:c0,c1,c2
按照计算:4/3=1,4%3=1
所以第一个消费者会多消费一个分区,最终消费分区为:
c0:p0,p1
c1:p2
c3:p3
RoundRobin策略
RoundRobin是基于分区的,会把该消费者组消费的所有分区都列出来,并按照字典序排序,然后再轮询的分给消费者。按照上面的例子,最终分配的结果为:
c0:p0,p3
c1:p1
c3:p2
Sticky策略
Sticky策略的实现会复杂一些,它是根据消费者组内每个消费者组的实际订阅数来进行分配。它主要逻辑有两点:
1.分区的分配尽可能的均匀,保证消费者分配到的主题分区数最多相差一个。
2.分区的分配尽可能的与上次分配的保持相同。
比如有3个Topic,分别有3个分区
A:A1,A2,A3
B:B1,B2,B3
C:C1,C2,C3
消费者组中有3个消费者,S1,S2,S3
按照RoundRobin分配结果如下:
S1:A1,B1,C1
S2:A2,B2,C2
S3:A3,B3,C3
当S3掉线后,根据RoundRobin进行重新分配,结果如下:
S1:A1,A3,B2,C1,C3
S2:A2,B1,B3,C2
但是如果按照Sticky策略重新分配,结果如下:
S1:A1,B1,C1,A3,C3
S2:A2,B2,C2,B3
保证了原分配的主题分区不变。
7.5 相关问题处理
如果某个消费者消费消息超时,触发Rebalance,重新分配后,该消息会被其他消费者消费,此时该消费者消费完成提交offset,导致错误怎么解决?
先说一下Generation,这个类似于“版本”的概念,Generation用来标记Rebalance,每次GroupCoordinator进行Rebalance时,都会使Generation加1,每次Consumer提交offset的时候,都会比对Generation是否一致,不一致就会拒绝。Kafka引入Consumer Generation主要是为了保护Consumer Group,防止无效的offset提交。
8、Kafka的消息存储原理
8.1 Kafka日志目录结构
.log 文件存储消息内容,.index文件存储消息的偏移量索引, .timeindex 文件存储消息的时间戳索引。
还会有其他文件
clear-offset-checkpoint:记录已清理和未清理的部分
log-start-offset-checkpoint:对应logStartOffset,用来标识日志的起始偏移量。各个副本在变动LEO和HW的过程中,logStartOffset也可能随之而动
meta.properties:记录分区的元数据。
recovery-point-offset-checkpoint:LEO(上面有解释)
replication-offset-checkpoint:HW(上面有解释)。
Kafka有一个定时任务,会将所有分区的LEO刷写到恢复点文件recovery-point-offset-checkpoint中,可以通过Broker参数log.flush.offset.checkpoint.interval.ms来配置,默认值为60000。还有一个定时任务负责将所有Partition的HW刷写到复制点文件replication-offset-checkpoint中,可以通过Broker端参数replica.high.watermark.checkpoint.interval.ms来配置,默认值为5000。
8.2 日志写入
顺序写:利用磁盘的顺序访问速度快,Kafka的消息都是追加append操作,Partition是有序的,节省了磁盘的寻道时间,同时通过批量操作、节省写入次数。Partition物理上分为多个segment存储,方便删除。
一个日志段文件满了,就自动创建一个新的日志段文件来写入,避免单个文件过大,影响文件的读写性能。
8.3日志存储
Kafka是基于硬盘存储,消息堆积能力更强。
Kafka具有存储功能,默认保存数据时间为7天或者大小1G,超7天或者1G,就会被清理掉。但是对于某些文件,其部分日志存储时间未达到7天,部分达到了7天,此时还是会保留该文件,直至其所有的消息都超过留存时间。所以消息消费完后,Kafka并不会删除对应的消息,而是由内部过期删除机制进行消息的删除。该过期时间可以配置(retention.ms和log.segment.bytes)。
索引文件(.index文件)
.index文件采取稀疏索引存储方式,减少索引文件大小,通过mmap(内存映射)可以直接内存操作。(kafka消息都是以batch的形式进行存储,因而索引文件中索引元素的最小单元是batch,通过索引文件能够定位到消息所在的batch,而没法定位到消息的具体位置,查找的时候,还需要对batch进行遍历)。
9、Kafka的高性能方案
9.1 异步发送
Kafka 提供了异步和同步两种消息发送方式。在异步发送中,整个流程都是异步的。调用异步发送方法后,消息会被写入 channel,然后立即返回成功。此时会有线程从 channel 轮询消息,将其发送到 Broker,同时会有另一个异步线程负责处理 Broker 返回的结果。同步发送本质上也是异步的,但是在处理结果时,会变成同步。使用异步发送可以最大化提高消息发送的吞吐能力。
9.2批量发送
Kafka 支持批量发送消息,将多个消息打包成一个批次ProducerBatch进行发送,提高发送效率和吞吐量,减少带宽。以下两个参数比较关键:
- batch.size:控制批量发送消息的大小,默认值为 16KB,可根据实际情况进行调整。
- linger.ms:控制消息在批量发送前的等待时间,默认值为 0。当 linger.ms 大于 0 时,如果有消息发送,Kafka 会等待指定的时间,如果等待时间到达或者批量大小达到 batch.size,就会将消息打包成一个批次进行发送。可根据实际情况进行调整。
根据上面生产者架构可以知道,发送消息时,当缓冲区中的消息大小达到 batch.size 或者等待时间到达 linger.ms 时,Kafka 会将缓冲区中的消息打包成一个批次ProducerBatch进行发送。如果在等待时间内没有达到 batch.size,Kafka 也会将缓冲区中的消息发送出去,从而避免消息积压。
9.3消息压缩
Kafka支持消息压缩,将消息进行压缩后进行传输,从而减少网络开销,提高网络传输的效率和吞吐量,但是压缩和解压缩的过程会消耗CPU 资源,因此需要根据实际情况进行调整。
Kafka 支持多种压缩算法,在 Kafka2.1.0 版本之前,支持 GZIP,Snappy 和 LZ4,2.1.0 后还支持 Zstandard 算法。
吞吐量:
LZ4 > Snappy > Zstandard 和 GZIP,
压缩比:
Zstandard > LZ4 > GZIP > Snappy。
以下两个参数比较关键:
- compression.type:控制压缩算法的类型,默认值为 none,表示不进行压缩。
- compression.level:控制压缩的级别,取值范围为 0-9,默认值为-1。当值为-1 时,表示使用默认的压缩级别。
生产者这边,发送消息时,如果启用了压缩技术,Kafka 会将消息进行压缩后再进行传输。消费者这边,如果消息进行了压缩,会在消费消息时将其解压缩。注意:压缩算法要统一,避免不必要的性能开销。
9.4页缓存和顺序追加落盘
为了提升系统吞吐、降低时延,Broker 接收到消息后只是将数据写入PageCache中,而 PageCache 中的数据由操作系统的控制进行异步刷盘的时机(避免了同步刷盘的系统开销),将数据顺序追加写到磁盘日志文件中。Pagecache 是在内存中进行缓存,读写速度快,可以大大提高读写效率。顺序追加写充分利用顺序 I/O 写操作,避免了缓慢的随机 I/O 操作,可有效提升 Kafka 的吞吐量。
9.5零拷贝
零拷贝技术是指在网络传输过程中,避免不必要的内存拷贝,提高数据传输效率。而Kafka 中存在大量的网络数据持久化到磁盘和磁盘文件通过网络发送的过程(就是发送消息和消费消息的过程),所有Kafka在这方面做了操作,网络数据持久化磁盘用 mmap 技术,网络数据传输使用 sendfile 技术。
mmap
mmap 是一种内存映射文件的方法,它可以将文件映射到进程的地址空间中,使进程可以访问和操作文件内容,而无需进行显式的拷贝操作。
Kafka中的索引文件主要在于提取日志文件中的消息时进行高效查找。这些索引文件被维护为内存映射文件,这允许Kafka快速访问和搜索内存中的索引,从而快速定位日志文件中的消息。mmap 将内核空间中的读缓冲区的地址与用户空间的缓冲区地址进行映射,从而实现内核缓冲区与应用程序的内存共享,避免了将数据从内核缓冲区拷贝到用户缓冲区的过程。
sendfile
Kafka 在 Consumer 从 Broker 拉取数据过程中使用了sendfile 技术。通过 NIO 的transferTo/transferFrom调用操作系统的 sendfile 实现零拷贝。如上图。
9.6 稀疏索引
Kafka的位移索引文件.index和时间戳索引文件.timeindex 文件是按照稀疏索引的思想进行设计的。稀疏索引的核心是写入一定的记录之后才会增加一个索引值,具体这个区间可以通过 log.index.interval.bytes 参数进行控制,默认大小为 4 KB,表示Kafka 至少写入 4KB 消息数据后,才会在索引文件中增加一个索引项,因此 log.index.interval.bytes 也是 Kafka 调优一个重要参数值。由于索引文件也是按照消息的顺序性进行增加索引项的,因此 Kafka 可以利用二分查找算法来搜索目标索引项,时间复杂度为 O(logN),大大的缩短查找的时间。
9.7 Broker和数据分区
Kafka 集群包含多个 Broker。一个 Topic 下有多个 Partition,Partition 分布在不同的 Broker 上,用于存储 Topic 的消息,这使 Kafka 可以在多台机器上处理、存储消息,给 kafka 提供给了并行的消息处理能力和横向扩容能力。