Kafka 知识点整理

1. 简介

Kafka 是一个分布式流式处理平台。

应用场景

  1. 消息队列 :建立实时流数据管道,可靠地在系统或应用程序之间获取数据。
  2. 数据处理: 构建实时的流数据处理程序来转换或处理数据流。

优势

  1. 极致的性能 :基于 Scala 和 Java 语言开发,设计中大量使用了批量处理和异步的思想,最高可以每秒处理千万级别的消息。
  2. 生态系统兼容性无可匹敌 :Kafka与周边生态系统的兼容性是最好的,尤其在大数据和流计算领域。
  3. 高吞吐和低延迟:Kafka 写入数据采用追加写的方式,避免了磁盘随机写操作。在写数据的时候是先写操作系统的页缓存,然后由操作系统自行决定何时刷到磁盘。页缓存是在内存中分配的,所以消息写入的速度很快,即便 broker 挂了,页缓存数据也不会丢失,重启后就可以继续提供服务。Kafka 也不必和底层的文件系统进行交互,所有繁琐的 I/O 操作都由操作系统来处理。读取数据时利用以 sendfile 为代表的零拷贝技术提高了效率。

2. 消息投递模式

对于消息中间件,一般有两种消息投递模式:点对点(P2P,Point-to-Point)模式和发布/订阅(Pub/Sub)模式。Kafka同时支持两种消息投递模式。

  1. 点对点:基于队列的,消息生产者发送消息到队列,消息消费者从队列中接收消息。
  2. 发布/订阅:向一个内容节点发布和订阅消息。在Kafka中这个内容节点称为主题,主题是消息传递的中介,消息发布者将消息发布到某个主题,而消息订阅者从主题中订阅消息。主题使得消息的发布者和订阅者互相保持独立,不需要进行接触即可保证消息的传递,在消息的一对多广播时采用。

实现

  1. 点对点:如果所有的消费者都属于同一个消费组,那么所有的消息都会被均衡地投递给每一个消费者,即每条消息只会被一个消费者处理。
  2. 发布/订阅:如果所有的消费者都属于不同的消费组,那么所有的消息都会被广播给所有的消费者,即每条消息会被所有的消费者处理。

3. 基本概念

3.1 消息

Kafka的数据单元被称为消息,可以把消息看成是数据库里的一个数据行。消息由字节数组组成,所以对于Kafka 来说,消息里的数据没有特别的格式或含义。

消息有个可选的元数据,也就是键,键也是字节数组。键会生成一致性散列值,然后对主题分区进行取模,为消息选择分区,保证具有相同键被写到相同的分区。

3.2 批次

批次就是一组消息,这些消息属于同一个主题和分区。如果每一个消息都单独穿行于网络,会导致大量的网络开销,所以把消息分成批次传输减少网络开销。

批次的大小由batch.size参数来指定,默认为163834,即16KB。

3.3 模式

对于Kafka来说,消息就只是字节数组,可以使用模式来定义消息内容,让它们更易于理解。比如:JSON、XML或Avro等。

3.4 主题

Kafka的消息通过主题进行分类。主题可被分为多个分区,一个分区就是一个提交日志。消息以追加的方式写入分区,然后以先入先出的顺序读取。由于一个主题一般包含几个分区,因此无法在整个主题范围内保证消息的顺序,但可以保证消息在单个分区内的顺序。
主题与分区

3.5 分区

每个主题由多个分区组成,每个分区内部的数据保证了有序性,消息以追加的方式写入分区的尾部。这种连续性的文件存储,有效的利用磁盘的线性存取,也减轻了内存的压力。根据配置,Kafka 也会定期清理过期的文件。Kafka通过分区来实现数据冗余和伸缩性,分区可以分布在不同的服务器上。也就是说,一个主题可以横跨多个服务器,以此来提供比单个服务器更强大的性能。

3.6 生产者

生产者创建消息。

在默认情况下,生产者会把消息均衡地发布到主题的所有分区上,而并不关心消息会被写到哪个分区。但在某些情况下,当指定消息键的时候,分区器会为键生成一个散列值,并将其映射到指定的分区上,生产者会把消息直接写到指定的分区。这样可以保证包含同一个键的消息会被写到同一个分区上。当然也可以使用自定义的分区器,根据不同的业务规则将消息映射到分区。

3.7 消费者

