kafka相关

113 阅读10分钟

kafka如何保证高可用

1、消息从producer可靠的发送至broker

保证Producer 发送消息后,能够收到来自 Broker 的消息保存成功 ack;

三种ACK策略:

  • Request.required.acks = 0:请求发送即认为成功,不关心有没有写成功,常用于日志进行分析场景。
  • Request.required.acks = 1:当 leader partition 写入成功以后,才算写入成功,有丢数据的可能。
  • Request.required.acks= -1:ISR 列表里面的所有副本都写完以后,这条消息才算写入成功,强可靠性保证。

为了实现强可靠的 kafka 系统,我们需要设置 Request.required.acks= -1,同时还会设置集群中处于正常同步状态的副本 follower 数量 min.insync.replicas>2,另外,设置 unclean.leader.election.enable=false 使得集群中 ISR 的 follower 才可变成新的 leader,避免特殊情况下消息截断的出现。

2、发送到Broker的消息可靠持久化

  • Broker 返回 Producer 成功 ack 时,消息是否已经落盘?

kafka 为了获得更高吞吐,Broker 接收到消息后只是将数据写入 PageCache 后便认为消息已写入成功,而 PageCache 中的数据通过 linux 的 flusher 程序进行异步刷盘(刷盘触发条:主动调用 sync 或 fsync 函数、可用内存低于阀值、dirty data 时间达到阀值),将数据顺序写到磁盘。消息处理示意图如下:

image.png

PageCache(页面缓存):

Linux内核用于缓存磁盘数据的一种内存机制,它作为文件系统与底层存储设备之间的桥梁,主要作用是平衡内存与物理磁盘之间的数据存取速率差异。

PageCache的工作原理:

  • 写入缓冲:当应用程序写入文件时,数据首先被写入PageCache中的"脏页"(dirty pages),而不是直接写入磁盘
  • 读取缓存:当读取文件时,内核会优先从PageCache中查找数据,若命中则无需访问磁盘
  • 异步回写:内核线程(pdflush)会定期将脏页写回磁盘,保证数据最终持久化

Kafka与PageCache的关系:

Kafka高度依赖PageCache来提升性能:

  • 写入路径:生产者发送的消息首先被追加到PageCache中的日志文件
  • 读取路径:消费者读取消息时,直接从PageCache获取,避免磁盘I/O
  • 刷盘策略:Kafka不依赖fsync强制刷盘,而是依靠操作系统异步刷盘机制

kafka认为消息错乱比消息丢失更为严重,所以允许消息丢失,但不允许消息错乱

首先来看两个kafka的概念:

  • HW: High Watermark,高水位,表示已经提交(commit)的最大日志偏移量,Kafka 中某条日志“已提交”的意思是 ISR 中所有节点都包含了此条日志,并且消费者只能消费 HW 之前的数据;

  • LEO: Log End Offset,表示当前 log 文件中下一条待写入消息的 offset;

image.png

下图A是主节点,B是从节点,如果B还没来的及拉取m2这条数据A就宕机,B则会被选为leader副本,然后B收到了一条新数据m3,此时A节点重启,重启后采用Leader Epoch思想(先与leader副本进行数据同步来确定如何处理当前副本的数据),与leader副本进行数据同步后发现offset 1的位置上的数据为m3,则会丢弃掉m2,记录m3

这种消息丢失,我认为只会出现在producer的 Request.required.acks = 0 或 1 时才会出现问题,如果-1的话不会出现,因为如果B没写入A就挂掉,producer应该会重试

image.png

但是即使Request.required.acks = -1,极端情况下主从副本全部写入pageCache,然后全部宕机,数据依然会丢失,且producer会收到写入成功的返回值

总结: Broker 接收到消息后只是将数据写入 PageCache 后便认为消息已写入成功,但是,通过副本机制并结合 ACK 策略可以大概率规避单机宕机带来的数据丢失问题,并通过 HW、副本同步机制、 Leader Epoch 等多种措施解决了多副本间数据同步一致性问题,最终实现了 Broker 数据的可靠持久化。

3、消费者从kafka消费消息

enable.auto.commit = true,消费者拉取数据后,会间隔一段时间(默认5s),自动提交偏移量,如果逻辑没有处理完成,消费者崩溃,未处理的消息就会丢失

image.png

如果想要精准消费,保证消息不丢失,应该使用手动提交偏移量的方式,auto.commit = false,而且消费逻辑需要支持幂等。

kafka高性能探究

零拷贝技术:

Kafka 中存在大量的网络数据持久化到磁盘(Producer 到 Broker)和磁盘文件通过网络发送(Broker 到 Consumer)的过程,这一过程的性能直接影响 Kafka 的整体吞吐量。传统的 IO 操作存在多次数据拷贝和上下文切换,性能比较低。Kafka 利用零拷贝技术提升上述过程性能,其中网络数据持久化磁盘主要用mmap技术,网络数据传输环节主要使用 sendfile 技术。

索引加速之 mmap

传统模式下,数据从网络传输到文件需要 4 次数据拷贝、4 次上下文切换和两次系统调用。如下图所示:

image.png

为了减少上下文切换以及数据拷贝带来的性能开销,Kafka使用mmap来处理其索引文件。Kafka中的索引文件用于在提取日志文件中的消息时进行高效查找。这些索引文件被维护为内存映射文件,这允许Kafka快速访问和搜索内存中的索引,从而加速在日志文件中定位消息的过程。mmap 将内核中读缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer)进行映射,从而实现内核缓冲区与应用程序内存的共享,省去了将数据从内核读缓冲区(read buffer)拷贝到用户缓冲区(user buffer)的过程,整个拷贝过程会发生 4 次上下文切换,1 次 CPU 拷贝和 2次 DMA 拷贝。

