1. 几个重要概念#

Kafka 几个常见概念: Producer, Consumer, Broker, Topic, Partition:

  • 在 Kafka 中, 集群里的每一台服务器都被称为 Broker, 负责接收生产者发送的消息并存储这些消息

  • Topic 是 Kafka 中消息的逻辑分组, 生产者按照消息所属 Topic 将消息发送到 Broker, 消费者从 Broker 中读取消息

  • 每个 Topic 可以分成多个分区, 然后不同的分区可能会存储在不同的 Broker 上, 例如: user-clicks Topic 有 3 个分区, 可能分布在 2 个 Broker 上,Broker 1 存分区 0 和 1,Broker 2 存分区 2

注意 Broker 本身并不主动“分发”消息, 只负责存储消息并等待消费者主动拉取

分区(Partition)

  • 每个 Topic 被分成多个分区,分区是 Kafka 数据的基本存储单位
  • 例如,一个 Topic user-clicks 有 3 个分区:Partition 0、Partition 1、Partition 2

副本(Replica)

  • 每个分区可以有多个副本(由复制因子 replication-factor 决定),这些副本分布在不同的 Broker 上

  • 例如,replication-factor=2 表示每个分区有 2 个副本:一个主副本(Leader Replica)和一个从副本(Follower Replica)

在 Kafka 集群里,不是一台 Broker 扛所有活,而是多台 Broker 一起上。数据先按照 Topic 分类,每个 Topic 又被拆分成多个分区,这些分区会均匀分布到不同的 Broker 上。这样,生产者在写入数据、消费者在读取数据时,相关请求会自动分散到各个 Broker,从而实现负载均衡和高吞吐量

2. 异步处理#

异步处理是指任务的发起和执行不要求实时同步完成, 消息队列常用于异步处理, 生产者将消息发送到队列后无需等待消费者立即处理, 可以继续执行其他任务, 从而提高系统效率,

之前讨论高并发点赞的问题, 用到了 Redis Lua 脚本保证了 检查+更新 面临的数据一致性问题, 然后利用 Kafka 实现了异步更新到数据库, 这也算是消息队列异步处理常见的一个应用场景,

异步处理优点是解耦服务、提高响应速度和容错性;缺点可能是数据一致性难以保证(比如消费者处理失败),需要额外的错误处理机制(如重试或死信队列)

3. 消息持久化#

我觉得至少有几个问题需要先弄明白, 我们知道消息由生产者产生发送到 Broker, 然后 Broker 存储消息, 之后的事由 消费者主动消费(拉取)消息,

  • 消息默认存在哪个位置的?

  • 为什么需要消息持久化呢, 消息不就是为了消费的吗, 如果存在磁盘保存起来, 消费者频繁读取, 读取之后的消息应该视为无用的垃圾了吧, 难道还要分别删除, 这也太浪费性能了吧?

3.1. 消息默认存在哪个位置的?#

在 Kafka 中, 消息是按 Topic 归类的, 可以理解为一类消息放在同一个 Topic 中:

  • 每个 Topic 的每个分区对应一个日志文件(例如 topic-name-0.log)

  • 如果一个 Topic 只有 1 个分区,那么所有消息都会顺序追加到这个分区上

  • 如果一个 Topic 有多个分区,那么生产者会根据一定的策略(Key 的哈希或者轮询等)把消息分散到不同的分区

在 Kafka 的 Broker 上, 每个分区的底层都对应着一个日志文件, 这个日志并不是单独的一个物理文件,而是一系列 Segment 文件的有序集合, 形成一个“日志”概念:

  • 你可以把它理解成一个“顺序追加写”的结构, 每当生产者发送的消息抵达该分区时, Kafka 就将它顺序地追加到 Log 的尾部(对应 Segment 文件)
  • 为了方便维护和查询,Kafka 会把一个分区的 Log 拆分为多个 Segment 进行管理

3.2. 为什么是磁盘而不是内存?#

简单一句话为了保证可靠性, 消息队列和 Redis 缓存还不一样, 缓存的数据丢失可以在业务逻辑上通过双写一致性保证, 毕竟缓存的数据就是为了提高速度, 权威数据 Source of Truth 还是在数据库里的, 而消息队列不同, 消息丢了就是丢了, 因为它本身就是权威数据,

在消息被消费者消费之前, 如果 Broker 宕机, 内存中的消息会丢失, 而磁盘上的消息可以恢复,

  • Kafka 的设计目标是高吞吐量和高可靠性,直接把消息存在内存中虽然快,但内存容量有限,且宕机后数据会丢失
  • Kafka 通过顺序写入磁盘(而不是随机读写)和操作系统的页面缓存(Page Cache),让磁盘的性能接近内存的读写速度