消费者读取消息。订阅一个或多个主题,按照消息生成的顺序读取它们,并通过检查消息的偏移量来区分已经读取过的消息。

偏移量是一个不断递增的整数,在创建消息时,Kafka会把它添加到消息里。在给定的分区里,每个消息的偏移量都是唯一的。消费者把每个分区最后读取的偏移量保存在主题__consumer_offsets,如果消费者关闭或重启,它的读取状态不会丢失。

3.8 消费者组

消费者是消费者群组的一部分,会有一个或多个消费者共同读取一个主题。群组保证每个分区只能被一个消费者使用。

消费者与分区之间的映射通常被称为消费者对分区的所有权关系。通过这种方式,消费者可以消费包含大量消息的主题。如果有消费者失效,群组里的其他消费者可以接管失效消费者的工作。
消费者和消费者组

3.9 broker

一个独立的Kafka服务器被称为broker。broker接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存;broker为消费者提供服务,对读取分区的请求作出响应,返回已经提交到磁盘上的消息。

集群
broker是集群的组成部分。在集群中,一个主题有多个分区,一个分区从属于一个broker,该broker被称为分区的Leader;一个分区可以分配给多个broker,会发生分区复制。

控制器
每个集群都有一个broker同时充当了集群控制器的角色,它自动从集群的活跃成员中选举出来。控制器管理 broker 加入和退出集群、分区重分配、创建和删除主题、主题分区扩展以及整个集群中分区和副本的状态。

当某个分区的Leader副本出现故障时,由控制器负责为该分区选举新的Leader副本。当检测到某个分区的ISR集合发生变化时,由控制器负责通知所有broker更新其元数据信息。
集群

4. Zookeeper 在 Kafka 中的作用

管理broker
节点 /brokers/ids 下记录运行着broker列表。每个 broker 在启动时,都会到该节点下创建临时节点,将IP和端口等信息记录到该节点上。

主题配置
节点 /brokers/topics 下记录现有主题列表,每个主题的分区数,所有副本的位置,分区Leader等分区信息及与 broker 的对应关系。

访问控制列表
维护了所有主题的访问控制列表。

控制器选举
节点 /controller 记录当前控制器的brokerid的值。
节点 /controller_epoch 记录当前控制器的纪元,保证控制器的唯一性,避免脑裂(有两个节点同时认为自己是当前的控制器)。

在任意时刻,集群中有且仅有一个控制器。每个broker启动的时候都会尝试获取节点 /controller 下的brokerid的值,如果值不为-1,则表示有其他broker节点选举为控制器,就放弃选举。如果不存在节点 /controller,或者节点数据异常,就会创建 /controller 节点,当前broker创建的时候,也有可能其他broker也在同时去尝试创建这个节点,只有创建成功的那个broker才会成为控制器。每个broker都会在内存中保存当前控制器的brokerid值。

5. 生产者

生产者流程
生产者
整个生产者客户端由两个线程协调运行,分别为主线程和Sender线程。

主线程中由KafkaProducer创建消息,然后通过拦截器(可选)、序列化器和分区器后,缓存到消息累加器中。消息累加器由多个消息分区所对应的双端队列组成。新消息会记录到一个批次中,然后追加到分区对应双端队列尾部,一个双端队列中所有消息都发往相同的主题和分区。

Sender 线程负责从消息累加器中获取消息并将其发送到Kafka中。将消息累加器中的消息经应用逻辑层面到网络IO层面的转换后,再封装成Request。请求在发送之前还会保存到InFlightRequests中。InFlightRequests主要是缓存了已经发出去但还没有收到响应的请求。参数max.in.flight.requests.per.connection限制缓存每个连接最多缓存的请求数,默认为5。

Kafka在收到消息后会返回一个响应。如果消息成功写入,就返回RecordMetaData对象,它包含了主题和分区的信息,以及分区里的偏移量。如果失败,返回一个错误。生产者在收到错误之后会进行重试,如果几次之后如果还是失败,就返回错误信息。参数retries限制请求失败时重试次数。

发送消息的方式

  1. 发送并忘记(fire-and-forget):把消息发送给服务器,但并不关心它是否正常到达。
  2. 同步发送:使用send()方法发送消息,它会返回一个Future对象,调用get()方法进行等待。
  3. 异步发送:调用send()方法并指定一个回调函数,服务器在返回响应时调用该函数。

6. 消费者和消费者组

