4. Kafka
1. 概述
官网:Apache Kafka
传统定义:Kafka是一个分布式的基于发布/订阅模式的消息队列(Message Queue),主要应用于大数据实时处理领域。
新定义: Kafka是一个开源的分布式事件流平台(Event Streaming Platform),被数千家公司用于高性能数据管道、流分析、数据集成和关键任务应用。
传统的消息队列的主要应用场景包括:缓存/消峰、解耦和异步通信。
-
缓冲/消峰:有助于控制和优化数据流经过系统的速度,解决生产消息和消费消息的处理速度不一致的情况。
-
允许独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。
-
允许用户把一个消息放入队列,并不立即处理它(不需要等待不那么重要的系统流程)。
两种常见的模式:
- 点对点模式:消费者主动拉取数据,消息收到后清除数据
- 发布/订阅模式:可以有多个topic主题;消费者消费数据之后,不删除数据;每个消费者相互独立,都可以消费到数据
基础架构
- 为了方便拓展&提高吞吐量,一个topic分为多个partition
- 配合分区的设计,突出消费者组的概念,组内每个消费者并行消费
- 为提高可用性,为每个partition增加若干个副本
- ZK中记录这Leader的信息(2.8.0版本以后可以不采用ZK)
工作示意图:

- Producer:消息生产者,就是向 Kafka broker 发消息的客户端。
- Consumer:消息消费者,向 Kafka broker 取消息的客户端。
- Consumer Group(CG):消费者组,由多个 consumer 组成。消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费;消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
- Broker:一台 Kafka 服务器就是一个 broker。一个集群由多个 broker 组成。一个 broker 可以容纳多个 topic。
- Topic:主题,生产者和消费者面向的都是一个 topic。
- Partition:为了实现扩展性,一个非常大的 topic 可以分布到多个 broker(即服务器)上,一个 topic 可以分为多个 partition,每个 partition 是一个有序的队列。
- Replica:副本。一个 topic 的每个分区都有若干个副本,一个 Leader 和若干个 Follower。
- Leader:每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数据的对象都是 Leader。
- Follower:每个分区多个副本中的“从”,实时从 Leader 中同步数据,保持和 Leader 数据的同步。Leader 发生故障时,某个 Follower 会成为新的 Leader。
安装
清华镜像:Index of /apache/kafka (tsinghua.edu.cn)
# 解压
tar -zxvf kafka_2.13-3.6.0.tgz -C /opt/app/
# 修改名称
mv kafka_2.13-3.6.0/ kafka
# 修改配置文件
vim kafka/config/server.properties
# broker 的全局唯一编号,不能重复,只能是数字。
broker.id=0
# 处理网络请求的线程数量
num.network.threads=3
# 用来处理磁盘 IO 的线程数量
num.io.threads=8
# 发送套接字的缓冲区大小
socket.send.buffer.bytes=102400
# 接收套接字的缓冲区大小
socket.receive.buffer.bytes=102400
# 请求套接字的缓冲区大小
socket.request.max.bytes=104857600
# kafka 运行日志(数据)存放的路径,路径不需要提前创建,kafka 自动帮你创建
# 可以配置多个磁盘路径,路径与路径之间可以用","分隔
log.dirs=/tmp/kafka-logs
# topic 在当前 broker 上的分区个数
num.partitions=1
# 用来恢复和清理 data 下数据的线程数量
num.recovery.threads.per.data.dir=1
# 每个topic创建时的副本数,默认时 1 个副本
offsets.topic.replication.factor=1
# segment文件保留的最长时间,超时将被删除
log.retention.hours=168
# 每个 segment 文件的大小,默认最大 1G
log.segment.bytes=1073741824
# 检查过期数据的时间,默认 5 分钟检查一次是否数据过期
log.retention.check.interval.ms=300000
# 配置连接 Zookeeper 地址(在 zk 根目录下创建/kafka,方便管理,集群使用,分隔)
zookeeper.connect=localhost:2181
配置环境变量:
vim /etc/profile.d/kafka.sh
#KAFKA_HOME
export KAFKA_HOME=/opt/module/kafka
export PATH=$PATH:$KAFKA_HOME/bin
# 刷新环境变量
source /etc/profile
启动
启动Kafka前需要先启动Zookeeper
./zk.sh start
# 启动Kafka
cd /opt/app/kafka
bin/kafka-server-start.sh -daemon config/server.properties
# 关闭
bin/kafka-server-stop.sh
应为Zookeeper和Kafka都是Java开发,查看当前进程:
[root@rabbitmq kafka]# jps 1345 QuorumPeerMain 10916 Jps 3034 Kafka
脚本:
#! /bin/bash
case $1 in
"start"){
echo " --------启动 $i Kafka-------"
/opt/app/kafka/bin/kafka-server-start.sh -daemon /opt/app/kafka/config/server.properties
};;
"stop"){
echo " --------停止 $i Kafka-------"
/opt/app/kafka/bin/kafka-server-stop.sh
};;
esac
此时连接Zookeeper的客户端:
bin/zkCli.sh
[zk: localhost:2181(CONNECTED) 0] ls /
[admin, brokers, cluster, config, consumers, controller, controller_epoch, feature, isr_change_notification, latest_producer_id_block, log_dir_event_notification, zookeeper]
/brokers/ids/下就绑定着我们在配置文件设定的broker.id
命令行操作
主题
查看命令参数:
bin/kafka-topics.sh
| 参数 | 描述 |
|---|---|
| --bootstrap-server <String: server toconnect to> | 连接的 Kafka Broker 主机名称和端口号 |
| --topic <String: topic> | 操作的 topic 名称 |
| --create | 创建主题 |
| --delete | 删除主题 |
| --alter | 修改主题 |
| --list | 查看所有主题 |
| --describe | 查看主题详细描述 |
| --partitions <Integer: # of partitions> | 设置分区数 |
| --replication-factor <Integer: replication factor> | 设置分区副本 |
| --config <String: name=value> | 更新系统默认的配置 |
示例:
# 查看所有topic
bin/kafka-topics.sh --bootstrap-server localhost:9092 --list
# 创建1分区1副本的topic
bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --partitions 1 --replication-factor 1 --topic topic1
# 查看 topic1 主题的详情
bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic topic1
# 修改分区数(注意:分区数只能增加,不能减少)
bin/kafka-topics.sh --bootstrap-server localhost:9092 --alter --topic topic1 --partitions 3
# 删除主题
bin/kafka-topics.sh --bootstrap-server localhost:9092 --delete --topic topic1
生产者
查看命令参数:
bin/kafka-console-producer.sh
| 参数 | 描述 |
|---|---|
| --bootstrap-server <String: server toconnect to> | 连接的 Kafka Broker 主机名称和端口号 |
| --topic <String: topic> | 操作的 topic 名称 |
示例:
# 发送消息
bin/kafka-console-producer.sh --bootstrap-server 127.0.0.1:9092 --topic topic1
这里不能识别localhost:DNS resolution failed for loaclhost
消费者
查看命令参数:
bin/kafka-console-consumer.sh
| 参数 | 描述 |
|---|---|
| --bootstrap-server <String: server toconnect to> | 连接的 Kafka Broker 主机名称和端口号 |
| --topic <String: topic> | 操作的 topic 名称 |
| --from-beginning | 从头开始消费 |
| --group <String: consumer group id> | 指定消费者组名称 |
示例:
# 从当前时间开始消费
bin/kafka-console-consumer.sh --bootstrap-server 127.0.0.1:9092 --topic topic1
# 消费所有数据
bin/kafka-console-consumer.sh --bootstrap-server 127.0.0.1:9092 --from-beginning --topic topic1
2. 生产者
流程讲解
在消息发送的过程中,涉及到了两个线程——main 线程和 Sender 线程。在 main 线程中创建了一个双端队列 RecordAccumulator。main 线程将消息发送给 RecordAccumulator,Sender 线程不断从 RecordAccumulator 中拉取消息发送到 Kafka Broker。