image.png

网络数据传输之 sendfile

传统方式实现:先读取磁盘、再用 socket 发送,实际也是进过四次 copy。如下图所示:

image.png

为了减少上下文切换以及数据拷贝带来的性能开销,Kafka 在 Consumer 从 Broker 读数据过程中使用了 sendfile 技术。具体在这里采用的方案是通过 NIO 的 transferTo/transferFrom 调用操作系统的 sendfile 实现零拷贝。总共发生 2 次内核数据拷贝、2 次上下文切换和一次系统调用,消除了 CPU 数据拷贝,如下:

image.png

Kafka为什么高吞吐量

1. 顺序读写 (Sequential I/O) - 最核心的因素

这是 Kafka 高性能的基石。无论是机械硬盘 (HDD) 还是固态硬盘 (SSD),顺序读写的速度都远高于随机读写(通常快几个数量级)。

  • 写入:Kafka 将所有即将到来的消息仅追加(Append-only)  到日志文件的末尾。这种操作是纯粹的顺序写入,最大限度地利用了磁盘的吞吐能力。
  • 读取:消费者按顺序从日志文件中读取消息,同样是高效的顺序读取。
  • 对比:传统消息队列在消息被消费后往往执行删除操作,会导致磁盘的随机读写,性能低下。Kafka 则通过保留策略(retention policy)来批量删除过期数据,也是顺序操作。

2. 页缓存 (Page Cache) 和零拷贝 (Zero-Copy) - 高效利用操作系统

Kafka 重度依赖操作系统内核的优化,而不是在 JVM 堆内存中维护大量缓存,这避免了 GC 开销并提高了效率。

  • 页缓存 (Page Cache)

    • 当 Kafka 写入数据时,它直接写入到操作系统的页缓存(内存的一部分),这是一个非常快的操作。
    • 当 Kafka 读取数据时,它会优先从页缓存中查找。如果数据还在缓存中,则直接返回,速度极快。
    • 操作系统负责在后台异步地将页缓存中的数据刷盘(flush)  到物理硬盘上。这样生产者和消费者大多数时间都在与内存速度的页缓存打交道,而不是直接与慢速的磁盘 I/O 交互。
  • 零拷贝 (Zero-Copy)

    • 传统的数据传输路径:硬盘 -> 内核缓冲区 -> 应用程序缓冲区 -> 套接字缓冲区 -> 网卡。这个过程涉及多次上下文切换和数据拷贝,CPU 开销大。

    • Kafka 使用 sendfile 系统调用实现零拷贝技术:

      • 路径变为:硬盘 -> 内核缓冲区 -> 网卡
      • CPU 不需要将数据从内核空间拷贝到用户空间(应用程序缓冲区) ,大大减少了上下文切换和不必要的数据拷贝,极大地提升了网络传输效率,降低了 CPU 占用。

3. 批处理 (Batching) - 化零为整

Kafka 在生产和消费两端都广泛使用批处理来分摊开销。

  • 生产者 (Producer) :生产者客户端并不是每条消息都发送一次,而是会将多条消息批量累积到一个记录批次(RecordBatch)中。当批次达到一定大小(batch.size)或等待时间(linger.ms)后,一次性发送到 Broker。
  • Broker:持久化时也是批量写入磁盘。
  • 消费者 (Consumer) :同样可以配置一次拉取一批消息进行处理。
  • 好处:将大量小的 I/O 操作合并为少量大的、顺序的 I/O 操作,显著减少了网络往返(Round-Trip)和磁盘寻道(Seek)的开销。

4. 高效的数据格式和压缩 (Compression)

  • 批处理基础上压缩:生产者可以对整个消息批次进行压缩(如 gzip, snappy, lz4, zstd),然后再发送到 Broker。由于批处理的数据量更大,压缩效率更高。

  • 好处

    1. 减少网络带宽:更小的数据包在网络中传输更快。
    2. 减少磁盘占用:写入磁盘的数据更少,I/O 压力更小。
    3. Broker 端不解压:Broker 接收和存储的是压缩后的批次,并在消费者拉取时直接发送(除非需要重新打包),避免了额外的 CPU 开销。

5. 分区和并行性 (Partitioning & Parallelism)

Kafka 通过主题分区(Topic Partitioning)  实现了水平的可扩展性和并行处理。

  • 写入并行:一个主题可以划分为多个分区,消息被均衡地写入不同分区。这些分区可以分布在不同的 Broker 上,允许多个 Broker 同时处理写入请求,磁盘 I/O 也是并行的。
  • 消费并行:每个分区只能被一个消费者组内的一个消费者消费。这意味着一个主题可以由多个消费者同时消费,吞吐量线性增长。
  • 分区是 Kafka 实现负载均衡横向扩展的核心机制。

6. 精简的消息格式和持久化模型

  • 简单的格式:Kafka 的消息格式设计得非常紧凑,减少了元数据开销。
  • 无删除开销:消息被消费后不会被立即删除,而是根据时间或大小配置批量删除。这避免了随机删除操作带来的性能抖动。
  • 索引优化:Kafka 为每个日志文件维护了稀疏索引(.index 和 .timeindex 文件),用于快速定位消息,而不是全局扫描。虽然需要查找,但索引很小,可以轻松加载到内存中,查找速度极快。