消费者从属于一个消费者组。一个群组里的消费者订阅同一个主题,每个消费者接收主题中一部分分区的消息。

6.1 再均衡(Rebalance)

消费者通过向被指派为群组协调器的broker (不同的群组可以有不同的协调器)发送心跳来维持它们和群组的从属关系以及它们对分区的所有权关系。只要消费者以正常的时间间隔发送心跳,就被认为是活跃的。消费者也会在轮询消息或提交偏移量时发送心跳。

如果消费者停止发送心跳的时间足够长,会话就会过期,群组协调器认为它已经死亡,就会触发一次再均衡。如果一个消费者发生崩溃,并停止读取消息,群组协调器会等待几秒钟,确认它死亡了才会触发再均衡。

再均衡是指分区的所有权从一个消费者转移到另一个消费者的行为。保证了消费者组的高可用性和伸缩性,使我们可以方便又安全地删除消费组内的消费者或往消费组内添加消费者。再均衡期间,消费者组内的消费者无法读取消息,会造成整个群组一小段时间的不可用。另外,当一个分区被重新分配给另一个消费者时,消费者当前的状态也会丢失。一般情况下,应尽量避免不必要的再均衡的发生。

触发条件

  1. 旧的消费者被挂了
  2. 新的消费者加入
  3. 主题添加新分区
  4. 消费者发送心跳超时,会话超时
  5. 消费者两次poll超过最大时间间隔 max.poll.interval.ms

再均衡过程

  1. 消费者加入群组。消费者向群组协调器发送加入组的请求,然后群组协调器从消费者中选出一个群组Leader(第一个加入群组的消费者),并把群组成员和订阅信息发送给群组Leader,群组Leader制定消费分配方案。
  2. 同步分配方案。群组Leader为群组成员分配主题和分区,完成后发送给群组协调器。群组协调器接收到分配方案后再发送给群组成员。这样群组成员就知道该消费哪些分区。

避免不必要的Rebalance

  1. 及时发送心跳。保证消费者被判断死亡之前,能够至少经过3轮的心跳请求。即 session.timeout.ms >= 3 * heartbeat.interval.ms
    如:设置 session.timeout.ms = 6s,heartbeat.interval.ms = 2s。
  2. 防止消费时间过长。适当增大参数max.poll.interval.ms,表示最大轮询间隔,即调用poll()之间的最大延迟。

再均衡监听器
消费者在退出和进行分区再均衡之前,会做一些清理工作。可以在消费者失去对一个分区的所有权之前提交最后一个已处理记录的偏移量。如果消费者准备了一个缓冲区用于处理偶发的事件,那么在失去分区所有权之前,需要处理在缓冲区累积下来的记录。可能还需要关闭文件句柄、数据库连接等。

6.2 偏移量和提交

Kafka中有两个offset,一个表示消息在分区中对应的位置,一个表示消费到分区中某个消息的位置。对于消息在分区中的位置,将offset称为偏移量;对于消费者消费到的位置,将offset称为位移。

提交就是将消费位移存储在Kafka内部主题__consumer_offsets中,提交的值为当前消费到的位置加1。

如果提交的位移小于上一次提交的位移,会导致重复消费。
重复消费
如果提交的位移大于上一次提交的位移,会导致消息丢失。
消息丢失

提交方式

  1. 自动提交。参数enable. auto.commit设为true,那么默认每过5s(由参数auto.commit.interval.ns控制),消费者会自动把接收到的最大偏移量提交上去。
    如果按默认5s提交,在最后一次提交后的3s内发生的再均衡,那么偏移量会落后3s,在这3s内到达的消息,有可能重复消费。
  2. 同步提交 commitsync()。参数auto.commit.offset设为false,使用 commitSync() 同步提交偏移量。
    如果发生了再均衡,从最近一批消息到发生再均衡之间的所有消息都将被重复处理。
  3. 异步提交 commitAsync()。参数auto.commit.offset设为false,使用 commitAsync() 异步提交偏移量。
    在成功提交或碰到无法恢复的错误之前,commitSync() 会一直重试,但是 commitAsync() 不会。异步提交之所以不进行重试,是因为在它收到服务器响应的时候,可能有一个更大的偏移量已经提交成功,防止出现重复消费。

6.3 回溯消费(指定位置消费)

消费者可以通过 seek() 方法手动设置位移,从而实现指定位置消费。