现代磁盘(无论是机械硬盘还是 SSD)在处理顺序写入时, 性能表现通常远优于随机写入:

  • 当数据是顺序写入的时候, 磁盘连续地写入数据, 不需要频繁地更换写入位置, 避免了磁头寻道(或者在SSD中内部块寻址)的开销, 从而极大地提高了写入速度, 而随机写入要求磁盘在不同位置之间不断切换, 造成额外的延时和性能下降,
  • Kafka 采用的是追加写入的方式, 也就是每次都是在日志文件的末尾顺序写入数据, 从而避免了随机访问的开销

看完上面的不仅好奇 Kafka 还能控制操作系统的页面缓存吗? 这是怎么实现的?

3.2.1. 什么是页面缓存 (Page Cache)#

页面缓存是操作系统在内存中开辟出的一块区域, 用来临时存储从磁盘读取的数据, 或者即将写入磁盘的数据, 其主要作用有:

  • 读缓存: 当应用再次请求已经被缓存的数据时,可以直接从内存中获取数据,大大提高读取速度

  • 写缓存: 数据写入时,操作系统可以先将数据写入页面缓存,然后异步地将数据刷新(flush)到磁盘,这样应用看起来是直接在内存中写入数据

3.2.2. Kafka 怎么利用页面缓存#

Kafka 是一个分布式消息系统,它需要处理大量的消息读写。为了做到高吞吐量和高可靠性,Kafka 选择把消息持久化到磁盘上,而不是只保存在内存中。但磁盘很慢,怎么才能让性能接近内存呢?答案就是巧妙利用操作系统的页面缓存。

顺序写入磁盘

  • Kafka 写入数据时,不是随机跳来跳去地写(随机写很慢),而是按顺序追加到日志文件末尾
  • 顺序写入非常快,因为磁盘读写时不需要频繁移动磁头(对于机械硬盘)或者做复杂寻址(对于 SSD)
  • 写完之后,这些数据会被操作系统自动加载到页面缓存中
  • 顺序追加写入可以利用操作系统对顺序数据写入的优化, 这让操作系统更容易把数据缓存到页面缓存中

操作系统在管理页面缓存时, 更倾向于缓存连续的数据块, 顺序写入的数据通常会被预先加载到页面缓存中, 因为操作系统认为接下来很可能还会访问相邻的数据, 这样可以避免频繁地进行磁盘读写, 直接从内存中读取数据, 速度更快

依赖页面缓存加速读取

  • 当消费者(Consumer)来读取消息时,Kafka 不会直接去磁盘找数据,而是让操作系统从页面缓存里拿
  • 因为刚写入的数据还在页面缓存中(内存里),读取速度几乎和直接从内存读一样快
  • 如果数据不在缓存中,操作系统会从磁盘加载到页面缓存,然后再给 Kafka,这样后续的读取也能变快

尽量少干预缓存管理

Kafka 并不自己管理内存, 而是把缓存的工作交给操作系统, 操作系统的页面缓存机制已经很成熟, 能根据系统的内存使用情况自动调整哪些数据保留在缓存里, 哪些可以丢弃

3.3. 消息被消费后 会被立马删除吗#

Kafka 不会在每条消息写完后就跑去删除,而是 “以 Segment 为单位” 进行清理:

  1. 当 Segment 中所有消息都超过了保留时间(或者日志大小达到限制)时, 这个 Segment 文件就可以被删除
  2. Kafka 会周期性地检查每个分区日志中最早的 Segment 是否已经满足删除条件, 如果满足, 就把这个 Segment 文件(以及对应的 .index.timeindex 文件)从磁盘上删除

Consumer 读取消息的时候,只需要记录自己当前消费到哪个 offset 并提交, 即使这条消息还在 Kafka 的日志文件里, 它是否被删除是由上面提到的保留策略决定, 并不因为 Consumer 读到它就删除

3.4. 为什么不浪费性能#

  • **顺序写Append-only + 批量处理 Batch **:消费者不是一条条读,而是批量拉取消息(比如一次拉 1000 条),减少频繁交互, 最大化磁盘吞吐,减少网络请求、磁盘刷盘次数,提升写入与读取性能

  • 零拷贝技术:Kafka 使用操作系统的零拷贝(Zero Copy)机制,从磁盘到网络传输消息时不经过用户态,效率极高

  • 操作系统 Page Cache:通过操作系统缓存机制来提升文件的读写效率,减少实际磁盘 IO;配合零拷贝进一步提升传输性能

  • 自动清理:消息过期后,Kafka 后台线程自动删除老日志,不需要人工干预,也不需要消费者操心

