1. 环境搭建
| 主机名 | IP |
|---|---|
| ubuntu1 | 192.168.150.100 |
| ubuntu2 | 192.168.150.101 |
| ubuntu3 | 192.168.150.102 |
注意,以下均为Kraft方式搭建,官方不建议生产环境这么做 截止2022.8.17
1.1 三节点原生集群搭建(Kraft)
# ubuntu 22.04 TLS 直接一键安装jdk11
sudo apt-get update
sudo apt-get install default-jdk
echo 192.168.150.100 ubuntu1 >> /etc/hosts
echo 192.168.150.101 ubuntu2 >> /etc/hosts
echo 192.168.150.102 ubuntu3 >> /etc/hosts
source /etc/hosts
#1.官网下载安装包,解压至 /opt/kafka
tar -zxvf
#2.进入其config目录,修改 server.properties 文件
vim /opt/kafka/kafka_2.12-3.2.1/config/server.properties
node.id=1/2/3 # 三台kafka一定要不同
advertised.listeners=PLAINTEXT://ubuntu2:9092 # ubuntu2是当前kafka主机名
log.dirs=/opt/kafka/kafka_2.12-3.2.1/data/kraft-combined-logs # 自定义数据目录
controller.quorum.voters=1@ubuntu1:9093,2@ubuntu2:9093,3@ubuntu3:9093 # 彼此连接
echo export KAFKA_HOME=/opt/kafka/kafka_2.12-3.2.1 >> /etc/profile
echo export PATH=$PATH:$KAFKA_HOME/bin >> /etc/profile
source /etc/profile
# 生成集群id
kafka-storage.sh random-uuid
# 集群初始化,三台节点都要执行
kafka-storage.sh format -t _852RDWbTAeZuD9ABwgteg -c /opt/kafka/kafka_2.12-3.2.1/config/kraft/server.properties
# 启动
kafka-server-start.sh -daemon /opt/kafka/kafka_2.12-3.2.1/config/kraft/server.properties
kafka-server-stop.sh
# 测试
# 创建first主题并分配3个分区和3个副本
kafka-topics.sh --bootstrap-server 192.168.150.100:9092 --create --topic second --partitions 3 --replication-factor 3
# 查看主题
kafka-topics.sh --bootstrap-server 192.168.150.101:9092 --list
# 生产数据 1号服务器执行
kafka-console-producer.sh --bootstrap-server ubuntu1:9092 --topic first
# 消费数据 2号服务器执行
kafka-console-consumer.sh --bootstrap-server ubuntu2:9092 --topic first
# 一些便利性脚本
# 首先让服务器之间免密登陆
ssh-keygen -t rsa # 控制节点执行
ssh-copy-id -i ~/.ssh/id_rsa.pub root@ubuntu1 # 拷贝公钥->1 自己也要拷贝
ssh-copy-id -i ~/.ssh/id_rsa.pub root@ubuntu2 # 拷贝公钥->2
ssh-copy-id -i ~/.ssh/id_rsa.pub root@ubuntu3 # 拷贝公钥->3
# 如果想要删除,干掉 /root/.ssh/authorized_keys 即可
#! /bin/bash
case $1 in
"start"){
for i in ubuntu1 ubuntu2 ubuntu3
do
echo " --------启动 $i Kafka2-------"
ssh $i "kafka-server-start.sh -daemon /opt/kafka/kafka_2.12-3.2.1/config/kraft/server.properties"
done
};;
"stop"){
for i in ubuntu1 ubuntu2 ubuntu3
do
echo " --------停止 $i Kafka3-------"
ssh $i "/opt/kafka/kafka_2.12-3.2.1/bin/kafka-server-stop.sh"
done
};;
esac
1.2 三节点Docker方式集群搭建(Kraft)
sudo apt install docker-compose
# 自己家网络有点问题,用服务器上传到了腾讯云,有需要的可以自己拉取
cat <<EOF> /etc/docker/daemon.json
{
"registry-mirrors": [
"https://mirror.ccs.tencentyun.com"
]
}
sudo systemctl restart docker
docker pull myteam-p-docker.pkg.coding.net/mall-project/public/kafka:latest # bitnami/kafka:3.2.1
docker tag myteam-p-docker.pkg.coding.net/mall-project/public/kafka:latest bitnami/kafka:latest
# 三台服务器都要执行
version: "3"
services:
kafka:
image: 'bitnami/kafka:latest'
user: root
ports:
- '9092:9092'
- '9093:9093'
environment:
- KAFKA_ENABLE_KRAFT=yes
- KAFKA_CFG_PROCESS_ROLES=broker,controller
- KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://192.168.150.100:9092 # 哪台机器执行,改成对应的IP
- KAFKA_BROKER_ID=1 # 每台kafka这里都需要改一下,不同即可
- KAFKA_KRAFT_CLUSTER_ID=LelM2dIFQkiUFvXCEcqRWA
- KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@192.168.150.100:9093,2@192.168.150.101:9093,3@192.168.150.102:9093
- ALLOW_PLAINTEXT_LISTENER=yes # 写IP
volumes:
- /opt/docker-kafka/kraft:/bitnami/kafka:rw
# 三台服务器分别执行上面yaml,注意内容
docker-compose up -d
# 【测试】注意:由于我已经原生搭建过kafka集群,所以这一步我可以直接在容器外使用命令行!容器内互通的
# 创建first主题并分配3个分区和3个副本
kafka-topics.sh --bootstrap-server 192.168.150.100:9092 --create --topic second --partitions 3 --replication-factor 3
# 查看主题
kafka-topics.sh --bootstrap-server 192.168.150.101:9092 --list
# 生产数据 1号服务器执行
kafka-console-producer.sh --bootstrap-server ubuntu1:9092 --topic first
# 消费数据 2号服务器执行
kafka-console-consumer.sh --bootstrap-server ubuntu2:9092 --topic first
1.3 Kafka-UI
github:github.com/provectus/k…
# kafka-ui
docker run -p 8080:8080 \
-e KAFKA_CLUSTERS_0_NAME=local \
-e KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=192.168.150.100:9092,192.168.150.101:9092,192.168.150.102:9092 \
-d myteam-p-docker.pkg.coding.net/mall-project/public/kafka-ui:latest
2. 核心概念
-
Producer:消息生产者,就是向 Kafka broker 发消息的客户端。
-
Consumer:消息消费者,向 Kafka broker 取消息的客户端。
-
Consumer Group(CG):消费者组,由多个 consumer 组成。消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费;消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
-
Broker:一台 Kafka 服务器就是一个 broker。一个集群由多个 broker 组成。一个broker 可以容纳多个 topic。
-
Topic:可以理解为一个队列,生产者和消费者面向的都是一个 topic。
-
Partition:为了实现扩展性,一个非常大的 topic 可以分布到多个 broker(即服务器)上,一个 topic 可以分为多个 partition,每个 partition 是一个有序的队列。 Topic与Partition是一对多的关系
-
Replica:副本。一个 topic 的每个分区都有若干个副本,一个 Leader和若干个 Follower
-
Leader:每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数据的对象都是 Leader。
-
Follower:每个分区多个副本中的“从”,实时从 Leader 中同步数据,保持和Leader 数据的同步。Leader 发生故障时,某个 Follower 会成为新的 Leader。
2.1 基础命令
| 参数 | 描述 |
|---|---|
| --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> | 更新系统默认配置 |
2.2 Producer
2.2.1 发送流程
// JavaAPI简单Demo
// 1. 创建 kafka 生产者的配置对象
Properties properties = new Properties();
// 2. 给 kafka 配置对象添加配置信息:bootstrap.servers
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop102:9092");
// key,value 序列化(必须):key.serializer,value.serializer
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.StringSerializer");
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.StringSerializer");
// 3. 创建 kafka 生产者对象
KafkaProducer<String, String> kafkaProducer = new
KafkaProducer<String, String>(properties);
// 4. 调用 send 方法,发送消息
for (int i = 0; i < 5; i++) {
// 注意这个ProducerRecor对象
kafkaProducer.send(new ProducerRecor<>("first","atguigu " + i));
}
// 5. 关闭资源
kafkaProducer.close();
2.2.2 分区规则
主要是ProducerRecor这个类的构造方法
自定义分区规则
- 定义类实现 Partitioner 接口。;
- 重写 partition()方法;
- 加入配置类
// 简单Demo
@Override
public int partition(String topic, Object key, byte[]
keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
// 获取消息
String msgValue = value.toString();
// 创建 partition
int partition;
// 判断消息是否包含 atguigu
if (msgValue.contains("atguigu")) {
partition = 0;
} else {
partition = 1;
}
// 返回分区号
return partition;
}
// 关闭资源
@Override
public void close() {}
// 配置方法
@Override
public void configure(Map<String, ?> configs) {}
=====================================================
// Properties对象指定自定义分区规则
properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.kafka.producer.MyPartitioner");
2.2.3 几个调优参数
好文推荐:[kafka生产者性能相关的参数理解 - 代码天地 (codetd.com)](www.codetd.com/article/132… 当执行KafkaProducer.send,() 和KafkaProducer.partitionsFor ()时阻塞等待的时间,之所以会阻塞时因为可能buffer满了或者获取元数据异常,那么超过这个时间就会抛出异常。)
batch.size:# 批次大小,默认16k
linger.ms:# 等待时间,修改为5-100ms
compression.type:# 压缩snappy 可选值 none, gzip, snappy, lz4, or zstd
RecordAccumulator:# 缓冲区大小,修改为64m
retries: # 重试次数,默认Integer.MAX_VALUE 官方建议通过这个参数delivery.timeout.ms来控制重试行为。
delivery.timeout.ms # 调用send()返回后报告成功或失败时间的上限 默认值为2分钟
2.2.4 Ack机制
消息发送方可以通过配置request.required.acks属性来保证消息的安全发送,值包括:
0:表示不进行消息接收是否成功的确认
1:表示当Leader落盘成功时发送确认
-1(ALL):表示Leader和ISR队列都落盘成功时确认
官方文档说明如下:
offsets.commit.required.acks
acks=0If set to zero then the producer will not wait for any acknowledgment from the server at all. The record will be immediately added to the socket buffer and considered sent. No guarantee can be made that the server has received the record in this case, and theretriesconfiguration will not take effect (as the client won't generally know of any failures). The offset given back for each record will always be set to-1.acks=1This will mean the leader will write the record to its local log but will respond without awaiting full acknowledgement from all followers. In this case should the leader fail immediately after acknowledging the record but before the followers have replicated it then the record will be lost.acks=allThis means the leader will wait for the full set of in-sync replicas to acknowledge the record. This guarantees that the record will not be lost as long as at least one in-sync replica remains alive. This is the strongest available guarantee. This is equivalent to the acks=-1 setting.
2.2.5 数据幂等性
enable.idempotence
When set to 'true', the producer will ensure that exactly one copy of each message is written in the stream. If 'false', producer retries due to broker failures, etc., may write duplicates of the retried message in the stream. Note that enabling idempotence requires max.in.flight.requests.per.connection to be less than or equal to 5 (with message ordering preserved for any allowable value), retries to be greater than 0, and acks must be 'all'.
Idempotence is enabled by default if no conflicting configurations are set. If conflicting configurations are set and idempotence is not explicitly enabled, idempotence is disabled. If idempotence is explicitly enabled and conflicting configurations are set, a ConfigException is thrown.
解释如下
- 幂等性必须运行在 ack=-1 模式下;
- 在没有冲突配置环境下(比如 ack =-1),默认开启;
- 幂等性判断标准:<PID,Partition,SeqNumber> 全部相同;
- 只能保证单分区单会话条件下消息不重(也保证了消息不丢),全局唯一需要事务引入
2.2.6 事务
// 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;
注意:消息发送前必须指定了事务ID
properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,"transaction_id_0");
2.2.7 数据有序
max.in.flight.requests.per.connection # 控制Producer发送窗口大小(RabbitMQ有个 basicQos方法是消费端限流,区别于这个)
kafka在1.x及以后版本保证数据单分区有序,条件如下:
- 未开启幂等性:
max.in.flight.requests.per.connection需要设置为1 - 开启幂等性:
max.in.flight.requests.per.connection需要设置小于等于5- 原因说明:因为在kafka1.x以后,启用幂等后,kafka服务端会缓存producer发来的最近5个request的元数据,故无论如何,都可以保证最近5个request的数据都是有序的
max.in.flight.requests.per.connection
The maximum number of unacknowledged requests the client will send on a single connection before blocking. Note that if this config is set to be greater than 1 and
enable.idempotenceis set to false, there is a risk of message re-ordering(个人认为这里应该翻译为乱序而不是重排序) after a failed send due to retries (i.e., if retries are enabled). Additionally, enabling idempotence requires this config value to be less than or equal to 5. If conflicting configurations are set and idempotence is not explicitly enabled, idempotence is disabled.
2.3 Broker
2.3.1 Zookeeper中记录的节点信息
2.3.2 Broker 总体工作流程 & Leader选举流程
- Leader上位顺序为 Replicas 中的顺序(从左到右),但必须在 Isr 中存活,
unclean.leader.election.enable
Indicates whether to enable replicas not in the ISR set to be elected as leader as a last resort, even though doing so may result in data loss
该参数如果设为 true 可以让不在isr队列里的节点参加Leader选举!默认false(3.2.1),但是这个默认值在之前的版本经常变更
2.3.3 Kafka副本
- 默认副本1个,生产环境一般配置为2个,保证数据可靠性;太多副本会增加磁盘空间以及网络传输负担;
- Kfaka中副本分为:Leader和Follower。Kafka生产者只会把数据发往Leader,Follower找Leader同步;
- Kafka分区中所有副本统称为AR(Assigned Repllicas) AR = ISR + OSR
- ISR表示和Leader保持同步的Follower集合。Leader故障时,从ISR中选取新的Leader;ISR有以下几个参数
replica.lag.time.max.ms默认10000 即 10秒 这个参数很有讲究,参考这篇文章 (2条消息) Kafka之ISR机制的理解_搬砖党弟中弟的博客-CSDN博客_isr机制
- OSR表示Follower与Leader副本同步时,延迟过多的副本
- ISR表示和Leader保持同步的Follower集合。Leader故障时,从ISR中选取新的Leader;ISR有以下几个参数
replica.lag.time.max.ms当follower副本将leader副本的LEO之前的日志全部同步时,则认为该follower副本已经追赶上leader副本。
此时更新该副本的lastCaughtUpTimeMs标识。
Kafka的副本管理器(ReplicaManager)启动时会启动一个副本过期检测的定时任务,
会定时检查当前时间与副本的lastCaughtUpTimeMs差值是否大于参数replica.lag.time.max.ms指定的值。
所以replica.lag.time.max.ms的正确理解是:
follower在过去的replica.lag.time.max.ms时间内,已经追赶上leader一次了。
2.3.3.1 Leader与Follower同步问题(LEO/HW)
注:小于 HW 值的offset所对应的消息被认为是“已提交”或“已备份”的消息,才对消费者可见。
2.3.3.2 分区副本分配
创建16个分区,默认分配如下(规则自行脑补,我也不会):
手动调整
编写 json 手动设置,略
2.3.3.3 Leader Rebalance
auto.leader.rebalance.enable:开启leader balancing,默认true
当一个broker停止或者crashes时,所有本来将它作为leader的分区将会把leader转移到其他broker上去,极端情况下,会导致同一个leader管理多个分区,导致负载不均衡,同时当这个broker重启时,如果这个broker不再是任何分区的leader,kafka的client也不会从这个broker来读取消息,从而导致资源的浪费。
kafka中有一个被称为优先副本(preferred replicas)的概念。如果一个分区有3个副本,且这3个副本的优先级别分别为0,1,2,根据优先副本的概念,0会作为leader 。当0节点的broker挂掉时,会启动1这个节点broker当做leader。当0节点的broker再次启动后,会自动恢复为此partition的leader。不会导致负载不均衡和资源浪费,这就是leader的均衡机制。
在配置文件conf/ server.properties中配置开启(默认就是开启):auto.leader.rebalance.enable true
解释一下 leader 均衡机制(auto.leader.rebalance.enable=true):
当 partition 1 的 leader,就是 broker.id = 1 的节点挂掉后,那么 leader 0 或 leader 2 成为 partition 1 的 leader,那么 leader 0 或 leader 2 会管理两个 partition 的读写,性能会下 降,当 leader 1 重新启动后,如果开启了 leader 均衡机制,那么 leader 1 会重新成为 partition 1 的 leader,降低 leader 0 或 leader 2 的负载
下面两个参数需开启
auto.leader.rebalance.enable
leader.imbalance.per.broker.percentage:,默认情况下,此设置设置为10,因此Kafka允许多达10%的领导者在非首选副本上,然后再次选举首选副本。
leader.imbalance.check.interval.seconds:默认值300秒。检查leader负载是否平衡的间隔时间。
开启这个参数势必有性能损耗,主要是leader重新分配的消耗,而开启这个参数好处在于请求均摊,服务负载均衡
2.3.4 文件存储
log.segment.bytes: # 默认值 1G。 Kafka 中 log 日志是分成一块块存储的,此配置是指 log 日志划分成块的大小,
log.index.interval.bytes: # 默认 4kb。kafka 里面每当写入了 4kb 大小的日志(.log),然后就往 index 文件里面记录一个索引。 稀疏索引。
2.3.4.1 文件清除策略
log.retention.hours # 优先级最低,默认168,即7天
log.retention.minutes # 优先级中等,覆盖前者
log.retention.ms # 优先级最高,覆盖前者,-1表示没有限制
log.retention.check.interval.ms # 300000=5min 检查周期,注意和上面参数有关联
log.retention.bytes # The maximum size of the log before deleting it
log.cleanup.policy :compact/delete, 默认delete 启用数据删除策略
- 基于时间:默认开启,以segment中所有记录的最大时间戳作为该log文件的时间戳
- 基于空间:默认关闭,超过
log.retention.bytes设置大小,删除最早的segment,默认-1,表示无穷大
如果采用 compact 策略,那么压缩后对于相同key的不同value值,只保留最后一个版本。
压缩后的offset可能是不连续的,比如上图中没有6,当从这些offset消费消息时,将会拿到比这个offset大 的offset对应的消息,实际上会拿到offset为7的消息,并从这个位置开始消费。这种策略只适合特殊场景,比如消息的key是用户ID,value是用户的资料,通过这种压缩策略,整个消息集里就保存了所有用户最新的资料。
2.3.4.2
- Kafka 本身是分布式集群,可以采用分区技术,并行度高
- 读数据采用稀疏索引,可以快速定位要消费的数据
- 顺序写磁盘
- Kafka 的 producer 生产数据,要写入到 log 文件中,写的过程是一直追加到文件末端,为顺序写。官网有数据表明,同样的磁盘,顺序写能到 600M/s,而随机写只有 100K/s。这与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间
- 页缓存 + 零拷贝技术
log.flush.interval.messages :强制页缓存刷写到磁盘的条数,默认是 long 的最大值,9223372036854775807。一般不建议修改,交给系统自己管理。
log.flush.interval.ms :每隔多久,刷数据到磁盘,默认是 null。一般不建议修改,交给系统自己管理
2.4 Consumer
2.4.1 消费者工作流程
整体流程
cluster->consumer
max.poll.records # 一次拉取数据返回消息的最大条数,默认500条
fetch.min.bytes # 每批次最小抓取大小,默认1字节
fetch.max.bytes # 每批次最大抓取大小 默认50m
fetch.max.wait.ms # 一批数据最小值未达到的超时时间,默认500ms
max.poll.interval.ms # 默认值5分钟,表示若5分钟之内消费者没有消费完上一次poll的消息,那么consumer会主动发起离开group的请求
bootstrap.servers # 向 Kafka 集群建立初始连接用到的 host/port 列表。
key.deserializer # 指定接收消息的 key 和 value 的反序列化类型。一定要写全类名。
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。超过该值,该消费者被移除,消费者组执行再平衡。
partition.assignment.strategy # 消 费 者 分 区 分 配 策 略 , 默 认 策 略 是 Range + CooperativeSticky。Kafka 可以同时使用多个分区分配策略。可 以 选 择 的 策 略 包 括 : Range 、 RoundRobin 、 Sticky 、CooperativeSticky
2.4.2 两种消费方式
2.4.3 消费者组
2.4.4 消费者组初始化
heartbeat.interval.ms # consumer发送心跳包的周期,默认3s
session.timeout.ms # 心跳超时时间 默认45s
max.poll.interval.ms # 默认值5分钟,表示若5分钟之内消费者没有消费完上一次poll的消息,那么consumer会主动发起离开group的请求
2.4.5 分区的分配以及再平衡
partition.assignment.strategy 消 费 者 分 区 分 配 策 略 , 默 认 策 略 是 Range + CooperativeSticky。Kafka 可以同时使用多个分区分配策略。可 以 选 择 的 策 略 包 括 : Range 、 RoundRobin 、 Sticky 、CooperativeSticky
2.4.5.1 Range
2.4.5.2 RoundRobin
2.4.5.3 Sticky
粘性分区是 Kafka 从 0.11.x 版本开始引入这种分配策略,首先会尽量均衡的放置分区到消费者上面,在出现同一消费者组内消费者出现问题的时候,会尽量保持原有分配的分区不变化。
2.4.6 Offset偏移量
想要查看需要在 config/consumer.properties 中添加配置 exclude.internal.topics=false 表示允许消费系统主题,主题名:__consumer_offsets
2.4.6.1 自动提交
enable.auto.commit**:**是否开启自动提交offset功能,默认是trueauto.commit.interval.ms:自动提交offset的时间间隔,默认是5s
当设置 enable.auto.commit 为 true,Kafka 会保证在开始调用 poll 方法时,提交上次 poll 返回的所有消息。从顺序上来说,poll 方法的逻辑是先提交上一批消息的位移,再处理下一批消息,因此它能保证不出现消费丢失的情况。
自动提交位移的一个问题在于,它可能会出现重复消费。
如果设置 enable.auto.commit **为 true,Consumer 按照 auto.commit.interval.ms设置的值(默认5秒)自动提交一次位移。**我们假设提交位移之后的 3 秒发生了 Rebalance 操作。在 Rebalance 之后,所有 Consumer 从上一次提交的位移处继续消费,但该位移已经是 3 秒前的位移数据了,**故在 Rebalance 发生前 3 秒消费的所有数据都要重新再消费一次。**虽然你能够通过减少 auto.commit.interval.ms 的值来提高提交频率,但这么做只能缩小重复消费的时间窗口,不可能完全消除它。这是自动提交机制的一个缺陷。
2.4.6.2 手动提交
有两个方法:
- commitSync
- commitAsync
commitAsync 不能够替代 commitSync。commitAsync 的问题在于,出现问题时它不会自动重试。因为它是异步操作,倘若提交失败后自动重试,那么它重试时提交的位移值可能早已经“过期”或不是最新值了。因此,异步提交的重试其实没有意义,所以 commitAsync 是不会重试的。
2.4.6.3 手动提交和自动提交中的 reblance 问题
- 如果设置为手动提交,当集群满足 reblance 的条件时,集群会直接 reblance,不会等待所有消息被消费完,这会导致所有未被确认的消息会重新被消费,会出现重复消费的问题
- 如果设置为自动提交,当集群满足 reblance 的条件时,集群不会马上 reblance,而是会等待所有消费者消费完当前消息,或者等待消费者超时(等待过程中会报如下 warning), 之后才会 reblance。
2.4.6.4 指定偏移量提交
auto.offset.reset earliest | latest | none 默认是 latest。
当 Kafka 中没有初始偏移量(消费者组第一次消费)或服务器上不再存在当前偏移量时(例如该数据已被删除),该怎么办?
- earliest:自动将偏移量重置为最早的偏移量,--from-beginning。
- latest(默认值):自动将偏移量重置为最新偏移量。
- none:如果未找到消费者组的先前偏移量,则向消费者抛出异常。
- anything else:throw exception to the consumer.
2.4.6.5 指定时间消费
简单Demo
Set<TopicPartition> assignment = new HashSet<>();
while (assignment.size() == 0) {
kafkaConsumer.poll(Duration.ofSeconds(1));
// 获取消费者分区分配信息(有了分区分配信息才能开始消费)
assignment = kafkaConsumer.assignment();
}
HashMap<TopicPartition, Long> timestampToSearch = new HashMap<>();
// 封装集合存储,每个分区对应一天前的数据
for (TopicPartition topicPartition : assignment) {
timestampToSearch.put(topicPartition, System.currentTimeMillis() - 1 * 24 * 3600 * 1000);
}
// 获取从 1 天前开始消费的每个分区的 offset
Map<TopicPartition, OffsetAndTimestamp> offsets = kafkaConsumer.offsetsForTimes(timestampToSearch);
// 遍历每个分区,对每个分区设置消费时间。
for (TopicPartition topicPartition : assignment) {
OffsetAndTimestamp offsetAndTimestamp =
offsets.get(topicPartition);
// 根据时间指定开始消费的位置
if (offsetAndTimestamp != null) {
kafkaConsumer.seek(topicPartition, offsetAndTimestamp.offset());
}
}
3.SpringBoot整合Demo
@RestController
public class KafkaController {
private final static String TOPIC_NAME = "first";
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@RequestMapping("/send/first")
public String send(@RequestParam("msg") String msg) {
kafkaTemplate.send(TOPIC_NAME, "key", msg);
return String.format("消息 %s 发送成功!", msg);
}
@RequestMapping("/send/second")
public String send2(@RequestParam("msg") String msg) {
kafkaTemplate.send(TOPIC_NAME, "key", msg);
return String.format("消息 %s 发送成功!", msg);
}
}
@Component
public class MyConsumer {
@KafkaListener(topics = "first", groupId = "default-group")
public void firstConsumer(ConsumerRecord<String, String> record, Acknowledgment ack) {
String value = record.value();
System.out.println("first message: " + value);
System.out.println("first record: " + record);
ack.acknowledge();
}
//配置多个消费组
@KafkaListener(topics = "second", groupId = "default-group")
public void secondConsumer(ConsumerRecord<String, String> record, Acknowledgment ack) {
String value = record.value();
System.out.println("second message: " + value);
System.out.println("second record: " + record);
ack.acknowledge();
}
}
spring:
application:
name: SpringStudy
kafka:
bootstrap-servers: 192.168.150.100:9092,192.168.150.101:9092,192.168.150.102:9092
producer: # 生产者
retries: 3 # 设置大于0的值,则客户端会将发送失败的记录重新发送
batch-size: 16384
buffer-memory: 33554432
acks: all
# 指定消息key和消息体的编解码方式
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
consumer:
group-id: default-group
enable-auto-commit: false
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
listener:
# 当每一条记录被消费者监听器(ListenerConsumer)处理之后提交
# RECORD
# 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后提交
# BATCH
# 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,距离上次提交时间大于TIME时提交
# TIME
# 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,被处理record数量大于等于COUNT时提交
# COUNT
# TIME | COUNT 有一个条件满足时提交
# COUNT_TIME
# 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后, 手动调用Acknowledgment.acknowledge()后提交
# MANUAL
# 手动调用Acknowledgment.acknowledge()后立即提交,一般使用这种
# MANUAL_IMMEDIATE
ack-mode: manual_immediate
# 应用服务 WEB 访问端口
server:
port: 8080