public void seek(TopicPartition partition, long offset)

7. 分区

7.1 多副本机制

Kafka为分区引入了多副本机制,通过增加副本的数量来提升容灾能力,保证消息存储的安全性。同一分区的不同副本中保存的是相同的消息,但同一时刻,副本之间并非完全一样。

副本之间是一主多从的关系,其中Leader副本负责处理读写请求,Follower副本只负责同步Leader副本的消息。副本处于不同的broker中,当Leader副本出现故障时,从Follower副本中重新选举出新的Leader副本对外提供服务,实现了故障的自动转移,保证集群中某个broker失效时仍能提供服务。当选举出新的Leader副本后,相关的生产者和消费者都要重新进行连接。

7.2 分区副本

  1. AR (Assigned Replicas):分区中的所有副本。
  2. ISR (In-Sync Replicas):所有与Leader副本保持一定程度同步的副本(包括Leader副本在内), ISR集合是AR集合中的一个子集。
  3. OSR (Out-of-Sync Replicas):所有与Leader副本同步滞后过多的副本(不包括Leader副本)。

AR=ISR+OSR。在正常情况下,所有的Follower副本都应该与Leader副本保持一定程度的同步, 即AR=ISR,OSR集合为空。

Leader副本负责维护和跟踪ISR集合中所有Follower副本的滞后状态,当Follower副本落后太多或失效时,Leader副本会把它从ISR集合中剔除。如果OSR集合中有Follower副本“追上”了Leader副本,那么Leader副本会把它从OSR集合转移至ISR集合。默认情况下,当Leader副本发生故障时,只有在ISR集合中的副本才有资格被选举为新的Leader,而在OSR集合中的副本则没有任何机会(这个原则可通过修改参数配置来改变)。

HW与LEO
ISR与HW和LEO也有紧密的关系。分区ISR集合中的每个副本都会维护自身的LEO,而ISR集合中最小的LEO即为分区的HW,消费者只能消费HW之前的消息。

  1. HW(High Watermark, 高水位):表示一个特定的消息偏移量,消费者只能拉取到这个偏移量之前的消息。
  2. LEO(Log End Offset,日志结束偏移量):表示当前日志文件中下一条待写入消息的偏移量,即当前日志分区中最后一条消息的偏移量值加1。

Kafka 的复制机制既不是完全的同步复制,也不是单纯的异步复制。事实上,同步复制要求所有能工作的Follower副本都复制完,这条消息才会被确认为已成功提交,但这种复制方式极大地影响了性能。而在异步复制方式下, Follower副本异步地从Leader副本中复制数据,数据只要被Leader副本写入就被认为已经成功提交。在这种情况下,如果Follower副本都还没有复制完而落后于Leader副本,突然Leader副本宕机,就会造成数据丢失。Kafka使用的这种ISR的方式则有效地权衡了数据可靠性和性能之间的关系。

7.3 优先副本

在创建主题的时候,该主题的分区和副本会尽可能均匀的分布到集群的各个broker节点上,对应的Leader副本也分配的比较均匀。随着时间的更替,集群中的broker节点不可避免地会遇到宕机或崩溃的问题,当分区的Leader节点发生故障时,其中一个Follower节点就会成为新的Leader节点,这样就会导致集群的负载不均衡,从而影响整体的健壮性和稳定性。

为了能够有效地治理负载失衡的情况,Kafka 引入了优先副本(preferred replica)。优先副本是指在AR集合列表中的第一个副本。理想情况下,优先副本就是该分区的Leader副本。 Kafka 要确保所有主题的优先副本在集群中均匀分布,这样就保证了所有分区的Leader均衡分布。如果Leader分布过于集中,就会造成集群负载不均衡。

所谓的优先副本的选举就是将优先副本被选举为Leader副本,以此来促进集群的负载均衡,这一行为也可以称为分区平衡。

7.4 分区重分配

当broker失效时,该节点上的分区副本都已经处于功能失效的状态,Kafka并不会将这些失效的分区副本自动地迁移到集群中剩余的可用broker节点上。如果放任不管,则不仅会影响整个集群的负载均衡,还会影响整体服务的可用性和可靠性。

当要对集群中的一个节点进行有计划的下线操作时,为了保证分区及副本的合理分配,应该将该节点上的分区副本迁移到其他的可用节点上当集群中新增broker节点时,只有新创建的主题分区才有可能被分配到这个节点上,而之前的主题分区并不会自动分配到新加入的节点中,因为在它们被创建时还没有这个新节点,这样新节点的负载和原先节点的负载之间严重不均衡。

