一、概念
1. 产生背景与技术起源
Kafka 最初由 LinkedIn 公司基于 Scala 语言开发,2010 年贡献给 Apache 基金会并成为顶级开源项目。在大数据时代,商业、社交、搜索等应用系统持续产生海量信息,由此催生三大核心挑战:高效信息收集、深度数据分析 以及 实时响应处理。为满足生产者与消费者间的消息传递需求,Kafka 应运而生,作为高吞吐量的分布式消息系统,实现了生产 - 消费两端的无缝衔接。
2. 核心特性
- 极致性能:具备每秒处理数十万条消息的超高吞吐量,延迟最低仅几毫秒,满足实时数据处理需求。
- 灵活扩展:支持集群热扩展,可根据业务增长动态调整资源。
- 可靠持久:消息持久化至本地磁盘,并通过数据备份机制保障数据安全,避免丢失。
- 强大容错:允许集群内节点故障,若副本数为 n,可容忍 n-1 个节点失效。
- 高并发支持:能同时承载数千客户端的读写操作,适应大规模数据交互场景。
3. 典型应用场景
- 日志收集中枢:统一归集企业内各类服务日志,以标准化接口提供给 Hadoop、HBase、Solr 等系统进行分析处理。
- 消息系统基石:实现生产者与消费者解耦,缓存消息以平衡上下游处理节奏。
- 用户行为追踪:捕获 Web 或 App 用户的浏览、搜索等行为数据,支持实时监控分析或离线深度挖掘。
- 运营指标监控:汇集分布式应用数据,生成操作反馈,辅助系统报警与报告生成。
- 流式处理引擎:与 Spark Streaming、Storm 等流式计算框架集成,驱动实时数据处理。
- 事件溯源核心:记录系统关键事件,为业务追溯与分析提供完整数据链。
4. 关键设计理念
- Consumer Group(消费者组):多个消费者可组成群组,同一消息仅能被组内一个消费者处理;如需多消费者处理同一消息,需分属不同群组。
- 消息状态管理:Kafka 将消息消费状态存储于消费者端,Broker 仅记录 offset(分区内下一条待消费消息的位置),若消费者处理不当,可能导致消息重复消费。
- 持久化存储:消息高效持久化至本地文件系统,兼顾存储可靠性与读写性能。
- 消息生命周期:支持长期保留消息,消费策略与留存时长可灵活配置。
- 批量发送机制:允许以消息集合为单位批量推送,提升数据传输效率。
- 异步交互模式:Producer 采用异步推送(支持同步 / 异步参数配置),Consumer 主动拉取消息,实现生产 - 消费异步解耦。
- 集群架构设计:Broker 节点地位对等,无主从之分,支持节点动态增删;0.8.x 版本通过 metadata API 实现负载均衡,0.7.x 依赖 Zookeeper 管理负载。
- 分区与顺序性:Broker 端支持消息分区,Producer 可指定分区;同一分区内消息严格按生产顺序存储,通过分区数量配置优化系统扩展性与吞吐量。
- 生态集成能力:活跃的社区生态衍生出众多插件,无缝对接 Storm、Hadoop、Flume 等技术栈,拓展应用边界。
二、消息队列通信的模式
1. 点对点模式
点对点模式是一种基于拉取或轮询的消息传递模型,其核心特性在于:发送至队列的每条消息仅能被一个消费者处理,确保消息处理的唯一性与不可重复性。在此模式下,生产者将消息发布到队列后,消费者需主动发起请求从队列中拉取消息进行消费,形成 “生产 - 存储 - 主动获取” 的流程闭环。
该模式的显著优势在于消费自主性:消费者可灵活控制消息拉取频率,根据自身处理能力与系统负载动态调整获取节奏,避免因消息推送过快导致处理过载。然而,其弊端也较为明显:消费者无法实时感知队列中是否存在待处理消息,因此需额外部署监控线程或采用轮询机制,持续检查队列状态以触发拉取操作。这种设计虽然赋予消费者主动权,但也增加了系统的复杂性与资源消耗,需在灵活性与监控成本之间进行权衡。
2. 发布订阅模式
发布订阅模式是一种基于消息的通信模型,支持多个不同消费者同时订阅同一类消息。生产者将消息发送至消息队列后,队列会主动推送给所有订阅该主题的消费者,类似于微信公众号的信息推送机制。在这种模式下,消费者被动接收消息,无需主动轮询队列状态,简化了消息获取流程。
然而,该模式存在显著的性能适配问题:由于不同消费者的机器性能、处理逻辑复杂度存在差异,导致消息处理能力各不相同。但消息队列通常无法动态感知消费者的处理速度,容易造成推送速度与消费能力不匹配。例如,假设三个消费者的处理速度分别为 8M/s、5M/s、2M/s:
- 若队列以 5M/s 的速度推送消息,处理能力仅 2M/s 的 consumer3 将因处理过载而出现消息积压,甚至可能导致系统崩溃;
- 若队列以 2M/s 的速度推送消息,尽管 consumer3 能够正常处理,但 consumer1 和 consumer2 的处理能力却被严重闲置,造成资源浪费。
这种速度不匹配的问题,使得发布订阅模式在实际应用中需要额外的流量控制、背压策略或动态调整机制,以平衡消息推送与消费效率,保障系统稳定性。
3. Java 客户端示例
3.1. 生产者示例
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.clients.producer.Callback;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
public class SimpleKafkaProducer {
public static void main(String[] args) {
// 设置生产者配置属性
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092"); // Kafka broker地址
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// 创建生产者实例
try (KafkaProducer<String, String> producer = new KafkaProducer<>(props)) {
// 构造消息记录
ProducerRecord<String, String> record = new ProducerRecord<>("my-topic-name", "key", "Hello, Kafka!");
// 发送消息并等待结果(同步)
try {
RecordMetadata metadata = producer.send(record).get();
System.out.printf("Message sent to topic:%s partition:%d offset:%d%n",
metadata.topic(), metadata.partition(), metadata.offset());
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
// 或者异步发送消息并提供回调函数
producer.send(record, new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception == null) {
System.out.printf("Message sent to topic:%s partition:%d offset:%d%n",
metadata.topic(), metadata.partition(), metadata.offset());
} else {
exception.printStackTrace();
}
}
});
// 强制发送所有消息并关闭生产者
producer.flush();
}
}
}
3.2. 消费者示例
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import java.time.Duration;
import java.util.Collections;
import java.util.Properties;
public class SimpleKafkaConsumer {
public static void main(String[] args) {
// 设置消费者配置属性
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092"); // Kafka broker地址
props.put("group.id", "test-group"); // 消费者组ID
props.put("enable.auto.commit", "true"); // 自动提交偏移量
props.put("auto.commit.interval.ms", "1000");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// 创建消费者实例
try (KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props)) {
// 订阅主题
consumer.subscribe(Collections.singletonList("my-topic-name"));
// 开始循环消费消息
while (true) {
// 拉取消息
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
// 遍历记录集中的每条记录
for (ConsumerRecord<String, String> record : records) {
System.out.printf("offset = %d, key = %s, value = %s%n",
record.offset(), record.key(), record.value());
}
}
}
}
}
三、Kafka的架构原理
Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者规模的网站中的所有动作流数据,具有高性能、持久化、多副本备份、横向扩展能力。
1. 基础架构与名词解释
- Producer(生产者):消息的创建与发布者,作为消息进入 Kafka 集群的入口,负责将业务数据封装为消息并发送至指定 Topic。
- Consumer(消费者):消息的订阅与处理方,从 Kafka 集群拉取消息进行业务逻辑处理,是消息的最终出口。
- Broker(代理):Kafka 集群中的服务器节点,每个节点运行一个或多个 Kafka 实例。集群内各 Broker 通过唯一编号区分(如 broker-0、broker-1),共同承担消息存储与转发任务。
- Topic(主题):消息的逻辑分类单元,类似于传统消息队列中的队列,用于组织和管理消息。每个 Broker 均可创建多个 Topic,实现不同业务数据的隔离存储。
- Partition(分区):Topic 的物理子单位,通过将 Topic 数据分散到多个分区,实现消息的并行处理与负载均衡,显著提升 Kafka 的吞吐量。每个分区在磁盘上表现为独立文件夹,同一 Topic 的不同分区数据互不重复。
- Replica(副本Replication):分区的数据副本,用于保障数据冗余与高可用性。每个分区包含一个 Leader 副本和多个 Follower 副本:Leader 负责处理读写请求,Follower 实时同步 Leader 数据;当 Leader 故障时,Follower 将被选举为新的 Leader。副本数量上限为 10,且必须小于 Broker 总数,同一节点不会同时存在同一分区的多个副本。
- Zookeeper:Kafka 集群的元数据与状态管理中枢,负责存储集群拓扑、Broker 信息、Topic 配置等关键数据,并协调 Leader 选举、副本同步等核心操作,确保集群的稳定性与可用性。
- Consumer Group(消费者组):多个消费者的逻辑集合,同一分区的消息仅能被组内一个消费者消费,避免重复处理;不同消费者可处理同一 Topic 的不同分区数据,进一步提升消息消费并行度。
- Message(消息):Kafka 中数据传输的基本单元,包含消息主体内容及元数据(如 Topic、分区、偏移量等),是生产者与消费者之间传递的核心对象。
2. kafka基础知识
2.1. Topic 主题
kafka 学习了数据库里面的设计,在里面设计了topic(主题),这个东西类似于关系型数据库的表。
2.2. Partition 分区
kafka还有一个概念叫Partition(分区),分区具体在服务器上面表现起初就是一个目录,一个主题下面有多个分区,这些分区会存储到不同的服务器上面,或者说,其实就是在不同的主机上建了不同的目录。这些分区主要的信息就存在了.log文件里面。跟数据库里面的分区差不多,是为了提高性能。
至于为什么提高了性能,很简单,多个分区多个线程,多个线程并行处理肯定会比单线程好得多
Topic 和 partition 像是 HBASE 里的 table 和 region 的概念,table 只是一个逻辑上的概念,真正存储数据的是 region,这些 region 会分布式地存储在各个服务器上面,对应于kafka,也是一样,Topic 也是逻辑概念,而 partition 就是分布式存储单元。
这个设计是保证了海量数据处理的基础。我们可以对比一下,如果 HDFS 没有 block 的设计,一个 100T 的文件也只能单独放在一个服务器上面,那就直接占满整个服务器了,引入 block后,大文件可以分散存储在不同的服务器上。
注意:
- 分区会有单点故障问题,所以我们会为每个分区设置副本数
- 分区的编号是从0开始的
2.3. Producer 生产者
往消息系统里面发送数据的就是生产者
2.4. Consumer 消费者
从 kafka 里读取数据的就是消费者
2.5. Message 消息
kafka 里面的我们处理的数据叫做消息
2.6. kafka的消息存储和生产消费模型
(1)一个topic分成多个partition
(2)每个partition内部消息强有序,其中的每个消息都有一个序号叫offset
(3)一个partition只对应一个broker,一个broker可以管多个partition
(4)消息不经过内存缓冲,直接写入文件,根据时间策略删除,而不是消费完就删除
(5)producer自己决定往哪个partition写消息,可以是(默认)轮询的负载均衡(分布式,一条一条写),或者是基于hash的partition策略(可能数据倾斜)
3. kafka的集群架构
创建一个 TopicA 的主题,3个分区分别存储在不同的服务器,也就是 broker 下面。Topic 是一个逻辑上的概念,并不能直接在图中把 Topic 的相关单元画出
需要注意:kafka在0.8版本以前是没有副本机制的,所以在面对服务器宕机的突发情况时会丢失数据,所以尽量避免使用这个版本之前的kafka
3.1. Replica 副本
kafka 中的 partition 为了保证数据安全,所以每个 partition 可以设置多个副本。
此时我们对分区 0,1,2 分别设置 3 个副本(其实设置两个副本是比较合适的)
而且其实每个副本都是有角色之分的,它们会选取一个副本作为 leader,而其余的作为follower,我们的生产者在发送数据的时候,是直接发送到 leader partition 里面,然后follower partition 会去 leader 那里自行同步数据,消费者消费数据的时候,也是从leader那去消费数据的。
3.2. Consumer Group 消费者组
我们在消费数据时会在代码里面指定一个 group.id,这个 id 代表的是消费组的名字,而且这个 group.id 就算不设置,系统也会默认设置。
conf.setProperty("group.id","tellYourDream")
我们所熟知的一些消息系统一般来说会这样设计,就是只要有一个消费者去消费了消息系统里面的数据,那么其余所有的消费者都不能再去消费这个数据。可是 kafka 并不是这样,比如现在 consumerA 去消费了一个 topicA 里面的数据。
consumerA:
group.id = a
consumerB:
group.id = a
consumerC:
group.id = b
consumerD:
group.id = b
再让 consumerB 也去消费 TopicA 的数据,它是消费不到了,但是我们在 consumerC中重新指定一个另外的 group.id,consumerC 是可以消费到 topicA 的数据的。而consumerD 也是消费不到的,所以在 kafka 中,不同组可有唯一的一个消费者去消费同一主题的数据。
所以消费者组就是让多个消费者并行消费信息而存在的,而且它们不会消费到同一个消息,如下,consumerA,B,C是不会互相干扰的
consumer group:a
consumerA
consumerB
consumerC
如图,因为前面提到过了消费者会直接和leader建立联系,所以它们分别消费了三个leader,所以一个分区不会让消费者组里面的多个消费者去消费,但是在消费者不饱和的情况下,一个消费者是可以去消费多个分区的数据的。
3.3. Controller
熟知一个规律:在大数据分布式文件系统里面,95%的都是主从式的架构,个别是对等式的架构,比如 ElasticSearch。
kafka也是主从式的架构,主节点就叫controller,其余的为从节点,controller是需要和zookeeper 进行配合管理整个kafka集群。
3.4. kafka和zookeeper如何配合工作
kafka严重依赖于zookeeper集群(所以之前的zookeeper文章还是有点用的)。所有的broker在启动的时候都会往zookeeper进行注册,目的就是选举出一个controller,这个选举过程非常简单粗暴,就是一个谁先谁当的过程,不涉及什么算法问题。
那成为controller之后要做啥呢,它会监听zookeeper里面的多个目录,例如有一个目录/brokers/,其他从节点往这个目录上注册(就是往这个目录上创建属于自己的子目录而已)自己,这时命名规则一般是它们的id编号,比如/brokers/0,1,2
注册时各个节点必定会暴露自己的主机名,端口号等等的信息,此时controller就要去读取注册上来的从节点的数据(通过监听机制),生成集群的元数据信息,之后把这些信息都分发给其他的服务器,让其他服务器能感知到集群中其它成员的存在。
此时模拟一个场景,我们创建一个主题(其实就是在zookeeper上/topics/topicA这样创建一个目录而已),kafka会把分区方案生成在这个目录中,此时controller就监听到了这一改变,它会去同步这个目录的元信息,然后同样下放给它的从节点,通过这个方法让整个集群都得知这个分区方案,此时从节点就各自创建好目录等待创建分区副本即可。这也是整个集群的管理机制。
4. 工作流程分析
4.1. 发送数据
我们看上面的架构图中,producer就是生产者,是数据的入口。注意看图中的红色箭头,Producer在写入数据的时候永远的找leader,不会直接将数据写入follower!那leader怎么找呢?写入的流程又是什么样的呢?
发送的流程就在图中已经说明了,就不单独在文字列出来了!需要注意的一点是,消息写入leader后,follower是主动的去leader进行同步的!producer采用push模式将数据发布到broker,每条消息追加到分区中,顺序写入磁盘,所以保证同一分区内的数据是有序的!写入示意图如下:
上面说到数据会写入到不同的分区,那kafka为什么要做分区呢
Kafka 分区的核心作用
- 灵活水平扩展:通过将一个 Topic 划分为多个 Partition,Kafka 能够轻松应对数据量的持续增长。新增机器时,只需将新 Partition 分配至节点,即可实现存储与处理能力的线性扩展。
- 提升并发性能:以 Partition 为独立读写单元,支持多个消费者并行处理不同分区的数据,避免资源争抢,大幅提高消息处理效率。
熟悉负载均衡的朋友应该知道,当我们向某个服务器发送请求的时候,服务端可能会对请求做一个负载,将流量分发到不同的服务器,那在kafka中,如果某个topic有多个partition,producer又怎么知道该将数据发往哪个partition呢?
Producer 数据写入分区策略
Producer 发送消息时,可依据以下规则决定数据写入的 Partition:
- 指定分区写入:若明确指定目标 Partition,则消息直接写入对应分区。
- 基于 Key 哈希分配:未指定分区但设置消息 Key 时,Kafka 通过哈希算法计算 Key 值,将消息映射至对应分区,确保相同 Key 的消息始终写入同一分区,实现数据局部有序性。
- 轮询分配策略:当未指定分区且无 Key 时,Kafka 采用轮询机制,依次将消息写入各分区,实现负载均衡。
保证消息不丢失是一个消息队列中间件的基本保证,那producer在向kafka写入消息的时候,怎么保证消息不丢失呢?其实上面的写入流程图中有描述出来,那就是通过ACK应答机制!在生产者向队列写入数据的时候可以设置参数来确定是否确认kafka接收到数据,这个参数可设置的值为0、1、all。
消息可靠性保障:ACK 应答机制
Producer 向 Kafka 写入消息时,可通过配置acks
参数控制消息确认策略,确保消息不丢失:
- acks=0:Producer 发送消息后无需等待集群响应,立即继续发送下一条。此模式效率最高,但不保证消息发送成功,存在丢失风险,适用于允许少量数据丢失的场景。
- acks=1:Producer 在收到 Leader 节点对消息的确认后,即可发送下一条。仅确保消息在 Leader 分区写入成功,若 Leader 故障且 Follower 尚未同步,可能导致数据丢失。
- acks=all(或acks=-1):Producer 需等待所有 ISR(In-Sync Replicas,同步副本集)中的 Follower 完成从 Leader 的同步,才继续发送后续消息。此模式安全性最高,可保障消息在 Leader 与副本中均成功持久化,但因需等待多节点确认,性能相对较低。
最后要注意的是,如果往不存在的topic写数据,能不能写入成功呢?kafka会自动创建topic,分区和副本的数量根据默认配置都是1。
Topic 自动创建机制
当 Producer 向不存在的 Topic 写入数据时,Kafka 会自动创建该 Topic,并依据默认配置初始化分区数与副本数(通常均为 1)。此特性简化了 Topic 管理流程,但实际应用中建议提前规划 Topic 配置,避免因默认设置导致性能瓶颈或数据丢失风险。
4.2. 保存数据
Producer将数据写入kafka后,集群就需要对数据进行保存了!kafka将数据保存在磁盘,可能在我们的一般的认知里,写入磁盘是比较耗时的操作,不适合这种高并发的组件。Kafka初始会单独开辟一块磁盘空间,顺序写入数据(效率比随机写入高)。
4.2.1. Partition 结构
前面说过了每个topic都可以分为一个或多个partition,如果你觉得topic比较抽象,那partition就是比较具体的东西了!Partition在服务器上的表现形式就是一个一个的文件夹,每个partition的文件夹下面会有多组segment文件,每组segment文件又包含.index文件、.log文件、.timeindex文件(早期版本中没有)三个文件, log文件就实际是存储message的地方,而index和timeindex文件为索引文件,用于检索消息。
如上图,这个partition有三组segment文件,每个log文件的大小是一样的,但是存储的message数量是不一定相等的(每条的message大小不一致)。文件的命名是以该segment最小offset来命名的,如000.index存储offset为0~368795的消息,kafka就是利用分段+索引的方式来解决查找效率的问题。
4.2.2. Message结构
上面说到log文件就实际是存储message的地方,我们在producer往kafka写入的也是一条一条的message,那存储在log中的message是什么样子的呢?消息主要包含消息体、消息大小、offset、压缩类型……等等!我们重点需要知道的是下面三个:
- offset:offset是一个占8byte的有序id号,它可以唯一确定每条消息在parition内的位置!
- 消息大小:消息大小占用4byte,用于描述消息的大小。
- 消息体:消息体存放的是实际的消息数据(被压缩过),占用的空间根据具体的消息而不一样。
4.2.3. 存储策略
无论消息是否被消费,kafka都会保存所有的消息。那对于旧数据有什么删除策略呢?
- 基于时间,默认配置是168小时(7天)。
- 基于大小,默认配置是1073741824。
需要注意的是,kafka读取特定消息的时间复杂度是O(1),所以这里删除过期的文件并不会提高kafka的性能!
4.2.4. 消费数据
消息存储在log文件后,消费者就可以进行消费了。在讲消息队列通信的两种模式的时候讲到过点对点模式和发布订阅模式。Kafka采用的是发布订阅模式,消费者主动的去kafka集群拉取消息,与producer相同的是,消费者在拉取消息的时候也是找leader去拉取。
多个消费者可以组成一个消费者组(consumer group),每个消费者组都有一个组id!同一个消费组者的消费者可以消费同一topic下不同分区的数据,但是不会组内多个消费者消费同一分区的数据
图示是消费者组内的消费者小于partition数量的情况,所以会出现某个消费者消费多个partition数据的情况,消费的速度也就不及只处理一个partition的消费者的处理速度!如果是消费者组的消费者多于partition的数量,那会不会出现多个消费者消费同一个partition的数据呢?上面已经提到过不会出现这种情况!多出来的消费者不消费任何partition的数据。所以在实际的应用中,建议消费者组的consumer的数量与partition的数量一致!
在保存数据的小节里面,我们聊到了partition划分为多组segment,每个segment又包含.log、.index、.timeindex文件,存放的每条message包含offset、消息大小、消息体……我们多次提到segment和offset,查找消息的时候是怎么利用segment+offset配合查找的呢?假如现在需要查找一个offset为368801的message是什么样的过程呢?
- 定位 Segment 文件:通过二分查找算法,快速确定目标消息所在的
segment
文件。例如,经检索发现目标消息位于第二个segment
文件。 - 解析索引文件:打开对应
segment
的.index
索引文件(如368796.index
,其起始偏移量为368796
),计算目标消息在该文件内的相对偏移量(368801 - 368796 = 5
)。由于.index
采用稀疏索引存储相对偏移量与物理偏移量的映射关系,直接查找相对偏移量5
可能不存在,因此再次利用二分查找,找到小于或等于目标相对偏移量的最大索引条目(此处为相对偏移量4
)。 - 确定物理位置:根据找到的索引条目(相对偏移量
4
),获取对应消息的物理偏移位置(如256
)。随后打开数据文件,从该位置开始顺序扫描,直至定位到offset=368801
的目标消息。
该机制依赖offset
的有序性,通过 segment
文件切分、有序offset
编排、稀疏索引存储、二分查找加速 与 顺序扫描精确定位 的组合策略,实现高效数据检索。
消费位移管理演进
在 Kafka 中,消费者通过记录消费位移(即已消费消息的offset
)来维护消费进度,其管理方式经历两次重要迭代:
- 早期 Zookeeper 方案:旧版本中,消费者将消费位移存储于 Zookeeper,并定期上报。但该方式存在 重复消费风险(如上报延迟导致位移未及时更新)与 性能瓶颈(频繁读写 Zookeeper 集群)。
- 新版本内部 Topic 方案:当前版本引入
__consumer_offsets
内部 Topic,消费者直接将消费位移持久化至该 Topic。这种设计减少了对外部组件的依赖,降低重复消费概率,同时通过 Kafka 自身的高吞吐、低延迟特性,显著提升位移管理的性能与可靠性。
5. kafka的优势
5.1. kafka性能好的原因
- 顺序磁盘写入:采用顺序写盘策略,数据持续追加至磁盘文件末尾,避免传统随机写入导致的磁盘寻道开销。这种方式充分利用磁盘顺序读写速度优势,大幅提升数据写入效率,尤其适合大规模、高吞吐量的消息存储场景。
- 零拷贝技术(sendFile):通过 sendFile 系统调用,Kafka 实现数据在操作系统内核空间内的直接传输,绕过用户空间与内核空间之间的数据拷贝过程。该技术减少了 CPU 与内存资源消耗,降低数据传输延迟,显著提升消息发送与转发性能。
- 批量读写机制:采用批量处理策略,以固定大小(如 64KB)为单位聚合消息进行读写操作。批量操作减少了 I/O 请求次数与网络传输开销,降低系统资源占用,同时提升磁盘 I/O 与网络带宽利用率,进一步增强 Kafka 的吞吐能力。
5.2. Kafka性能好在什么地方?
顺序写
在数据读写过程中,操作系统对磁盘的寻址操作是性能关键。对于机械硬盘,随机读写需频繁定位数据物理位置,导致大量时间消耗在寻道上。
Kafka 采用顺序写磁盘策略:数据以追加方式写入文件末尾,无需频繁寻址。这种设计充分发挥磁盘顺序读写的高效性,在磁盘数量与转速固定的条件下,顺序写性能可与内存读写速度相当。相比之下,随机写因需在文件任意位置修改数据,频繁的磁头移动使得性能远低于顺序写,这正是 Kafka 在存储层实现高吞吐的重要原因。
零拷贝
先来看看非零拷贝的情况
传统数据传输过程(非零拷贝)存在多次数据拷贝与上下文切换:数据需先从内核空间拷贝到 Kafka 服务进程的用户空间,再从用户空间拷贝到 Socket 缓存区,最终发送至网络。这一过程不仅占用大量 CPU 资源,还因频繁的内存复制与进程切换产生高延迟。
Kafka 借助 Linux 的sendFile
技术(NIO)实现零拷贝优化:数据无需进入用户空间,直接在内核空间完成从文件读取到网络发送的全流程。该技术通过消除一次数据拷贝和多次进程切换,大幅降低 CPU 利用率与传输延迟,显著提升消息发送效率,尤其适用于大规模数据的快速传输场景。
5.3. 日志分段存储
Kafka规定了一个分区内的.log文件最大为1G,做这个限制目的是为了方便把.log加载到内存去操作
00000000000000000000.index00000000000000000000.log00000000000000000000.timeindex
00000000000005367851.index00000000000005367851.log00000000000005367851.timeindex
00000000000009936472.index00000000000009936472.log00000000000009936472.timeindex
5.4. Kafka的网络设计
kafka的网络设计和Kafka的调优有关,这也是为什么它能支持高并发的原因
首先客户端发送请求全部会先发送给一个Acceptor,broker里面会存在3个线程(默认是3个),这3个线程都是叫做processor,Acceptor不会对客户端的请求做任何的处理,直接封装成一个个socketChannel发送给这些processor形成一个队列,发送的方式是轮询,就是先给第一个processor发送,然后再给第二个,第三个,然后又回到第一个。消费者线程去消费这些socketChannel时,会获取一个个request请求,这些request请求中就会伴随着数据。
线程池里面默认有8个线程,这些线程是用来处理request的,解析请求,如果request是写请求,就写到磁盘里。读的话返回结果。processor会从response中读取响应数据,然后再返回给客户端。这就是Kafka的网络三层架构。
所以如果我们需要对kafka进行增强调优,增加processor并增加线程池里面的处理线程,就可以达到效果。request和response那一块部分其实就是起到了一个缓存的效果,是考虑到processor们生成请求太快,线程数不够不能及时处理的问题。
所以这就是一个加强版的reactor网络线程模型。
5.5. Kafka 通过以下机制来保证数据不丢失:
- 副本机制:Kafka 的数据副本机制是保障数据可靠性的基础。即便在没有副本的情况下,只要数据在正常流程中被成功保存到磁盘,就不会丢失。不过,为了进一步增强数据的安全性,Kafka 会为每个分区创建多个副本。
- ACKS 参数设置:当 Kafka 使用副本机制时,为了确保数据在副本间的一致性和完整性,生产者可以通过设置
acks
参数来控制消息确认机制。当生产者发送消息时,若设置acks
为all
(或-1
),意味着生产者会等待所有处于同步状态的副本(ISR,In - Sync Replicas)都成功接收并写入消息后,才会收到集群的确认信息,然后继续发送下一条消息。这样,即使在某个副本写入失败或某个节点出现故障的情况下,由于其他同步副本已经成功保存了数据,也能保证数据不会丢失。
5.6. kafka使用场景
- 日志收集:收集各种系统和应用的日志数据,统一存储和管理,方便后续的查询和分析。
- 消息队列:作为消息队列系统,实现应用程序之间的异步通信和解耦,提高系统的可扩展性和可靠性。
- 实时流处理:对实时产生的消息进行处理和分析,如实时监控、实时推荐、实时报警等。
- 数据集成:在不同的系统和数据源之间进行数据集成和传输,实现数据的共享和交换。
四、知识点
1. 数据读写、数据同步
1.1. 数据写入过程
消息首先写入分区 Leader 的内存 PageCache,再由 flusher 线程将数据刷盘至.log
日志文件。刷盘策略包括:
- Linux 脏页刷盘:当 PageCache 中的数据标记为
dirty
(写入新数据时标记),且达到系统设定的时间阈值后,操作系统自动触发刷盘,刷盘完成后清除dirty
标记,此为推荐方式。 - Kafka 主动刷盘:通过配置参数控制:
配置参数 | 默认值 | 作用 |
log.flush.interval.messages | Long.MaxValue | 每累积 N 条消息触发一次刷盘,默认不主动按消息数刷盘 |
log.flush.interval.ms | null | 每隔 N 毫秒触发一次刷盘,默认不主动按时间刷盘 |
1.2. 数据同步机制
Leader 节点将日志写入磁盘后,向 Follower 副本推送数据。当所有 Follower 返回 ACK 确认后,更新 HW(High Watermark,高水位),此时消息被标记为已提交(Committed)。
1.3. 数据丢失风险与 ACK 机制
生产者通过acks
参数控制消息确认策略,不同配置下的数据丢失风险:
- acks=0:生产者发送消息后无需等待 Leader 确认,消息极易丢失。
- acks=1:仅需 Leader 确认,若 Leader 写入 PageCache 后未刷盘即宕机,且尚未同步至 Follower,会导致数据丢失。
- acks=-1(或 all):等待所有 ISR 副本确认,数据安全性最高,但性能开销较大。
1.4. 数据丢失预防策略
- acks=-1:强制等待所有 ISR 副本确认。
- min.insync.replicas=2:将 ISR(同步副本集)最小副本数设为 2,确保至少两个副本同步数据。
- unclean.leader.election=false:禁止从不健康副本(OSR)中选举新 Leader,避免数据不一致。
2. 消息分区策略
2.1. 分区目的与压力分散
生产者将消息写入主分区,若主分区集中在单个 Broker,会导致负载不均。Kafka 提供多种分区策略分散压力:
- 轮询机制(默认):按顺序依次将消息写入不同分区。
- hash(key):根据消息中的 key 计算哈希值,相同 key 的消息写入同一分区;无 key 时退化为轮询。
2.2. 粘性分区(Sticky Partitioner)
2.2.1. 核心概念
Batch(批次)是生产者将多条消息打包发送至 Broker 的机制,具有以下优势:
- 减少网络开销:批量发送降低请求次数。
- 提升磁盘效率:支持 Broker 顺序写入日志文件。
- 增强压缩性能:整批消息可统一压缩(如 gzip、snappy、lz4)。
2.2.2. 配置参数
参数 | 默认值 | 作用 |
batch.size | 16 KB | 单个 Batch 的最大字节数,达到该值或超时(linger.ms)即发送 |
linger.ms | 0 | 生产者等待消息入 Batch 的最长时间,调大可提升吞吐但增加延迟 |
max.request.size | 1 MB | 限制单个请求大小,约束 Batch 总容量 |
compression.type | none | 消息压缩算法,可选 gzip、snappy、lz4 等 |
2.2.3. 工作原理
- 初始分区选择:首次发送时随机选定一个分区(如 P1)。
- 持续粘性写入:后续消息优先写入同一分区,直至:
-
- Batch 达到
batch.size
上限。 - 等待时间超过
linger.ms
阈值。 - 分区因 Leader 切换等原因不可用。
- Batch 达到
2.3. 自定义分区
开发者可通过实现partitioner
接口,根据业务逻辑自定义消息分区规则,灵活适配复杂场景需求。
3. Kafka 选举机制
3.1. 控制器 Broker 选举
在 Kafka 集群中,控制器 Broker 承担着多项关键职责:
- 主题管理:控制器负责 Topic 的创建、删除操作,以及增加 Topic 分区等任务。这些操作是 Kafka 主题生命周期管理的核心,确保主题配置的一致性和正确性。
- 分区重分配:当执行 Kafka 的 reassign 脚本对 Topic 分区进行重分配时,控制器发挥着关键作用。当集群中 Broker 异常退出,若该 Broker 持有分区的副本 leader,控制器会遍历其他副本,选择新的 leader,并更新分区的 ISR(In-Sync Replicas,同步副本)集合。若有新的 Broker 加入集群,控制器会根据 Broker ID 判断其是否包含现有分区的副本,若有则触发数据同步,保证数据的完整性和可用性。
- Preferred leader 选举:在 Kafka 集群的运行过程中,Broker 的宕机或崩溃可能导致 leader 转移,引发 leader 不均衡问题,影响集群性能。Preferred leader 选举机制旨在将 leader 调整回最初的 Broker,确保集群负载均衡和性能优化。
- 集群成员管理:控制器通过监听 Zookeeper 中
/brokers/ids
下临时节点的变化,实时监控新 Broker 的加入、Broker 的主动关闭或被动宕机事件。基于这些监控信息,控制器对 Broker 中的 leader 节点进行动态调整,维护集群的稳定运行。 - 元数据服务:控制器存储着最全的集群元数据信息,并定期向其他 Broker 发送元数据更新请求,确保所有 Broker 内存中的缓存数据与集群元数据保持一致,支持高效的消息处理和数据存储。
3.2. 分区副本选举
分区副本选举在以下场景中触发:
- 创建主题:新主题创建时,需要选举分区的 leader 副本,以确保消息的有序处理和存储。
- 增加分区:当主题增加新的分区时,同样需要选举新分区的 leader 副本,实现分区数据的有效管理。
- 分区下线:若分区中原先的 leader 副本下线,分区需要选举新的 leader 上线,保证分区的持续服务能力。
- 分区重分配:在进行分区重分配操作时,会触发 leader 副本的选举,确保分区重分配过程中数据的一致性和可用性。
分区 leader 副本的选举由 Kafka 控制器 Broker 负责实施,具体过程如下:
- 读取 ISR 集合:从 Zookeeper 中获取当前分区的所有 ISR 集合,作为选举的基础数据。
- 选择 leader:调用配置的分区选择算法,从 ISR 集合中选择合适的副本作为新的 leader,保证选举结果的合理性和高效性。
4. Rebalance 与负载均衡
4.1. Rebalance 机制详解
定义与作用:Rebalance 本质上是 Kafka 中 Consumer Group 内部的协调协议,用于确定组内所有消费者实例如何分配订阅 Topic 的分区。例如,当一个 Consumer Group 包含 20 个消费者实例,且订阅了一个拥有 100 个分区的 Topic 时,Rebalance 会将每个消费者平均分配 5 个分区,确保消费任务均匀分布。
触发条件:Rebalance 由以下三种场景触发:
- 组成员变更:新消费者实例加入或现有实例离开消费组;
- 订阅 Topic 数量变化:消费组新增或减少订阅的 Topic;
- 分区数量调整:已订阅 Topic 的分区数量发生增减。
影响与弊端:Rebalance 过程需组内所有消费者实例协同参与,虽然能实现分区的公平分配,但会导致整个 Consumer Group 短暂不可用。在此期间,所有消费者暂停消息消费,直至 Rebalance 完成。
执行流程:Rebalance 分两个阶段完成:
- JoinGroup 阶段:消费者实例向组内协调者发送 JoinGroup 请求,上报自身订阅信息;协调者选举出 Leader 消费者,并等待其制定分区分配方案。
- SyncGroup 阶段:协调者将 Leader 制定的分配方案通过 SyncGroup 请求下发给所有消费者实例。当所有成员接收并确认方案后,Consumer Group 进入 Stable 状态,恢复正常消息消费。
4.2. 负载均衡实现原理
Kafka 的负载均衡依赖于生产者分区策略与消费者组协调机制的协同作用:
- 生产者分区策略:通过轮询、哈希(基于 key)、粘性分区或自定义策略,将消息均匀写入 Topic 的不同分区,避免单分区负载过高;
- 消费者组协调机制:利用 Rebalance 协议动态调整消费者与分区的映射关系,确保同一 Consumer Group 内的实例均衡处理分区数据,从而实现集群级别的负载均衡。
5. 幂等性与 Kafka 事务
5.1. 生产者幂等性
Kafka 通过PID(Producer_ID)与Sequence Number实现生产者幂等性:
- PID 分配:生产者初始化时,Kafka 为其分配唯一的 PID;
- Sequence Number 机制:每条消息携带从 0 开始递增的 Sequence Number。当生产者设置
acks=1
,若 Leader 返回的 ACK 因网络问题丢失导致重试,生产者会递增 Sequence Number。Broker 接收到消息后,若检测到相同 PID 与旧 Sequence Number,会直接返回 “ack-Duplicate”,避免重复写入,确保消息仅被处理一次。
5.2. 消费者幂等性
消费模式与重复消费风险:
消费者存在两种消费模式:
- 自动提交 offset(
enable.auto.commit=true
):消费进度自动提交; - 手动提交 offset(
enable.auto.commit=false
):需开发者手动控制提交时机。
重复消费常见原因包括:
- 异常中断:消费者线程被终止或系统宕机,导致 offset 未提交;
- 自动提交缺陷:自动提交模式下,关闭 Kafka 前调用
consumer.unsubscribe()
可能导致部分 offset 未提交; - 消费超时:因并发过高、处理耗时等导致消费超时(
session.time.out
),触发 Rebalance 后 offset 未同步,引发重复消费。
解决方案:
- 性能优化:提升单条消息处理速度(如异步处理、多线程);
- 参数调整:增大
session.time.out
与max.poll.interval.ms
,减小max.poll.records
,降低 Rebalance 触发频率,但无法完全避免重复; - 业务去重:为消息添加唯一标识符(如主键 ID),利用数据库或 Redis 实现幂等写入;
- 事务与幂等结合:开启生产者幂等性,配合 Kafka 事务,通过全局唯一的
Transaction ID
确保跨会话的消息一致性。
5.3. Kafka 事务
配置示例:
生产者配置:
Properties props = new Properties();
props.put("bootstrap.servers", "kafka:9092");
props.put("transactional.id", "my-transactional-id"); // 必需,事务ID
props.put("enable.idempotence", "true"); // 开启幂等性
props.put("acks", "all"); // 要求所有副本确认
props.put("retries", Integer.MAX_VALUE);
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
消费者配置:
Properties props = new Properties();
props.put("bootstrap.servers", "kafka:9092");
props.put("group.id", "my-group");
props.put("isolation.level", "read_committed"); // 仅读取已提交消息
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
隔离级别:
- read_uncommitted(默认):允许读取所有消息,包括未提交的事务消息;
- read_committed:仅读取已提交的事务消息与非事务消息,避免脏读风险。
6. Kafka 消息发送流程
Kafka 生产者(Producer)在消息发送过程中,通过main 线程与sender 线程协同工作,具体流程如下:
- 消息预处理与缓存
main 线程负责创建RecordAccumulator双端队列,其中包含多个ProducerBatch批次容器(默认总内存 32MB,可配置)。消息依次经过拦截器(执行自定义逻辑)、序列化器(将消息转换为字节流)和分区器(确定目标分区)处理后,被存入 RecordAccumulator 队列。 - sender 线程批量发送
sender 线程持续从 RecordAccumulator 中拉取消息,按照以下策略组装批次并发送:
- 大小触发:当 ProducerBatch 的消息总大小达到
batch.size
阈值(默认 16KB)时,立即将批次发送至 Kafka 集群; - 时间触发:若未达到
batch.size
,则等待linger.ms
时长(默认 0ms),超时后强制发送当前批次。
- 请求管理与应答处理
sender 线程维护InflightRequests队列(默认允许 1-5 个未确认请求,可配置),用于追踪已发送但未确认的消息批次。Kafka 集群接收消息后,根据生产者配置的acks
参数进行应答:
- 成功应答:当所有副本同步完成(若
acks=all
),sender 线程从 InflightRequests 中移除对应批次,释放资源; - 失败重试:若发送失败或未收到预期应答,sender 线程将根据
retries
配置(默认重试 0 次,可设置为Integer.MAX_VALUE
)自动重新发送消息,确保数据可靠性。
7. 消费速率问题及 Kafka 消费配置解析
在生产业务场景中,Kafka 消费者大多采用 Pull 模式,通过 poll
方法进行轮询来获取消息,以此控制消费速率。
7.1. Poll 请求的工作原理
- 消费者发起请求:消费者调用
poll(Duration timeout)
方法,向 Kafka 集群发起消息拉取请求。 - 与 Broker 通信:消费者客户端向 Broker 发送
FetchRequest
,表明需要获取指定分区的消息。 - Broker 响应:Broker 收到请求后,根据相关配置决定是否返回消息。若满足条件,会返回
FetchResponse
,其中包含指定分区的消息。 - 消费者处理:消费者接收到响应后,获取消息集合
ConsumerRecords
,并对这些消息进行业务处理。
7.2. Kafka 相关配置参数
参数 | 作用对象 | 说明 |
fetch.max.wait.ms | Broker 响应行为 | 控制 Broker 在返回数据前等待的最长时间。即便未达到 的要求,一旦超过此时间,Broker 也会返回响应。 |
fetch.min.bytes | Broker 响应行为 | Broker 必须累积至少该参数指定字节数的数据,才会对 请求作出响应,避免频繁响应少量数据。 |
max.poll.records | 消费者客户端 | 限制单次 调用返回的最大消息条数,可根据消费者处理能力进行调整。 |
max.poll.interval.ms | 消费者客户端 | 规定消费者两次 |
8. Kafka 脑裂问题及解决方案
在 Kafka 集群中,当 Controller 节点发生故障时,集群会触发重新选举,产生新的 Controller。然而,原 Controller 可能因短暂故障恢复正常,此时若无法识别自身已被取代,将导致集群内出现 脑裂 现象,即存在多个 Controller 同时运行,引发元数据管理冲突。
Kafka 通过 epoch number(纪元编号 / 隔离令牌)机制有效规避该问题。epoch number 是一个单调递增的数值:首次选举 Controller 时,其值初始化为 1;每次重新选举后,新 Controller 的 epoch number 自动递增(如第二次选举后值为 2)。
具体实现逻辑如下:
- 编号获取:新选举出的 Controller 通过 Zookeeper 的条件递增操作,获取比之前更大的 epoch number,确保编号唯一性与时序性。
- 消息验证:Broker 接收到 Controller 发送的消息后,会检查其中携带的 epoch number。若消息的 epoch number 小于当前已知的最大编号,则判定为旧 Controller 的指令并直接忽略;仅处理来自拥有最新(最大)epoch number 的 Controller 消息,从而保证集群内元数据管理的一致性,避免脑裂引发的混乱。
9. Kafka 概述与核心价值
Kafka 是开源的高吞吐量分布式消息中间件,具备以下核心功能:
- 缓冲削峰:平滑上游突发流量,允许下游服务按自身节奏处理数据
- 系统解耦:分离业务流程,通过消息队列实现接口层解耦,提升系统扩展性
- 数据冗余:支持一对多消息分发,满足多业务场景数据复用需求
- 异步通信:提供异步处理机制,允许消息延迟处理
- 系统健壮:支持消息堆积,保障业务连续性
10. Kafka 高性能的技术支撑
- 顺序写磁盘:利用磁盘顺序读写特性,性能接近内存
- 零拷贝技术:通过 sendFile 减少数据拷贝,提升传输效率
- 批量处理机制:合并小请求,提高网络和磁盘利用率
- 分区并行处理:通过 Partition 实现多节点、多磁盘并行处理
- Page Cache 优化:利用操作系统分页存储提升 I/O 性能
- 数据压缩:支持 GZIP、Snappy 等压缩算法,减少网络传输开销
11. 核心架构与关键概念
概念 | 说明 |
Broker | Kafka 服务器实例,集群由多个 Broker 组成 |
Producer | 消息生产者,负责发送消息到 Kafka 集群 |
Consumer | 消息消费者,从 Kafka 集群获取消息进行处理 |
Topic | 消息主题,消息分类和管理的逻辑单元 |
Partition | Topic 的物理分区,实现数据并行处理和负载均衡 |
Replica | 分区副本,包括 Leader 和 Follower,保障数据冗余和高可用 |
Offset | 消息偏移量,唯一标识分区内每条消息的顺序 |
Consumer Group | 消费者组,实现消息广播或单播,同一分区消息只能被组内一个消费者消费 |
12. 重要技术概念解析
12.1. AR、ISR 与 OSR
- AR(Assigned Replicas):分区的所有副本集合
- ISR(In-Sync Replicas):与 Leader 保持同步的副本集合,用于选举新 Leader
- OSR(Out-of-Sync Replicas):与 Leader 同步滞后的副本集合,达到条件可重新加入 ISR
12.2. HW 与 LEO
- LEO(Log End Offset):日志文件中下一条待写入消息的偏移量
- HW(High Watermark):ISR 集合中最小的 LEO,消费者只能消费 HW 之前的消息
12.3. ISR 伸缩机制
通过 "isr-expiration" 和 "isr-change-propagation" 定时任务:
- 定期检测副本同步状态,动态调整 ISR 集合
- 满足条件时触发 ISR 集合变更,平衡数据可靠性与性能
AR:Assigned Replicas 指当前分区中的所有副本。
ISR:In-Sync Replicas 副本同步队列。ISR中包括Leader和Foller。如果Leader进程挂掉,会在ISR队列中选择一个服务作为新的Leader。有replica.lag.max.message(延迟条数)和replica.lag.time.max.ms(延迟时间)两个参数决定一台服务器是否可以加入ISR副本队列,在0.10版本之后移除了replica.lag.max.message(延迟条数)参数,防治服务频繁的进出队列。任意一个维度超过阈值都会把Follower踢出ISR,存入OSR(Outof-Sync Replicas)列表,新加入的Follower也会先存放在OSR中。
OSR:Out-of-Sync Replica非同步副本队列。与leader副本同步滞后过多的副本(不包括leader副本)组成OSR。如果OSR集合中有follower副本“追上”了leader副本,那么leader副本会把它从OSR集合转移至ISR集合。默认情况下,当leader副本发生故障时,只有在ISR集合中的副本才有资格被选举为新的leader,而在OSR集合中的副本则没有任何机会(不过这个原则也可以通过修改unclean.leader.election.enable参数配置来改变)。unclean.leader.election.enable 为true的话,意味着非ISR集合的broker 也可以参与选举,这样就有可能发生数据丢失和数据不一致的情况,Kafka的可靠性就会降低;而如果unclean.leader.election.enable参数设置为false,Kafka的可用性就会降低。
LEO (Log End Offset),标识当前日志文件中下一条待写入的消息的offset。上图中offset为9的位置即为当前日志文件的 LEO,LEO 的大小相当于当前日志分区中最后一条消息的offset值加1.分区 ISR 集合中的每个副本都会维护自身的 LEO ,而 ISR 集合中最小的 LEO 即为分区的 HW,对消费者而言只能消费 HW 之前的消息。
HW(High Watermark):replica高水印值,副本中最新一条已提交消息的位移。leader 的HW值也就是实际已提交消息的范围,每个replica都有HW值,但仅仅leader中的HW才能作为标示信息(读写都只能从leader入口,不能实现读写分离)。什么意思呢,就是说当按照参数标准成功完成消息备份(成功同步给follower replica后)才会更新HW的值,代表消息理论上已经不会丢失,可以认为“已提交”。
13. 数据同步与可靠性保证
13.1. 数据写入流程
- 消息写入 Leader 分区的 PageCache
- 通过刷盘机制写入磁盘日志文件
- 同步至 Follower 副本
- 收到所有 ISR 副本确认后更新 HW
13.2. 可靠性保障策略
- Broker 层面:通过副本机制和刷盘策略保障数据持久化
- Producer 层面:
-
- 幂等性:通过 PID 和 Sequence Number 避免重复写入
- 事务性:通过 Transaction ID 实现跨分区、跨会话的原子操作
- ACK 机制:通过 acks 参数控制消息确认策略
- Consumer 层面:
-
- 手动提交 offset 避免自动提交导致的数据丢失
- 幂等消费:通过业务去重或事务机制保证消息不重复处理
14. 负载均衡与分区策略
14.1. 生产者负载均衡
- 轮询策略:默认策略,按顺序分配消息到分区
- Hash 策略:根据消息 Key 的哈希值分配分区
- 粘性分区:尽量保持消息分配到同一分区,减少小批次传输
- 自定义分区:通过实现 Partitioner 接口定制分区逻辑
14.2. 消费者负载均衡
- Rebalance 机制:
-
- 触发条件:消费者组成员变化、订阅 Topic 变化、分区数变化
- 执行过程:JoinGroup 请求和 SyncGroup 请求
- 影响:Rebalance 期间消费者组不可用
- 分区分配策略:
-
- Range:按 Topic 分配,可能导致分区不均衡
- RoundRobin:轮询分配,需满足特定条件
- StickyAssignor:保持分配结果稳定性,实现更均衡的分配
15. 选举机制与脑裂处理
15.1. 选举机制
- 控制器选举:通过 Zookeeper 选举产生,负责集群元数据管理
- 分区副本选举:从 ISR 集合中选举新的 Leader
- 消费组选举:消费组内选举 Leader 负责 Rebalance 方案制定
15.2. 脑裂问题处理
通过 epoch number 机制:
- 每次选举产生递增的 epoch number
- Broker 根据 epoch number 判断 Controller 有效性,避免脑裂