什么是零拷贝(Zero Copy)?

通常情况下,当程序(比如 Kafka 的 Broker)需要把数据从磁盘发送到网络(比如给消费者),会涉及多次数据拷贝:

  1. 操作系统从磁盘读取数据,拷贝到内核态的缓冲区(Kernel Buffer)
  2. 从内核态缓冲区拷贝到用户态的应用程序内存(比如 Kafka 的进程内存)
  3. 应用程序处理完后,再把数据从用户态内存拷贝回内核态的网络缓冲区
  4. 最后从内核态网络缓冲区发送到网卡,传给消费者

这个过程涉及多次数据拷贝(通常至少 4 次),而且在用户态和内核态之间切换(上下文切换),会消耗 CPU 和时间,效率不高, 零拷贝的目标是:尽量减少这些拷贝步骤,尤其是用户态和内核态之间的来回拷贝。操作系统提供了一些技术(比如 sendfile 系统调用),让数据直接从磁盘(或内核缓冲区)传输到网卡,而不需要经过应用程序的内存,

3.5. 总结#

  • 生产者(Producer)发消息 -> Broker 收到后,找到 TopicA 的分区 Partition0 -> 将消息追加写入到对应的 .log 文件(假设是 /var/lib/kafka-logs/TopicA-0/00000000000000000000.log

  • 消费者(Consumer)读消息 -> 只要知道该消息所在的分区和 offset,就可以去读取对应分区的日志文件中该 offset 的位置(当然中间是由 Kafka 协调完成的), 读完后并不影响 Kafka 对消息的保留

  • Kafka 以 segment 为单位做分段 -> 当文件大小或时间到了,就滚动新文件

  • 日志清理 -> 当最早的一些 segment 的消息都过了保留时间(或超了预设总大小),则 Kafka 定时删除这些 segment 文件,消息也随之被物理删除

  • 结果 -> 消费与删除解耦,Consumer 消费并不决定是否删除数据;真正的删除取决于保留策略和 Log 目录的定期清理

4. 消费者负载均衡#

消费者负载均衡是指在消息队列系统中,当有多个消费者需要处理消息时,系统会自动将工作(消息)合理分配给这些消费者,确保每个消费者都能分担一部分任务,而不是让某个消费者超载或闲置。简单来说,就是“大家一起干活,分工要公平”, 在 Kafka 中,这种负载均衡是通过 Consumer Group 和 Partition 来实现的,

Consumer Group

  • 每个消费者都隶属于一个“消费者组”(比如 group.id=test_group
  • 在同一时间, 一个分区只能被消费者组中的一个消费者实例消费, 因为一个分区只会被一个消费者处理, 所以同一个消息不会被多个消费者重复消费, 保证了消息处理的唯一性

分区分配

  • 假设有个 Topic 有 4 个分区(P0, P1, P2, P3),消费者组里有 2 个消费者(C1, C2)
  • Kafka 会自动分配:比如 C1 处理 P0 和 P1,C2 处理 P2 和 P3
  • 这种分配是动态的,由 Kafka 的 Coordinator,一个 Broker 来决定

并行处理

  • C1 和 C2 各自从自己的分区读取消息并处理,互不干扰,这样可以同时处理更多消息,提高效率

再平衡(Rebalance)

  • 如果 C2 宕机了,Kafka 检测到消费者数量变化,会重新分配任务,让 C1 接管所有 4 个分区
  • 如果新增一个消费者 C3,Kafka 也会重新分配,比如:C1 处理 P0,C2 处理 P1,C3 处理 P2 和 P3

消费者组通过分工, 让每个消费者处理一部分分区, 实现并行消费, 同时, Kafka 保证每个分区只分配给一个消费者, 避免重复处理

消费者比分区多怎么办?

如果消费者数量超过分区数, 多余的消费者会闲置

Kafka 消费者组是如何实现负载均衡的?

说明消费者组通过 Coordinator(Kafka Broker 中的一个角色)管理分区分配, 当消费者加入或退出时触发再平衡, 再平衡时,所有消费者会暂停处理消息, 等分配完成再继续, 如果频繁发生, 可能会影响性能

如果消费者处理能力跟不上生产者怎么办?

可以增加消费者实例(提高并行度), 优化消费者处理逻辑, 或者调整分区数(更多分区支持更多并发), 还可以提到背压(Backpressure)问题, 必要时限流生产者