为了解决上述问题,需要让分区副本再次进行合理的分配,也就是所谓的分区重分配。Kafka 提供了 脚本 kafka-reassign-partitions.sh 来执行分区重分配的工作,它可以在集群扩容、broker节点失效的场景下对分区进行迁移。

7.5 分区复制限流

分区重分配本质在于数据复制,先增加新的副本,然后进行数据同步,最后删除旧的副本来达到最终的目的。

数据复制会占用额外的资源,如果重分配的量太大必然会严重影响整体的性能。副本间的复制限流可以通过脚本 katfka-config.sh 和 kafka-ressign-partitions.sh 来实现。

7.6 分区选举

分区Leader副本的选举由控制器负责。当创建主题,增加分区或分区原Leader副本下线时都需要进行Leader副本的选举。

选举流程
从AR集合中找到第一个存活的副本,并且这个副本在ISR集合中。

一个分区的AR集合在分配的时候就被指定,并且只要不发生分区重分配的情况,集合内部副本的顺序是保持不变的,而分区的ISR集合中副本的顺序可能会改变。

  1. 分区重分配后Leader副本选举。从重分配的AR集合中找到第一个存活的副本,并且这个副本在目前的ISR列表中。
  2. 优先副本选举。直接将优先副本设置为Leader,AR集合中的第一个副本即为优先副本。
  3. 节点优雅关闭,该节点的Leader副本都会下线,对应分区执行Leader副本的选举。从AR集合中找到第一个存活的副本,并且这个副本在目前的ISR列表中,还要确保这个副本不处于正在被关闭的节点上。

8. 日志

Kafka的基本存储单位是分区。一个分区对应一个日志(Log)。为了防止日志过大,将日志切分为多个日志分段(LogSegment),相当于一个巨型文件被平均分配为多个相对较小的文件,方便消息的维护和清理。日志在物理上只以文件夹的形式存储,而每个日志分段对应于磁盘上的一个日志文件和两个索引文件,以及可能的其他文件。

向日志中追加消息时是顺序写入的,只有最后一个日志分段才能执行写入操作,称为活跃分段(activeSegment)。随着消息的不断写入,当活跃满足一定的条件时, 就需要创建新的活跃分段,之后追加的消息将写入新的活跃分段。参数 log.segment.bytes 限制单个日志段文件的最大大小,默认是1G。参数 log.roll.jitter.ms 表示日志段新增扰动值,防止同一时刻进行多个文件的切分。

每个日志分段都有一个基准偏移量(baseOffset),用来表示当前日志分段中第一条消息的偏移量。偏移量是一个64位的长整型数,日志文件和两个索引文件都是根据基准偏移量来命名的。名称固定为20位数字,没有达到的位数则用0填充。比如第一个日志分段的基准偏移量为0,对应的日志文件为00000000000000000000.log。

日志结构

8.1 日志索引

为了便于消息的检索,每个日志分段中的日志文件(以“.log” 为文件后缀)都有对应的两个索引文件:偏移量索引文件和时间戳索引文件。偏移量和时间戳都严格单调递增,因此通过二分查找查询索引,再顺序遍历日志文件找到消息。

  1. 偏移量索引文件:以“.index"为文件后缀。用来建立消息偏移量到物理地址之间的映射关系,方便快速定位消息所在的物理文件位置。
  2. 时间戳索引文件:以“.timeindex”为文件后缀。根据指定的时间戳来查找对应的偏移量信息。

Kafka中的索引文件以稀疏索引的方式构造消息的索引,它并不保证每个消息在索引文件中都有对应的索引项。每当写入一定量(由broker 端参数log.index.interval.bytes指定,默认值为4096,即4KB)的消息时,偏移量索引文件和时间戳索引文件分别增加一个偏移量索引项和时间戳索引项,增大或减小log.index.interval.bytes的值,对应地可以增加或缩小索引项的密度。

稀疏索引:只为某些值建立索引,索引项对应的记录可以有多个。

8.2 日志清理

Kafka 提供了两种日志清理策略。

  1. 日志删除(Log Retention):按照一定的保留策略直接删除不符合条件的日志分段。
  2. 日志压缩(Log Compaction):针对每个消息的key进行整合,对于有相同key的不同value值,只保留最后一个版本。