是Sender线程主动去拉数据。
batch.size:只有数据积累到batch.size之后,sender才会发送数据。默认16k
linger.ms:如果数据迟迟未达到batch.size,sender等待linger.ms设置的时间 到了之后就会发送数据。单位ms,默认值是0ms,表示没有延迟。
- ProducerRecord: 消息对象。
- Interceptor: 拦截器,Kafka一共有两种拦截器:生产者拦截器和消费者拦截器。生产者拦截器既可以用来在消息发送前做一些准备工作,比如按照某个规则过滤不符合要求的消息、修改消息的内容等,也可以用来在发送回调逻辑前做一些定制化的需求,比如统计类工作。
- Partitioner: 分区器,消息经过序列化之后确定它发往的分区
- RecordAccumulator: 主要用来缓存消息以便Sender线程可以批量发送,进而减少网络传输的资源消耗以提升性能。RecordAccumulator缓存的大小可以通过生产者客户端参数buffer.memory配置,默认值为32M。如果生产者发送消息的速度超过发送到服务器的速度,则会导致生产者空间不足,这个时候Producer的send()方法调用要么被阻塞,要么抛出异常,这个取决于参数max.block.ms的配置,此参数的默认值为60秒。主线程中发送过来的消息都会被迫加到RecordAccumulator的某个双端队列(Deque)中,在RecordAccumulator的内部为每个分区都维护了一个双端队列,队列中的内容就是ProducerBatch,即Deque
。消息写入缓存时,追加到双端队列的尾部:Sender读取消息时,从双端队列的头部读取。
RecordAccumulator内部还有个BufferPool,主要用来复用特定大小的ProducerBatch块,这个特定大小会通过batch.size指定,默认为16KB,例如说当有一条消息(ProducerRecord)流入RecordAccumulator,会先通过分区号寻找双端队列(如果没有则创建),再从队列的尾部获取一个ProducerBatch(如果没有则新建),查看ProducerBatch是否还可以装的下这个ProducerRecord,如果可以则写入,如果不可以则需创建个新的ProducerBatch。在新建ProducerBatch时会评估这个ProducerRecord的大小是否超过batch.size,也就是可复用ProducerBatch的大小,如果没超过,那么可以从BufferPool中拿个闲置的ProducerBatch来使用。如果超过,那么就按实际的大小创建ProducerBatch,这个ProducerBatch不会再被复用。
- InFlightRequests: 请求在从Sender线程发往Kafka之前还会保存到InFlightRequests中,InFlightRequests保存对象的具体形式为Map<Nodeld,Deque>,它的主要作用是缓存了已经发出去但还没有收到响应的请求(Nodeld表示broker节点的id编号)。InFlightRequests还提供了许多管理类的方法,并且通过配置参数还可以限制每个连接(也就是客户端与Node之间的连接)最多缓存的请求数。这个配置参数为max.in.flight.requests.per.connection,默认值为5,即每个连接最多只能缓存5个未响应的请求,超过该数值之后就不能再向这个连接发送更多的请求了,除非有缓存的请求收到了响应。InFlightRequests还可以获得leastLoadedNode,即所有Node中负载最小的那一个。选择leastLoadedNode发送请求可以使它能够尽快发出,避免因网络拥塞等异常而影响整体的进度。比如元数据请求,当需要更新元数据时,会先挑选出leastLoadedNode,然后向这个Node发送MetadataRequest请求来获取具体的元数据信息。
应答机制
分区收到生产者发送的数据后,都需要向生产者发送ack参数可设置的值为0、1、all(-1)
- 0:代表producer往集群发送数据不需要等到集群的返回,不确保消息发送成功。安全性最低但是效率最高。
- 1:代表producer往集群发送数据只要leader应答就可以发送下一条,只确保leader发送成功。
- all:默认值。代表producer往集群发送数据需要ISR都完成从leader的同步才会发送下一条,确保leader发送成功和所有的副本都完成备份。安全性最高,但是效率最低
当ACK=all时,Leader和follower(ISR)落盘才会返回ack,会有数据重复现象,如果在leader已经写完成,且follower同步完成,但是在返回ack的出现故障,则会出现数据重复现象;极限情况下,这个也会有数据丢失的情况,比如follower和leader通信都很慢,所以ISR中只有一个leader节点,这个时候,leader完成落盘,就会返回ack,如果此时leader故障后,就会导致丢失数据。
ISR:
为了防止一个follower的宕机导致集群的瘫痪(所有follower都同步信息的条件不能达成,导致不能应答),Leader中维护了一个动态的in-sync replica set(ISR),意为和Leader保持同步的Follower+Leader集合(leader:0,isr:0,1,2)。
如果Follower长时间未向Leader发送通信请求或同步数据,则该Follower将被踢出ISR。该时间阈值由replica.lag.time.max.ms参数设定,默认30s。例如2超时,(leader:0, isr:0,1)。这样就不用等长期联系不上或者已经故障的节点。
重试机制
重要参数
retry.backoff.ms:两次重试之间的时间间隔,默认是 100ms。retries:重试次数。默认是 int 最大值,2147483647。
当消费者接收数据失败或者响应时间超过了设置的retry.backoff.ms参数时,Selecto会根据设置的retries参数进行重试。
如果设置了重试,同时想要保证消息的有序性,需要设置MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION=1 ,避免在其之后消息的在重试时被发送出去。
enable.idempotence:是否开启幂等性,默认 truecompression.type:生产者发送的所有数据的压缩方式,默认是 none。支持压缩类型:none、gzip、snappy、lz4 和 zstd。
消息发送
异步发送
异步发送就是非阻塞的,即发送消息后不会等待消息是否成功发送到 Kafka。
代码示例:
<!-- 导入依赖 -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.5.1</version>
</dependency>
public static void main(String[] args) {
Properties properties = new Properties();
// 配置Kafka服务地址
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.124.105:9092");
// 配置key的序列化
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 配置value的序列化
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 自动关闭
try (KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties)) {
ProducerRecord<String, String> producerRecord = new ProducerRecord<>("topic1", "在吗?");
// 不带回调函数
kafkaProducer.send(producerRecord);
// 带回调函数
kafkaProducer.send(new ProducerRecord<>("topic1", "你好!"), new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (e == null) {
System.out.println("主题:" + recordMetadata.topic() + "->" + "分区:" + recordMetadata.partition());
} else {
System.out.println(e.getMessage());
}
}
});
System.out.println("运行完成!");
}
}
注意,kafka配置文件中要进行以下设置,否则不能远程连接:
listeners=PLAINTEXT://localhost:9092
可以事先在xShell中开启一个消费者,以便更好的观察效果。
消息发送失败会自动重试,不需要我们在回调函数中手动重试。
同步发送
只需要在异步发送的基础上再调用一下 get()方法即可。
kafkaProducer.send(producerRecord).get();
异步和同步获取RecordMetadata的时机不同:
- 同步发送的
send方法会将消息添加到内部的缓冲区,但它不会立即返回,get调用会一直阻塞,直到消息成功发送到 Kafka 或者发送出错,然后返回RecordMetadata。 - 异步发送的
send方法时,它会将消息添加到内部的缓冲区,并立即返回,Kafka 生产者会有一个后台线程,负责将缓冲区中的消息异步发送到 Kafka 服务器。
比如在异步调用的例子中,运行成功的提示是先打印的。
分区
分区的好处:
- 便于合理使用存储资源,每个Partition在一个Broker上存储,可以把海量的数据按照分区切割成一 块一块数据存储在多台Broker上。合理控制分区的任务,可以实现负载均衡的效果。
- 提高并行度,生产者可以以分区为单位发送数据;消费者可以以分区为单位进行消费数据。
默认分区策略
默认的分区器 DefaultPartitioner:
- 指明partition的情况下,直接将指明的值作为partition值;例如partition=0,所有数据写入分区0
- 没有指明partition值但有key的情况下,将key的hash值与topic的partition数进行取余得到partition值;
- 既没有partition值又没有key值的情况下(此时key默认为空串),Kafka采用Sticky Partition(黏性分区器),会随机选择一个分区,并尽可能一直使用该分区,待该分区的batch已满或者已完成,Kafka再随机一个分区进行使用(和上一次的分区不同)。
简单测试:
for (int i = 0; i < 5; i++) {
kafkaProducer.send(new ProducerRecord<>("topic1", 1, "test" + i, "test1"), (metadata, e) -> {
System.out.println("test1" + "->" + "分区:" + metadata.partition());
});
kafkaProducer.send(new ProducerRecord<>("topic1", "test" + i, "test2"), (metadata, e) -> {
System.out.println("test2" + "->" + "分区:" + metadata.partition());
});
kafkaProducer.send(new ProducerRecord<>("topic1", "test3"), (metadata, e) -> {
System.out.println("test3" + "->" + "分区:" + metadata.partition());
});
}
自定义分区器
public class MyPartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
if (key.toString().contains("1")) {
return 1;
} else if (key.toString().contains("2")) {
return 2;
}
return 0;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}
装配:
public static void main(String[] args) {
Properties properties = new Properties();
// 配置Kafka服务地址
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.124.105:9092");
// 配置key的序列化
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 配置value的序列化
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 配置自定义分区器
properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, MyPartitioner.class.getName());
try (KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties)) {
for (int i = 0; i < 5; i++) {
System.out.println(i);
// 为了方便观察分区器的效果,这里使用同步发送
kafkaProducer.send(new ProducerRecord<>("topic1", "test" + i, "test2"), (metadata, e) -> {
System.out.println("test2" + "->" + "分区:" + metadata.partition());
}).get();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
参数调整
提高吞吐量
调整吞吐量的重要参数:
- batch.size:批次大小,默认16k
- linger.ms:等待时间,修改为5-100ms
- compression.type:压缩,常用 snappy
- RecordAccumulator:缓冲区大小,修改为64m
// batch.size:批次大小,默认 16K
properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
// linger.ms:等待时间,单位ms,默认 0;
properties.put(ProducerConfig.LINGER_MS_CONFIG, 1);
// RecordAccumulator:缓冲区大小,默认 32M:buffer.memory
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,33554432);
// compression.type:压缩,默认 none,可配置值 gzip、snappy、lz4 和 zstd
properties.put(ProducerConfig.COMPRESSION_TYPE_CONFIG,"snappy");
数据可靠
数据完全可靠条件 = ACK级别设置为-1 + 分区副本大于等于2 + ISR里应答的最小副本数量大于等于2
在生产环境中,acks=0很少使用;acks=1,一般用于传输普通日志,允许丢个别数据;acks=-1,可靠性要求比较高的场景。
// 设置 acks
properties.put(ProducerConfig.ACKS_CONFIG, "all");
// 重试次数 retries,默认是 int 最大值,2147483647
properties.put(ProducerConfig.RETRIES_CONFIG, 3);
数据去重
在应答机制中讲到过,达成数据完全可靠条件的同时,会引发数据重复的问题:
- 至少一次(At Least Once)= ACK级别设置为-1 + 分区副本大于等于2 + ISR里应答的最小副本数量大于等于2
- 最多一次(At Most Once)= ACK级别设置为0
精确一次(Exactly Once):对于一些非常重要的信息,比如和钱相关的数据,要求数据既不能重复也不丢失。
幂等性
指Producer不论向Broker发送多少次重复数据,Broker端都只会持久化一条,保证了不重复。
精确一次(Exactly Once) = 幂等性 + 至少一次( ack=-1 + 分区副本数>=2 + ISR最小副本数量>=2) 。
重复数据的判断标准:具有<PID, Partition, SeqNumber>相同主键的消息提交时,Broker只会持久化一条。其中PID是Kafka每次重启都会分配一个新的;Partition 表示分区号;Sequence Number是单调自增的。
只能保证单分区单会话内的幂等性,默认开启
生产者事务
事务是建立在幂等性的功能上的,所以开启事务必须开启幂等性。
事务流程

Producer 在使用事务功能前,必须先自定义一个唯一的 transactional.id。有 了 transactional.id,即使客户端挂掉了, 它重启后也能继续处理未完成的事务
相关API:
// 1 初始化事务
void initTransactions();
// 2 开启事务
void beginTransaction() throws ProducerFencedException;
// 3 在事务内提交已经消费的偏移量(主要用于消费者)
void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets,String consumerGroupId) throws ProducerFencedException;
// 4 提交事务
void commitTransaction() throws ProducerFencedException;
// 5 放弃事务(类似于回滚事务的操作)
void abortTransaction() throws ProducerFencedException;
示例:
public static void main(String[] args) {
Properties properties = new Properties();
// 配置Kafka服务地址
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.124.105:9092");
// 配置key的序列化
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 配置value的序列化
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 设置事务 id(必须),事务 id 任意起名
properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, UUID.randomUUID().toString());
// 自动关闭
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);
try {
kafkaProducer.initTransactions();
kafkaProducer.beginTransaction();
for (int i = 0; i < 5; i++) {
kafkaProducer.send(new ProducerRecord<>("topic1", 1, "test" + i, "test" + i), (metadata, e) -> {
System.out.println("test1" + "->" + "分区:" + metadata.partition());
});
// if (i == 3) {
// i /= 0;
// }
}
kafkaProducer.commitTransaction();
} catch (Exception e) {
kafkaProducer.abortTransaction();
throw new RuntimeException(e);
} finally {
kafkaProducer.close();
}
}
数据有序
单分区内,Kafka接收和发送的数据是有序的。
两种设置方式:
- 未开启幂等性:
max.in.flight.requests.per.connection=1 - 开启幂等性:
max.in.flight.requests.per.connection需要设置小于等于5。
启用幂等后,kafka服务端会缓存producer发来的最近5个request的元数据, 故无论如何,都可以保证最近5个request的数据都是有序的。
1.x之前的版本:需要设置:
max.in.flight.requests.per.connection=1,即上一条消息确认接收才发下一条
3. Broker
之前浅略地查看了zookeeper中存放的broker的信息,接下来进行详细的讲解。
Zookeaper 中存储的Kafka 信息:

总体工作流程
Broker 总体工作流程

ISR之前做过介绍,不再赘述。
OSR:表示Follower与Leader副本同步时,延迟过多的副本。
AR:指的是分区中的所有副本,所以AR = ISR + OSR。
副本
- Kafka 副本的作用:提高数据可靠性。
- 默认副本 1 个,生产环境一般配置为 2 个,保证数据可靠性;太多副本会增加磁盘存储空间,增加网络上数据传输,降低效率。
- 副本分为:Leader 和 Follower。Leader负责消息的接收和发送, Follower 主动从 Leader 同步数据。
- Kafka 分区中的所有副本统称为 AR
选举流程
Kafka 集群中有一个 broker 的 Controller 会被选举为 Controller Leader,负责管理集群 broker 的上下线,所有 topic 的分区副本分配和 Leader 选举等工作。
leader选举规则:在isr中存活为前提,按 照AR中排在前面的优先。例如 ar[1,0,2],isr[1,0,2],那么leader 就会按照1,0,2的顺序轮询。
假设上图中的broker1宕机,那么broker0中的Controller会在监听到节点变化后从Zookeeper中获取ISR,通过前面提到的选举规则选举出新的Leader,最后更新Zookeeper中的Leader和ISR。
参数列表
| 参数名称 | 描述 |
|---|---|
| replica.lag.time.max.ms | ISR 中,如果 Follower 长时间未向 Leader 发送通信请求或同步数据,则该 Follower 将被踢出 ISR。时间阈值默认 30s。 |
| auto.leader.rebalance.enable | 默认是 true。 自动 Leader Partition 平衡。 |
| leader.imbalance.per.broker.percentage | 默认是 10%。每个 broker 允许的不平衡的 leader 的比率。如果每个 broker 超过了这个值,控制器会触发 leader 的平衡。 |
| leader.imbalance.check.interval.seconds | 默认值 300 秒。检查 leader 负载是否平衡的间隔时间 |
| log.segment.bytes | Kafka 中 log 日志是分成一块块存储的,此配置是指 log 日志划分成块的大小,默认值 1G。 |
| log.index.interval.bytes | 默认 4kb,每当kafka 写入了 4kb 大小的日志(.log),然后就往 index 文件里面记录一个索引。 |
| log.retention.hours | Kafka 中数据保存的时间,默认 7 天 |
| log.retention.minutes | Kafka 中数据保存的时间,分钟级别,默认关闭 |
| log.retention.ms | Kafka 中数据保存的时间,毫秒级别,默认关闭 |
| log.retention.check.interval.ms | 检查数据是否保存超时的间隔,默认是 5 分钟。 |
| log.retention.bytes | 默认等于-1,表示无穷大。超过设置的所有日志总大小,删除最早的 segment。 |
| log.cleanup.policy | 默认是 delete,表示所有数据启用删除策略;如果设置值为 compact,表示所有数据启用压缩策略。 |
| num.io.threads | 默认是 8。负责写磁盘的线程数。整个参数值要占总核数的 50% |
| num.replica.fetchers | 副本拉取线程数,这个参数占总核数的 50%的 1/3 |
| num.network.threads | 默认为 3。数据传输线程数,这个参数占总核数的 50%的 2/3 。 |
| log.flush.interval.messages | 强制页缓存刷写到磁盘的条数,默认是 long 的最大值,9223372036854775807。一般不建议修改,交给系统自己管理。 |
| log.flush.interval.ms | 刷数据到磁盘的间隔时间,默认是 null。一般不建议修改,交给系统自己管理。 |
服役新节点
如果不是新装的kafka,需要删除对应存放的日志(指定的日志目录和目录下的logs文件夹)
# 需要复杂均衡的主题
vim topics-to-move.json
# 让kafka生成负载均衡计划
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --topics-to-move-json-file topics-to-move.json --broker-list "0,1,2,3" --generate
# 创建副本存储计划,即将生成的计划存储到文件中
vim increase-replication-factor.json
# 执行副本存储计划
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file increase-replication-factor.json --execute
# 查看副本存储计划
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file increase-replication-factor.json --verify
{
"topics": [
{"topic": "first"}
],
"version": 1
}
退役节点可以直接停掉服务,或者按照服役新节点的操作手动执行新存储计划(broker-list`少一个节点)
故障处理原理
概念:
- LEO(Log End Offset):每个副本的最后一个offset,LEO其实就是最新的offset + 1。
- HW(High Watermark):所有副本中最小的LEO 。

Follower
- Follower发生故障后会被临时踢出ISR
- 期间内Leader和其它Follower继续接收数据
- 待该Follower恢复后,Follower会读取本地磁盘记录的上次的HW,并将log文件高于HW的部分删除掉,从HW开始向Leader进行同步。
- 等该Follower的LEO大于等于HW,即 Follower追上Leader之后,就可以重新加入ISR了。
Leader
- Leader发生故障之后,会从ISR中选出一个新的Leader
- 为保证多个副本之间的数据一致性,其余的Follower会先将各自的log文件高于HW的部分截掉,然后从新的Leader同步数据。
只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。
分区副本分配
默认Kafka会进行分区以及副本的负载均衡,以下3Broker16分区3副本可以体现(未出现宕机等情况):

手动调整
每台服务器的配置和性能不一致,但是Kafka只会根据自己的代码规则创建对应的分区副本,就会导致个别服务器存储压力较大。所有需要手动调整分区副本的存储。
比如当前有四台服务器,但希望将将所有分区和副本放在前两个机器中:
# 创建一个新的 topic
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create --partitions 4 --replication-factor 2 --topic three
# 查看分区副本存储情况
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic three
# 创建副本存储计划
vim increase-replication-factor.json
# 执行副本存储计划
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file increase-replication-factor.json --execute
# 验证副本存储计划
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file increase-replication-factor.json --verify
# 查看分区副本存储情况
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic three
{
"version":1,
"partitions":[{"topic":"three","partition":0,"replicas":[0,1]},
{"topic":"three","partition":1,"replicas":[0,1]},
{"topic":"three","partition":2,"replicas":[1,0]},
{"topic":"three","partition":3,"replicas":[1,0]}]
}
增加副本
# 创建topic
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create --partitions 3 --replication-factor 1 --topic four
# 创建副本存储计划
vim increase-replication-factor.json
# 执行副本存储计划
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file increase-replication-factor.json --execute
{"version":1,"partitions":[{"topic":"four","partition":0,"replicas":[0,1,2]},{"topic":"four","partition":1,"replicas":[0,1,2]},{"topic":"four","partition":2,"replicas":[0,1,2]}]}
文件存储
数据存储机制
Topic是逻辑上的概念,而partition是物理上的概念,每个partition对应于一个log文件,该log文件中存储的就是Producer生产的数据。
Producer生产的数据会被不断追加到该log文件末端,为防止log文件过大导致数据定位效率低下,Kafka采取了分片和索引机制,将每个partition分为多个segment。
每个segment包括:“.index”文件、“.log”文件和.timeindex等文件。这些文件位于一个文件夹下,该文件夹的命名规则为:topic名称+分区序号,例如:first-0。
- .log:日志文件
- .index:偏移量索引文件
- .timeindex:时间戳索引文件
index和log文件以当前segment的第一条消息的offset命名
可以通过工具查看index和log信息:
# 查看索引文件
/opt/app/kafka/bin/kafka-run-class.sh kafka.tools.DumpLogSegments --files ./00000000000000000000.index
# 查看日志文件
/opt/app/kafka/bin/kafka-run-class.sh kafka.tools.DumpLogSegments --files ./00000000000000000000.log
定位消息:
- 根据目标offset定位Segment文件
- 找到小于等于目标offset的最大offset对应的索引项
- 定位到log文件
- 向下遍历找到目标Record
Index文件中保存的offset为相对offset,这样能确保offset的值所占空间不会过大, 因此能将offset的值控制在固定大小
文件清理策略
参数列表处介绍了Kafka文件清理的四个参数:
- log.retention.hours
- log.retention.minutes
- log.retention.ms
- log.retention.check.interval.ms
# 所有数据启用删除策略
log.cleanup.policy=delete
- 基于时间删除:默认打开。以 segment 中所有记录中的最大时间戳作为该文件时间戳。
也就是说文件中的数据全部都过期才会删除
- 基于大小:默认关闭。超过设置的日志总大小,删除最早的segment。log.retention.bytes,默认等于-1,表示无穷大
# 所有数据启用删除策略
log.cleanup.policy=compact
对于相同key的不同value值,只保留最后一个版本。
这种策略只适合特殊场景,比如消息的key是用户ID,value是用户的资料,通过这种压缩策略,整个消息集里就保存了所有用户最新的资料。
高效读写数据
原因:
- Kafka 本身是分布式集群,可以采用分区技术,并行度高
- 读数据采用稀疏索引,可以快速定位要消费的数据
- 顺序写磁盘
Kafka 的 producer 生产数据,要写入到 log 文件中,写的过程是一直追加到文件末端,省去了大量磁头寻址的时间
- 页缓存 + 零拷贝技术
PageCache页缓存:Kafka重度依赖底层操作系统提供的PageCache功 能。当上层有写操作时,操作系统只是将数据写入 PageCache。当读操作发生时,先从PageCache中查找,如果找不到,再去磁盘中读取。实际上PageCache是把尽可能多的空闲内存都当做了磁盘缓存来使用
零拷贝:Kafka的数据加工处理操作交由Kafka生产者和Kafka消费者处理。Kafka Broker应用层不关心存储的数据,所以就不用走应用层,传输效率高。
零拷贝技术示意图:

4. 消费者
Kafka消费方式:
- pull(拉)模式:consumer采用从broker中主动拉取数据。Kafka采用
- push(推)模式:由broker 决定消息发送速率,很难适应所有消费者的消费速率。没采用
pull模式不足之处是,如果Kafka没有数据,消费者可能会陷入循环中,一直返回空数据。
工作流程
生产者将数据发送到Broker,根据策略发送到多个分区,消费者订阅的分区中拉取数据,同时会在每个broker中维护一个offset信息,用于记录数据消费的偏移量。
消费者组:由一个或多个groupid相同的消费者构成。
- 消费者组内每个消费者负责消费不同分区的数据,一个分区只能由组内的一个消费者消费。
如果消费者组的消费者数超过主题分区数,那么会有一部分消费者被闲置
- 消费者组之间互不影响。消费者组是逻辑上的一个订阅者。
消费分配
在 Kafka 中,每个消费者组都有三个主要的 Coordinator:
- Group Coordinator(组协调器): 负责管理消费者组的整体信息,处理组的加入和离开,以及进行 Rebalance 操作。每个消费者组有一个对应的 Group Coordinator。
- Offset Coordinator(偏移协调器): 负责处理消费者组提交的 offset 信息。每个消费者组中的每个消费者都会有一个 Offset Coordinator。
- Transaction Coordinator(事务协调器): 用于支持事务性生产者,负责管理事务的开始、提交和回滚等操作。每个事务性生产者都有一个对应的 Transaction Coordinator。
这里介绍的组协调器,规则:groupid的hashcode值 % 50( __consumer_offsets主题的默认分区数量)
__consumer_offsets主题是实际存储消费者组的 offset 信息的地方。每个分区对应一个消费者组的 offset 数据。这些 offset 数据包含了消费者组在各个分区中的消费进度。
消费分配流程:
- 每个consumer都向Coordinator发送JoinGroup请求
- Coordinator选出一个消费者作为消费者组的leader
- Coordinator把要消费的topic情况发送给leader 消费者
- leader制定好消费方案后将消费方案发送给Coordinator
- Coordinator把消费方案下发给各个consumer,consumer根据消费方案拉取数据
每个消费者都会和coordinator保持心跳(默认3s),一旦超时(
session.timeout.ms=45s),该消费者会被移除,并触发再平衡;或者消费者处理消息的时间过长(max.poll.interval.ms:5分钟),也会触发再平衡。
参数列表
| 参数名称 | 描述 |
|---|---|
| bootstrap.servers | 向 Kafka 集群建立初始连接用到的 host/port 列表。 |
| key.deserializer 和 value.deserializer | 指定接收消息的 key 和 value 的反序列化类型(全类名)。 |
| group.id | 标记消费者所属的消费者组。 |
| enable.auto.commit | 默认值为 true,消费者会自动周期性地向服务器提交偏移量。 |
| auto.commit.interval.ms | 如果设置了 enable.auto.commit 的值为 true, 则该值定义了消费者偏移量向 Kafka 提交的频率,默认 5s。 |
| auto.offset.reset | 当 Kafka 中没有初始偏移量或当前偏移量在服务器中不存在(如,数据被删除了),该如何处理? earliest:自动重置偏移量到最早的偏移量。 latest:默认,自动重置偏移量为最新的偏移量。 none:如果消费组原来的(previous)偏移量不存在,则向消费者抛异常。 anything:向消费者抛异常。 |
| offsets.topic.num.partitions | __consumer_offsets 的分区数,默认是 50 个分区。 |
| heartbeat.interval.ms | Kafka 消费者和 coordinator 之间的心跳时间,默认 3s。该值必须小于 session.timeout.ms ,且不应该高于 session.timeout.ms 的 1/3。 |
| session.timeout.ms | Kafka 消费者和 coordinator 之间连接超时时间,默认 45s。超过该值,该消费者被移除,消费者组执行再平衡 |
| max.poll.interval.ms | 消费者处理消息的最大时长,默认是 5 分钟。超过该值,该消费者被移除,消费者组执行再平衡。 |
| fetch.min.bytes | 默认 1 个字节。消费者获取服务器端一批消息最小的字节数。 |
| fetch.max.wait.ms | 默认 500ms。如果没有从服务器端获取到一批数据的最小字节数。该时间到,仍然会返回数据。 |
| fetch.max.bytes | 默认 Default: 52428800(50 m)。消费者获取服务器端一批消息最大的字节数。如果服务器端一批次的数据大于该值 (50m)仍然可以拉取回来这批数据,因此,这不是一个绝对最大值。一批次的大小受 message.max.bytes(broker config) or max.message.bytes (topic config)影响。 |
| max.poll.records | 一次 poll 拉取数据返回消息的最大条数,默认是 500 条。 |
使用示例
指定消费主题:
public static void main(String[] args) {
Properties properties = new Properties();
// 给消费者配置对象添加参数
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.124.105:9092");
// 配置序列化
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
// 配置消费者组(组名任意起名)
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
try (KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties)) {
// 注册要消费的主题(可以消费多个主题)
List<String> topics = new ArrayList<>();
topics.add("topic1");
kafkaConsumer.subscribe(topics);
// 拉取数据打印
while (true) {
// 设置 1s 中消费一批数据
ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, String> record : records) {
System.out.println(record);
}
}
}
}
使用
StringDeserializer实现反序列化
指定消费分区:
try (KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties)) {
// 指定分区消费
List<TopicPartition> topics = new ArrayList<>();
topics.add(new TopicPartition("topic1", 0));
kafkaConsumer.assign(topics);
// 拉取数据打印
while (true) {
// 设置 1s 中消费一批数据
ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, String> record : records) {
System.out.println(record);
}
}
}
分配策略与再平衡
前面介绍到了消费分配,但是消费者Leader是根据什么来指定消费方案呢?
Kafka有四种主流的分区分配策略: Range、RoundRobin、Sticky、CooperativeSticky。
可以通过配置参数partition.assignment.strategy来修改分区的分配策略,Kafka可以同时使用多个分区分配策略。默认策略是Range + CooperativeSticky。
Range
Range 是对每个 topic 而言的。
首先对同一个topic中的分区按照序号进行排序,并对消费者按照字母顺序进行排序;通过partitions数/consumer数 来决定每个消费者应该消费几个分区。如果除不尽,那么前面几个消费者将会多消费 1 个分区。
问题:容易产生数据倾斜,因为对于多个topic而言,前面的消费者会多消费一些分区
再平衡策略:
- 在45s以内某个消费者宕机,kafka会将其本应该担任的分区整体分配给其它消费者。
- 在45s以后,宕机的消费者会被踢出消费者组,消费者的分区进行重新分配
RoundRobin
RoundRobin 针对集群中所有Topic而言。
RoundRobin 轮询分区策略,是把所有的 partition 和所有的 consumer 都列出来,然后按照 hashcode 进行排序,最后通过轮询算法来分配 partition 给到各个消费者。
// 修改分区分配策略
properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFI
G, "org.apache.kafka.clients.consumer.RoundRobinAssignor");
再平衡策略:
- 45s内,按照 RoundRobin 的方式,分别将分区数据交给剩余的消费者
- 45s后,重新按照 RoundRobin 方式分配
Sticky
粘性分区定义:在执行一次新的分配之前,考虑上一次分配的结果,尽量少的调整分配的变动,可以节省大量的开销
首先会尽量均衡的放置分区到消费者上面,在出现同一消费者组内消费者出现问题的时候,会尽量保持原有分配的分区不变化。
再平衡策略:
- 45s内,尽可能均衡地随机将分区数据交给其他消费者
- 45s后,重新按照粘性方式分配
offset
0.9版本前,consumer默认将offset保存在Zookeeper中;0.9版本之后,consumer默认将offset保存在Kafka的__consumer_offsets主题中。
其中数据的key是group.id+topic+ 分区号,value就是当前offset的值。每隔一段时间,kafka内部会对这个topic进行compact,也就是保留最新数据。
该主题默认是不能消费的,需要在配置文件config/consumer.properties中添加配置exclude.internal.topics=false。
自动提交
默认开启,相关参数
- enable.auto.commit:是否开启自动提交offset功能
- auto.commit.interval.ms:自动提交offset的时间间隔,默认是5s
手动提交
手动提交offset的方法有两种:分别是commitSync(同步提交)和commitAsync(异步提交)。
- 相同点:都会将本次提交的一批数据最高的偏移量提交
- 不同点:同步提交阻塞当前线程,一直到提交成功,并且会自动失败重试(由不可控因素导致,也会出现提交失败);而异步提交则没有失败重试机制,故有可能提交失败。
同步提交必须等待offset提交完毕,才能去消费下一批数据
同步提交:
public static void main(String[] args) {
Properties properties = new Properties();
// 给消费者配置对象添加参数
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.124.105:9092");
// 配置序列化
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
// 设置手动提交
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
// 配置消费者组(组名任意起名)
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
try (KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties)) {
// 指定分区消费
List<TopicPartition> topics = new ArrayList<>();
topics.add(new TopicPartition("topic1", 0));
kafkaConsumer.assign(topics);
// 拉取数据打印
while (true) {
// 设置 1s 中消费一批数据
ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, String> record : records) {
System.out.println(record);
}
kafkaConsumer.commitSync();
}
}
}
异步提交:
// 异步提交 offset
consumer.commitAsync();
指定Offset消费
public static void main(String[] args) {
Properties properties = new Properties();
// 给消费者配置对象添加参数
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.124.105:9092");
// 配置序列化
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
// 设置手动提交
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
// 配置消费者组(组名任意起名)
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
try (KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties)) {
// 指定主题消费
kafkaConsumer.subscribe(Collections.singletonList("topic1"));
Set<TopicPartition> assignment= new HashSet<>();
while (assignment.isEmpty()) {
kafkaConsumer.poll(Duration.ofSeconds(1));
// 获取消费者分区分配信息(有了分区分配信息才能开始消费)
assignment = kafkaConsumer.assignment();
}
// 遍历所有分区,并指定 offset 从 200 的位置开始消费
for (TopicPartition tp: assignment) {
kafkaConsumer.seek(tp, 200);
}
// 拉取数据打印
while (true) {
// 设置 1s 中消费一批数据
ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, String> record : records) {
System.out.println(record);
}
kafkaConsumer.commitSync();
}
}
}
poll方法不是调用一次就能返回数据,需要多次交互等待消费方案的确定。
指定时间消费
public static void main(String[] args) {
Properties properties = new Properties();
// 给消费者配置对象添加参数
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.124.105:9092");
// 配置序列化
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
// 设置手动提交
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
// 配置消费者组(组名任意起名)
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
try (KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties)) {
// 指定主题消费
kafkaConsumer.subscribe(Collections.singletonList("topic1"));
Set<TopicPartition> assignment = new HashSet<>();
while (assignment.isEmpty()) {
kafkaConsumer.poll(Duration.ofSeconds(1));
// 获取消费者分区分配信息(有了分区分配信息才能开始消费)
assignment = kafkaConsumer.assignment();
}
Map<TopicPartition, Long> timeMap = new HashMap<>();
// 得到一周前的时间戳
Long time = LocalDateTime.now().minusWeeks(1).toEpochSecond(ZoneOffset.UTC);
// 封装集合存储,每个分区对应一天前的数据
for (TopicPartition tp : assignment) {
timeMap.put(tp, time);
}
// 获取从 1 周前开始消费的每个分区的 offset
Map<TopicPartition, OffsetAndTimestamp> offsetMap = kafkaConsumer.offsetsForTimes(timeMap);
// 遍历每个分区,对每个分区设置消费时间。
for (TopicPartition tp : assignment) {
OffsetAndTimestamp offsetAndTimestamp = offsetMap.get(tp);
// 根据时间指定开始消费的位置
if (offsetAndTimestamp != null) {
kafkaConsumer.seek(tp, offsetAndTimestamp.offset());
}
}
// 拉取数据打印
while (true) {
// 设置 1s 中消费一批数据
ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, String> record : records) {
System.out.println(record);
}
kafkaConsumer.commitSync();
}
}
}
漏消费和重复消费
- 重复消费:已经消费了数据,但是 offset 没提交。
自动提交offset的情况下,如果提交offset后的2s,consumer挂了,再次重启consumer,则从上一次提交的offset处继续消费,导致重复消费
- 漏消费:先提交 offset 后消费,有可能会造成数据的漏消费。
offset为手动提交,当offset被提交时,数据还在内存中未落盘,此时刚好消费者线程被kill掉,那么offset已经提交,但是数据未处理完成,导致这部分内存中的数据丢失。
消费者事务:
想要Consumer端实现精准一次性消费,需要Kafka消费端将消费过程和提交offset 过程做原子绑定,可以将Kafka的offset保存到支持事务的自定义介质(比 如 MySQL)。
提高吞吐量
- 增加Topic的分区数,并且同时提升消费组的消费者数量,消费者数 = 分区数。
- 提高每批次拉取的数量
5. Kafka-Eagle 监控
Kafka-Eagle 框架可以监控 Kafka 集群的整体运行情况。Kafka-Eagle 的安装依赖于 MySQL。
配置不足,该章节仅作记录
安装使用
关闭服务后,修改kafka文件bin/kafka-server-start.sh:
# 原设置
if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then
export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G"
fi
# 改为:
if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then
export KAFKA_HEAP_OPTS="-server -Xms2G -Xmx2G -XX:PermSize=128m -XX:+UseG1GC - XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=8 -XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70"
export JMX_PORT="9999"
#export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G"
fi
官网下载安装包:EFAK (kafka-eagle.org)
# 解压完成后改名
mv efak-web-2.0.8/ efak
# 修改配置文件
vim efak/conf/system-config.properties
需要修改的点:
efak.zk.cluster.alias=cluster1
cluster1.zk.list=hadoop102:2181,hadoop103:2181,hadoop104:2181/kafka
# offset 保存在 kafka
cluster1.efak.offset.storage=kafka
# 配置 mysql 连接
efak.driver=com.mysql.jdbc.Driver
efak.url=jdbc:mysql://hadoop102:3306/ke?useUnicode=true&characterEncoding=UT
F-8&zeroDateTimeBehavior=convertToNull
efak.username=root
efak.password=000000
添加环境变量:
vim /etc/profile.d/efak.sh
# kafkaEFAK
export KE_HOME=/opt/module/efak
export PATH=$PATH:$KE_HOME/bin
#刷新配置
source /etc/profile
启动zk以及kafka后启动efak:
bin/ke.sh start
# 停止
bin/ke.sh stop
Kafka-Kraft 模式
此部分没有进行实践,算是对未来架构的窥探,了解即可
在现有结构中,元数据在zookeeper中,运行时动态选举controller,由controller进行Kafka集群管理。
在Kraft模式架构中,是与哦给你三台controller节点代替zookeeper,元数据保存在controller中,由controller直接进行 Kafka 集群管理,不再依赖 zookeeper 集群。
好处:
- Kafka 不再依赖外部框架,而是能够独立运行;
- controller 管理集群时,不再需要从 zookeeper 中先读取数据,集群性能上升;
- 由于不依赖 zookeeper,集群扩展时不再受到 zookeeper 读写能力限制;
- controller 不再动态选举,而是由配置文件规定。这样我们可以有针对性的加强 controller 节点的配置,而不是对随机 controller 节点的高负载束手无策。
部署
# 解压kafka安装包
tar -zxvf kafka_2.12-3.0.0.tgz -C /opt/module/
mv kafka_2.12-3.0.0/ kafka
# 修改配置文件
vim /opt/module/kafka/config/kraft/server.properties
#kafka 的角色(controller 相当于主机、broker 节点相当于从机,主机类似 zk 功能)
process.roles=broker,controller
#节点 ID
node.id=2
#controller 服务协议别名
controller.listener.names=CONTROLLER
#全 Controller 列表
controller.quorum.voters=2@hadoop102:9093,3@hadoop103:9093,4@hadoop104:9093
#不同服务器绑定的端口
listeners=PLAINTEXT://:9092,CONTROLLER://:9093
#broker 服务协议别名
inter.broker.listener.name=PLAINTEXT
#broker 对外暴露的地址
advertised.Listeners=PLAINTEXT://hadoop102:9092
#协议别名到安全协议的映射
listener.security.protocol.map=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,SSL:SSL,SASL_PLAINTEXT:SASL_PLAINTEXT,SASL_SSL:SASL_SSL
#kafka 数据存储目录
log.dirs=/opt/module/kafka/data
node.id需要保证全局唯一,用于controller选举,值需要和 controller.quorum.voters 对应。
初始化集群数据目录:
- 生成存储目录唯一 ID
bin/kafka-storage.sh random-uuid
得到的值为:J7s9e8PPTKOO47PxzI39VA,用于之后指令执行
- 格式化 kafka 存储目录(三台节点)
# 分别在三台机器中执行
bin/kafka-storage.sh format -t J7s9e8PPTKOO47PxzI39VA -c
- 启动Kafka集群
# 分别在三台机器中执行
bin/kafka-server-start.sh -daemon config/kraft/server.properties
# 停止
bin/kafka-server-stop.sh
启停脚本:
#! /bin/bash
case $1 in
"start"){
for i in hadoop102 hadoop103 hadoop104
do
echo " --------启动 $i Kafka-------"
ssh $i "/opt/module/kafka2/bin/kafka-server-start.sh -daemon /opt/module/kafka/config/kraft/server.properties"
done
};;
"stop"){
for i in hadoop102 hadoop103 hadoop104
do
echo " --------停止 $i Kafka-------"
ssh $i "/opt/module/kafka2/bin/kafka-server-stop.sh "
done
};;
esac
6. SpringBoot整合
依赖:
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
配置文件:
spring:
application:
name: kafkaApp
kafka:
bootstrap-servers: 192.168.124.105:9092
logging:
level:
workspace: debug
启动类:
@SpringBootApplication
public class KafkaApp {
public static void main(String[] args) {
SpringApplication.run(KafkaApp.class, args);
}
}
配置类:
@Configuration
public class KafkaConfig {
@Value("${spring.kafka.bootstrap-servers}")
private String bootstrapServers;
@Bean
public KafkaProducer<String, String> kafkaProducer() {
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
return new KafkaProducer<>(properties);
}
@Bean
public KafkaConsumer<String, String> kafkaConsumer() {
Properties properties = new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "consumer1");
return new KafkaConsumer<>(properties);
}
@KafkaListener(topics = {"topic1"}, groupId = "group1")
public void listen(ConsumerRecord<String, String> record) {
System.out.println(record);
}
}
当然也可以在配置文件中声明
控制器:
@RestController
@RequestMapping("/kafka")
public class KafkaController {
@Autowired
private KafkaProducer<String, String> producer;
@Autowired
private KafkaConsumer<String, String> consumer;
@GetMapping("/{message}")
public Object sendMessage(@PathVariable String message) {
return producer.send(new ProducerRecord<>("topic1", message));
}
@GetMapping("/get/{topic}")
public String getMessage(@PathVariable String topic) {
consumer.subscribe(Collections.singletonList(topic));
ConsumerRecords<String, String> poll = consumer.poll(Duration.ofSeconds(1));
StringBuilder result = new StringBuilder();
for (ConsumerRecord<String, String> record : poll) {
result.append(record.value()).append("\n");
}
return result.toString();
}
}
7. 性能调优
硬件配置
- 服务器台数= 2 * (生产者峰值生产速率 * 副本 / 100) + 1
(生产者峰值生产速率 * 副本 / 100)的值需要进位,比如
20*2/100->1
- 磁盘:kafka 底层主要是顺序写,建议选择普通的机械硬盘。
- Kafka 内存组成:堆内存 + 页缓存
- Kafka 堆内存建议每个节点:10g ~ 15g
修改kafka-server-start.sh:
if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then
export KAFKA_HEAP_OPTS="-Xmx10G -Xms10G"
fi
- 查看GC情况:
#查看kafka进程号
jps
#根据进程号查看GC情况
jstat -gc [进程号] 1s 10
参数说明:
| 参数 | 解释 |
|---|---|
| S0C | 年轻代中第一个survivor (幸存区)的容量(kb) |
| S1C | 年轻代中第二个survivor (幸存区)的容量(kb) |
| S0U | 年轻代中第一个survivor (幸存区)目前已使用空间(kb) |
| S1U | 年轻代中第二个survivor (幸存区)目前已使用空间(kb) |
| EC | 年轻代中Eden(伊甸园)的容量 (kb) |
| EU | 年轻代中Eden(伊甸园)目前已使用空间 (kb) |
| OC | Old代的容量 (kb) |
| OU | Old代目前已使用空间 (kb) |
| PC | Perm(持久代)的容量 (kb) |
| PU | Perm(持久代)目前已使用空间 (kb) |
| YGC | 从应用程序启动到采样时年轻代中gc次数 |
| YGCT | 从应用程序启动到采样时年轻代中gc所用时间(s) |
| FGC | 从应用程序启动到采样时old代(全gc)gc次数 |
| FGCT | 从应用程序启动到采样时old代(全gc)gc所用时间(s) |
| GCT | 从应用程序启动到采样时gc用的总时间(s) |
- 查看 Kafka 的堆内存
jmap -heap [进程号]
- 每个节点页缓存大小 =(分区数 * 1g * 25%)/ 节点数
页缓存:页缓存是 Linux 系统服务器的内存。只需要保证 1 个 segment(1g)中 25%的数据在内存中就好。
- CPU
- num.io.threads:负责写磁盘的线程数,整个参数值要占总核数的 50%
- num.replica.fetchers:副本拉取线程数,这个参数占总核数的 50%的 1/3
- num.network.threads:数据传输线程数,这个参数占总核数的 50%的 2/3
- 网络带宽 = 峰值吞吐量
100Mbps 单位是 bit;10M/s 单位是 byte ; 1byte = 8bit,100Mbps/8 = 12.5M/s。

浙公网安备 33010602011771号