通过broker端参数log.cleanup.policy来设置日志清理策略,默认值为“delete”,即采用日志删除的清理策略。如果要采用日志压缩的清理策略,就需要该参数设置为“compact",并且还需要将log.cleaner.enable(默认值为true)设为true。通过将log.cleanup.policy参数设置为“delete,compact",还可以同时支持日志删除和日志压缩两种策略。日志清理的粒度可以控制到主题级别,log.cleanup.policy对应的参数为cleanup.policy。

8.2.1 日志删除

在Kafka的日志管理器中会有一个专门的日志删除任务来周期性地检测和删除不符合保留条件的日志分段文件,这个周期可以通过broker端参数log.retention.check.interval.ms来配置,默认5分钟。当前日志分段的保留策略有3种:基于时间、基于日志大小和基于日志起始偏移量。

基于时间
日志删除任务会检查当前日志文件中是否有保留时间超过设定的阈值(retentionMs)来寻找可删除的日志分段文件集合。

retentionMs可以通过broker端参数log.retention.hours,log.retention.minutes和log. retention.ms来配置。优先级:log.retention.ms>log. retention.minutes>log.retention.hour。默认情况下只配置了log.retention.hours参数,其值为168。所以默认情况下日志分段文件的保留时间为7天。

基于日志大小
日志删除任务会检查当前日志的大小是否超过设定的阈值(retentionSize)来寻找可删除的日志分段的文件集合。

retentionSize可以通过broker端参数1og.retention.bytes来配置,默认值为1,表示无穷大。参数1og.retention.bytes配置的是所有日志文件的总大小,而不是单个日志分段的大小。单个日志分段的大小由broker 端参数log.segment.bytes 来限制,默认为1GB。

基于日志起始偏移量
一般情况下,日志文件的起始偏移量等于第一个日志分段的基准偏移量,但这并不是绝对的,日志文件的起始偏移量可以通过
DeleteRecordsRequest请求(比如使用KafkaAdminClient的deleteRecords()方法、使用kafka-delete-records.sh脚本)、日志的清理和截断等操作进行修改。

基于日志起始偏移量的保留策略的判断依据是当前日志分段的下一个日志分段的起始偏移量是否小于等于日志文件的起始偏移量,若是,则可以删除当前日志分段。

8.2.2 日志压缩

日志压缩对于有相同key的不同value值,只保留最后一个版本。如果应用只关心key对应的最新value值,则可以开启Kafka的日志清理功能,Kafka会定期将相同key的消息进行合并,只保留最新的value值。

日志压缩执行前后,日志分段中的每条消息的偏移量和写入时的偏移量保持一致。日志压缩会生成新的日志分段文件,日志分段中每条消息的物理位置会重新按照新文件来组织。日志压缩执行过后的偏移量不再是连续的,不过这并不影响日志的查询。

墓碑消息:消息的key不为null,value为null。
日志清理线程发现墓碑消息时会先进行常规的清理,并保留墓碑消息一段时间。

8.3 磁盘存储

Kafka在设计时采用了文件追加的方式来写入消息,即只能在日志文件的尾部追加新的消息,并且也不允许修改已写入的消息。这种方式属于典型的顺序写盘的操作,所以就算Kafka使用磁盘作为存储介质,它所能承载的吞吐量也不容小觑。但这并不是让Kafka在性能上具备足够竞争力的唯一因素。

8.3.1 页缓存

Kafka中大量使用了页缓存,这是Kafka 实现高吞吐的重要因素之一。

8.3.2 磁盘I/O流程

不同的磁盘调度算法(以及相应的I/O优化手段)对Kafka这类依赖磁盘运转的应用的影响很大,建议根据不同的业务需求来测试并选择合适的磁盘调度算法。从文件系统层面分析,Kafka 操作的都是普通文件,并没有依赖于特定的文件系统,但是依然推荐使用EXT4或XFS。尤其是对XFS而言,它通常有更好的性能,这种性能的提升主要影响的是Kafka的写入性能。

8.3.3 零拷贝

除了消息顺序追加、页缓存等技术,Kafka 还使用零拷贝(Zero-Copy)来进一步提升性能。零拷贝是指将数据直接从磁盘文件复制到网卡设备中,而不需要经由应用程序之手。零拷贝是针对内核模式而言的,数据在内核模式下实现了零拷贝。零拷贝大大提高了应用程序的性能,减少了内核和用户模式之间的上下文切换。

DMA(Direct Memory Access,直接内存访问)是一种硬件设备绕开CPU独立直接访问内存的机制。所以DMA在一定程度上解放了CPU,把之前CPU的杂活让硬件直接自己做了,提高了CPU效率。目前支持DMA的硬件包括:网卡、声卡、显卡、磁盘控制器等。

非零拷贝流程

  1. 调用read(),将文件中的内容通过DMA复制到内核模式下的ReadBuffer中;
  2. 通过CPU复制将内核模式数据复制到用户模式下;
  3. 调用write()时,将用户模式下的内容通过CPU复制到内核模式下的Socket Buffer中;
  4. 将内核模式下的SocketBuffer中的数据通过DMA复制到网卡设备中传送。

在非零拷贝流程中,数据经历了4次状态切换,2次模式切换,2次DMA复制,2次CPU复制。
非零拷贝
非零拷贝

零拷贝流程
对Linux操作系统而言,零拷贝技术依赖于底层的sendfile()方法。Kafka使用FileChannel.transferTo()方法,其底层实现就是sendfile()方法。

  1. 将文件中的内容通过DMA复制到内核模式下的ReadBuffer中;
  2. sendfile() 将内核模式下的ReadBuffer中对应的数据描述信息(文件描述符、地址偏移量等信息)记录到socket缓冲区中;
  3. DMA控制器根据socket缓冲区中的地址和偏移量将数据从内核模式下的ReadBuffer中复制到网卡中。

在零拷贝流程中,数据经历了2次状态切换,0次模式切换,2次DMA复制,0次CPU复制。
零拷贝
零拷贝

9. Kafka 如何保证消息不丢失

生产者丢失消息
在发送后添加回调函数。将参数retries(重试次数)设置一个比较合理的值,一般是 3。设置完成之后,当出现网络问题之后能够自动重试消息发送,避免消息丢失。另外,建议还要设置重试间隔,间隔太小的话重试的效果就不明显了。

消费区丢失消息
关闭自动提交。在真正消费完消息之后再手动提交位移,但也有可能重复消费。

kafka弄丢消息

  1. 设置 acks = all。保证所有副本都要接收到该消息之后,该消息才算真正成功被发送。
  2. 设置 replication.factor >= 3。保证分区至少3个副本。
  3. 设置 min.insync.replicas > 1。保证消息至少要被写入到 2 个副本才算是被成功发送。
    为了保证整个 Kafka 服务的高可用性,确保 replication.factor > min.insync.replicas。如果两者相等的话,只要是有一个副本挂掉,整个分区就无法正常工作了。所以设置 replication.factor = min.insync.replicas + 1
  4. 设置 unclean.leader.election.enable = false。保证Leader副本发生故障时就不会从Follower副本中和Leader同步程度达不到要求的副本中选择出 Leader。
    Kafka 0.11.0.0版本开始 unclean.leader.election.enable 参数的默认值由原来的true改为false。

10. Kafka 如何保证消息不重复消费

原因
重复消费是消费者已经消费了数据,但是位移没有提交。

  1. 强行kill线程,导致消费数据后位移没有提交。
  2. 消费者消费数据处理耗时,超过了会话超时时间触发再均衡,此时有一定几率位移没有提交。
  3. 分区重分配。
  4. 消费者两次poll超过最大时间间隔 max.poll.interval.ms(默认值300000毫秒,5分钟),导致再均衡。

解决
保证接口幂等。使得接口任意多次执行所产生的影响均与一次执行的影响相同。

  1. 数据要写库,先根据主键查一下,如果数据有了,就别插入了,直接更新。
  2. 某个数据写Redis,那每次都set,Redis天然幂等性。
  3. 对于消息,可以建个表来专门存储消息消费记录。
    生产者在发送消息前判断表中是否有记录,没有记录就先入库,状态为待消费,然后发送消息时把主键id带上。有记录说明已发送,就不发送该消息。
    消费者接收消息,根据主键id查询消息消费记录表,判断消息状态是否已消费。如果没消费过,则处理消息,处理完后再更新消息记录为已消费。如果已消费,就忽略。
  4. 添加前置条件,如版本号。每次更数据前,比较当前数据的版本号是否和消息中的版本号一致 ,如果不一致就拒绝更新数据,更新数据的同时将版本号加1,实现幂等。

11. Kafka 异步发消息阻塞

在新版 Kafka 生产者中,客户端发送的消息都会被存储到消息累加器 RecordAccumulator 中,同时 Sender 线程从消息累加器中获取消息并将其发送到 broker。所以在新版生产者中的所有发送都可以看作是异步发送,仅保留了一个 send() 方法,同时返回一个 Future 对象,需要同步等待发送结果,就使用 Future.get() 方法阻塞获取发送结果。

在构建 Kafka 生产者时,参数 buffer.memory 设置自定义缓冲池大小,默认大小为 32M。因为缓冲池的大小是有限的,所以当缓冲池的内存块用完后,消息追加调用将会被阻塞,直到有空闲的内存块。如果每分钟需要发送几百万条消息,只要集群负载很高或者网络稍有波动,Sender 线程从缓冲池获取消息的速度赶不上客户端发送的速度,就会造成客户端发送阻塞。可以通过添加适当的延迟时间来控制发送的速度。

Kafka 生产者通常在第一次发送消息之前,需要获取该主题的元数据 Metadata,Metadata 包括了主题相关分区 Leader 所在节点信息、副本所在节点信息、ISR 列表等,生产者获取 Metadata 后,便会根据 Metadata 内容将消息发送到指定的分区 Leader 上。并且生产者在发送消息之前,会检查主题的 Metadata 是否需要更新,如果需要更新,则会唤醒 Sender 线程并发送 Metatadata 更新请求,此时 Kafka 生产者主线程则会阻塞等待 Metadata 的更新。如果 Metadata 一直无法更新,则会导致客户端一直阻塞在那里。

参考:
Kakfa权威指南
深入理解Kafka:核心设计与实践原理
Kafka 官方文档
Kafka
《浅入浅出》-Kafka
Kafka 常见问题
Kafka Rebalance机制分析
Kafka面试题总结
分布式事务、重复消费、顺序消费
kafka日志段如何读写
关于零拷贝的一点认识
终于有人把零拷贝Zero-Copy讲懂了

kafka是如何保证消息不被重复消费的
什么?搞不定Kafka重复消费?
消息中间件(四)之-kafka重复消费问题
Kafka如果丢了消息,怎么处理的?

Kafka Producer 异步发送消息居然也会阻塞?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
"Java高级架构面试知识点整理.pdf"是一份关于Java高级架构的面试知识点的文档。该文档主要包括以下几个方面的内容: 1. Java多线程和并发:讲解Java中的多线程概念、线程安全性、线程的生命周期和状态转换、线程同步与锁、并发工具类(如CountDownLatch、Semaphore等)、线程池等相关知识点。 2. JVM与GC算法:了解Java虚拟机(JVM)的基本原理、内存结构和内存模型,理解垃圾回收(GC)算法的原理和常见的垃圾回收器(如Serial、Parallel、CMS、G1等),掌握GC调优的一般方法。 3. 分布式架构和并发编程模型:认识分布式系统的基本概念、CAP定理、分布式存储和分布式计算的方案,了解常见的并发编程模型(如Actor模型、异步编程等)和实现方式。 4. 高性能网络编程:熟悉Java NIO的基本原理、底层实现和使用方式,了解Java网络编程的相关API和概念(如Socket、ServerSocket、Selector等),了解基于Netty框架的高性能网络编程。 5. 分布式消息队列和中间件:了解消息队列的基本概念、常见的消息中间件(如RabbitMQ、Kafka等)的特点和使用场景,理解消息队列的高可用、持久化、消息顺序等特性。 6. 微服务和服务治理:理解微服务的概念、优劣势和架构特点,了解微服务的拆分和组织原则,熟悉常见的服务治理框架(如Spring Cloud、Dubbo等)和相关的技术组件。 7. 高可用和容灾设计:掌握高可用架构的设计原则和常见的高可用技术方案(如集群、负载均衡、故障切换等),了解容灾方案的设计和实现方法,学习如何保证系统的可靠性和稳定性。 8. 性能优化与调优:了解性能优化的基本思路和方法,熟悉性能调优的一般流程和工具,掌握常见的性能调优技术(如缓存、异步、批处理等)和优化手段。 以上就是对于"Java高级架构面试知识点整理.pdf"文档的简要回答,希望对您有所帮助。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值