Kafka从入门到精通:分布式消息队列实战指南(Zookeeper 模式)

Apache Kafka是一个开源的分布式流处理平台,最初由LinkedIn开发并于2011年开源。它被设计用来处理实时数据流,具有高吞吐量、低延迟和可扩展性强的特点。

kafka官网:http://kafka.apache.org/

1. 消息队列介绍

消息队列(Message Queue,MQ)是一种异步通信的中间件技术 ,用于在分布式系统或应用组件之间传递消息。它通过解耦发送者(生产者)和接收者(消费者),提高系统的可扩展性、可靠性和灵活性。

核心功能:

  • 异步通信: 生产者和消费者无需同时在线,发送方无需等待处理完成
  • 解耦合: 应用程序通过消息交互,减少直接依赖关系
  • 流量削峰: 缓冲高峰期请求,平滑处理系统负载
  • 可靠性保障: 消息持久化存储,支持重试机制确保不丢失

常见消息模式:

  • 点对点:一个消息只能被一个消费者处理,适用于任务分发场景。
  • 发布/订阅:一个消息可被多个消费者订阅,适用于广播或事件通知。
  • 请求/响应:消费者处理消息后,将结果返回给生产者。

消息中间件对比:

|--------|---------------------|---------------|---------------|-----------------------|
| 特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
| 开发语言 | java | erlang | java | scala |
| 单机吞吐量 | 万级 | 万级 | 10万级 | 100万级 |
| 时效性 | ms | us | ms | ms级以内 |
| 可用性 | 高(主从) | 高(主从) | 非常高(分布式) | 非常高(分布式) |
| 功能特性 | 成熟的产品、较全的文档、各种协议支持好 | 并发能力强、性能好、延迟低 | MQ功能比较完善,扩展性佳 | 只支持主要的MQ功能,主要应用于大数据领域 |

消息中间件选择建议:

|-----------|------------------------------------------|
| 消息中间件 | 建议 |
| Kafka | 追求高吞吐量,适合产生大量数据的互联网服务的数据收集业务 |
| RocketMQ | 可靠性要求很高的金融互联网领域,稳定性高,经历了多次阿里双11考验 |
| RabbitMQ | 性能较好,社区活跃度高,数据量没有那么大,优先选择功能比较完备的RabbitMQ |

2. Kafka 核心名词解释

基础概念:

  • Broker: Kafka 服务器节点,负责存储数据和处理客户端请求。
  • Topic: 消息主题,是消息分类的逻辑概念。
  • Partition: 物理分区,每个 Topic 可以分为多个分区以实现并行处理。
  • Replica: 副本,每个分区可以有多个副本以提高可用性。

生产消费相关:

  • Producer: 消息生产者,向 Topic 发布消息的客户端应用。
  • Consumer: 消息消费者,订阅并处理 Topic 中消息的客户端应用。
  • Consumer Group: 消费者组,多个消费者组成的逻辑组,实现负载均衡。

数据管理:

  • Offset: 偏移量,分区内每条消息的唯一标识,表示消息在分区中的位置。
  • LEO (Log End Offset): 日志末端偏移量,表示下一条待写入消息的 offset。
  • HW (High Watermark): 高水位线,表示已提交消息的最大 offset。

集群管理:

  • Leader: 主副本,负责处理读写请求。
  • Follower: 从副本,负责从 Leader 同步数据。
  • ISR (In-Sync Replicas): 与 Leader 保持同步的副本集合。
  • Controller: Kafka 集群控制器,负责分区 Leader 选举等管理操作。

3. kafka安装配置

以 Windows 单机部署为例:

  1. 下载 Zookeeper 并解压。

下载地址:http://archive.apache.org/dist/zookeeper/

  1. 将 conf 目录下 zoo_sample.cfg 复制或重命名为 zoo.cfg,修改 dataDir 配置。示例:
bash 复制代码
# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial 
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between 
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just 
# example sakes.
dataDir=D:/apache-zookeeper-3.9.2/data
# the port at which the clients will connect
clientPort=2181
# the maximum number of client connections.
# increase this if you need to handle more clients
#maxClientCnxns=60
#
# Be sure to read the maintenance section of the 
# administrator guide before turning on autopurge.
#
# https://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance
#
# The number of snapshots to retain in dataDir
#autopurge.snapRetainCount=3
# Purge task interval in hours
# Set to "0" to disable auto purge feature
#autopurge.purgeInterval=1

## Metrics Providers
#
# https://prometheus.io Metrics Exporter
#metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider
#metricsProvider.httpHost=0.0.0.0
#metricsProvider.httpPort=7000
#metricsProvider.exportJvmInfo=true
  1. 下载 Kafka 并解压。

下载地址:https://kafka.apache.org/community/downloads/

  1. 将 config 目录下 zookeeper.properties 文件的 dataDir 配置修改。示例:
bash 复制代码
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
# 
#    http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# the directory where the snapshot is stored.
dataDir=D:/apache-zookeeper-3.9.2/data
# the port at which the clients will connect
clientPort=2181
# disable the per-ip limit on the number of connections since this is a non-production config
maxClientCnxns=0
# Disable the adminserver by default to avoid port conflicts.
# Set the port to something non-conflicting if choosing to enable this
admin.enableServer=false
# admin.serverPort=8080
  1. 将 config 目录下 server.properties 文件的 log.dirs 配置修改。示例:
bash 复制代码
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

#
# This configuration file is intended for use in ZK-based mode, where Apache ZooKeeper is required.
# See kafka.server.KafkaConfig for additional details and defaults
#

############################# Server Basics #############################

# The id of the broker. This must be set to a unique integer for each broker.
broker.id=0

############################# Socket Server Settings #############################

# The address the socket server listens on. If not configured, the host name will be equal to the value of
# java.net.InetAddress.getCanonicalHostName(), with PLAINTEXT listener name, and port 9092.
#   FORMAT:
#     listeners = listener_name://host_name:port
#   EXAMPLE:
#     listeners = PLAINTEXT://your.host.name:9092
#listeners=PLAINTEXT://:9092

# Listener name, hostname and port the broker will advertise to clients.
# If not set, it uses the value for "listeners".
#advertised.listeners=PLAINTEXT://your.host.name:9092

# Maps listener names to security protocols, the default is for them to be the same. See the config documentation for more details
#listener.security.protocol.map=PLAINTEXT:PLAINTEXT,SSL:SSL,SASL_PLAINTEXT:SASL_PLAINTEXT,SASL_SSL:SASL_SSL

# The number of threads that the server uses for receiving requests from the network and sending responses to the network
num.network.threads=3

# The number of threads that the server uses for processing requests, which may include disk I/O
num.io.threads=8

# The send buffer (SO_SNDBUF) used by the socket server
socket.send.buffer.bytes=102400

# The receive buffer (SO_RCVBUF) used by the socket server
socket.receive.buffer.bytes=102400

# The maximum size of a request that the socket server will accept (protection against OOM)
socket.request.max.bytes=104857600


############################# Log Basics #############################

# A comma separated list of directories under which to store log files
log.dirs=D:/kafka_2.12-3.5.1/data

# The default number of log partitions per topic. More partitions allow greater
# parallelism for consumption, but this will also result in more files across
# the brokers.
num.partitions=1

# The number of threads per data directory to be used for log recovery at startup and flushing at shutdown.
# This value is recommended to be increased for installations with data dirs located in RAID array.
num.recovery.threads.per.data.dir=1

############################# Internal Topic Settings  #############################
# The replication factor for the group metadata internal topics "__consumer_offsets" and "__transaction_state"
# For anything other than development testing, a value greater than 1 is recommended to ensure availability such as 3.
offsets.topic.replication.factor=1
transaction.state.log.replication.factor=1
transaction.state.log.min.isr=1

############################# Log Flush Policy #############################

# Messages are immediately written to the filesystem but by default we only fsync() to sync
# the OS cache lazily. The following configurations control the flush of data to disk.
# There are a few important trade-offs here:
#    1. Durability: Unflushed data may be lost if you are not using replication.
#    2. Latency: Very large flush intervals may lead to latency spikes when the flush does occur as there will be a lot of data to flush.
#    3. Throughput: The flush is generally the most expensive operation, and a small flush interval may lead to excessive seeks.
# The settings below allow one to configure the flush policy to flush data after a period of time or
# every N messages (or both). This can be done globally and overridden on a per-topic basis.

# The number of messages to accept before forcing a flush of data to disk
#log.flush.interval.messages=10000

# The maximum amount of time a message can sit in a log before we force a flush
#log.flush.interval.ms=1000

############################# Log Retention Policy #############################

# The following configurations control the disposal of log segments. The policy can
# be set to delete segments after a period of time, or after a given size has accumulated.
# A segment will be deleted whenever *either* of these criteria are met. Deletion always happens
# from the end of the log.

# The minimum age of a log file to be eligible for deletion due to age
log.retention.hours=168

# A size-based retention policy for logs. Segments are pruned from the log unless the remaining
# segments drop below log.retention.bytes. Functions independently of log.retention.hours.
#log.retention.bytes=1073741824

# The maximum size of a log segment file. When this size is reached a new log segment will be created.
#log.segment.bytes=1073741824

# The interval at which log segments are checked to see if they can be deleted according
# to the retention policies
log.retention.check.interval.ms=300000

############################# Zookeeper #############################

# Zookeeper connection string (see zookeeper docs for details).
# This is a comma separated host:port pairs, each corresponding to a zk
# server. e.g. "127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002".
# You can also append an optional chroot string to the urls to specify the
# root directory for all kafka znodes.
zookeeper.connect=localhost:2181

# Timeout in ms for connecting to zookeeper
zookeeper.connection.timeout.ms=18000


############################# Group Coordinator Settings #############################

# The following configuration specifies the time, in milliseconds, that the GroupCoordinator will delay the initial consumer rebalance.
# The rebalance will be further delayed by the value of group.initial.rebalance.delay.ms as new members join the group, up to a maximum of max.poll.interval.ms.
# The default value for this is 3 seconds.
# We override this to 0 here as it makes for a better out-of-the-box experience for development and testing.
# However, in production environments the default value of 3 seconds is more suitable as this will help to avoid unnecessary, and potentially expensive, rebalances during application startup.
group.initial.rebalance.delay.ms=0
  1. 编写 bat 脚本双击启动,如创建 kafka-start.bat 启动:
bash 复制代码
@echo off
set KAFKA_HOME=D:\kafka_2.12-3.5.1
set ZOOKEEPER_HOME=D:\apache-zookeeper-3.9.2
set KAFKA_LOG_DIR=%KAFKA_HOME%\logs

echo Deleting Kafka tmp and logs directories...
rd /s /q "%KAFKA_HOME%\data"
rd /s /q "%KAFKA_LOG_DIR%"

echo Deleting Zookeeper data directory...
rd /s /q "%ZOOKEEPER_HOME%\data"

echo Starting Zookeeper Server...
start cmd /k "%ZOOKEEPER_HOME%\bin\zkServer.cmd"

echo Waiting for Zookeeper to start...
timeout /t 10

echo Starting Kafka Server...
"%KAFKA_HOME%\bin\windows\kafka-server-start.bat" "%KAFKA_HOME%\config\server.properties"

echo Kafka Server started.

pause

注意:

  • Windows 启动 Kafka 需要将 Kafka 的 data 目录和 logs 目录,以及 Zookeeper 的 data 目录删除,避免启动失败。
  • Windows 启动目录树太深容易报错。
  • Kafka 对 windows 文件系统没有很好的支持,过高版本容易报错。
  • Windows 系统中由于权限或进程锁定的问题,删除 topic 会导致 Kafka 服务节点异常关闭。

Linux 集群部署:

https://blog.csdn.net/kersixy/article/details/142324612?spm=1001.2014.3001.5502https://blog.csdn.net/kersixy/article/details/142324612?spm=1001.2014.3001.5502

注意:

Windows 启动 Kafka 需要删除 server.properties 配置文件下的 log.dirs 配置指定的目录文件。

4. Kafka常用命令

4.1 Topic管理命令

4.1.1 创建Topic

创建一个名为 demo 的 topic,分区数为3,副本因子为1:

bash 复制代码
bin/kafka-topics.sh --create \
  --bootstrap-server localhost:9092 \
  --replication-factor 1 \
  --partitions 3 \
  --topic demo

说明:

  • Windows 执行 Kafka 命令需要将 bin目录下的 shell 脚本换成 bin\windows 目录下的 bat脚本。
  • 如果为 docker 启动的服务,则用 docker exec -it + 容器名 + 命令 或直接进入容器内部执行命令。例如:
bash 复制代码
docker exec -it kafka kafka-topics.sh --create \
  --bootstrap-server localhost:9092 \
  --replication-factor 1 \
  --partitions 3 \
  --topic demo

4.1.2 查看Topic列表

bash 复制代码
# 查看所有topic
bin/kafka-topics.sh --list --bootstrap-server localhost:9092

# 查看特定topic
bin/kafka-topics.sh --list --bootstrap-server localhost:9092 | grep "demo"

4.1.3 查看Topic详情

bash 复制代码
# 查看指定topic的详细信息
bin/kafka-topics.sh --describe --topic demo --bootstrap-server localhost:9092

# 查看所有topic的详细信息
bin/kafka-topics.sh --describe --bootstrap-server localhost:9092

4.1.4 删除Topic

bash 复制代码
# 删除指定topic
bin/kafka-topics.sh --delete --topic demo --bootstrap-server localhost:9092

4.1.5 修改Topic配置

bash 复制代码
# 增加分区数(注意:分区数只能增加不能减少)
bin/kafka-topics.sh --alter --topic demo --partitions 5 --bootstrap-server localhost:9092

# 修改topic配置
bin/kafka-configs.sh --alter --entity-type topics --entity-name demo \
  --add-config retention.ms=86400000 \
  --bootstrap-server localhost:9092

4.2 生产者命令

4.2.1 发送消息

bash 复制代码
# 启动控制台生产者
bin/kafka-console-producer.sh --topic demo --bootstrap-server localhost:9092

# 发送消息时指定acks参数
bin/kafka-console-producer.sh --topic demo \
  --broker-list localhost:9092 \
  --producer-property acks=all

# 从文件读取消息发送
bin/kafka-console-producer.sh --topic demo --bootstrap-server localhost:9092 < messages.txt

说明:

如果需要指定 key,可以在命令后添加

bash 复制代码
--property "parse.key=true" --property "key.separator=,"

其中:

  • Kafka 版本 >= 2.0 **默认禁用 Key 解析,**必须使用 --property "parse.key=true",低版本可以不指定;
  • key.separator 的值必须与您在消息中使用的分隔符一致(如 ,、| 等),文件中每行必须是 key,message 格式,才能正确解析 Key,示例:
bash 复制代码
key1,message1
key2,message2
key3,Hello World

4.3 消费者命令

4.3.1 消费消息

bash 复制代码
# 启动控制台消费者(从最新消息开始消费)
bin/kafka-console-consumer.sh --topic demo --bootstrap-server localhost:9092

# 从最早消息开始消费
bin/kafka-console-consumer.sh --topic demo \
  --bootstrap-server localhost:9092 \
  --from-beginning

# 消费消息时显示key和value
bin/kafka-console-consumer.sh --topic demo \
  --bootstrap-server localhost:9092 \
  --from-beginning \
  --property print.key=true \
  --property key.separator=,

# 使用正则表达式消费匹配的topic(test-开头的topic)
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 \
  --whitelist "test-.*" \
  --from-beginning

4.3.2 消费者组管理

bash 复制代码
# 指定消费者组
bin/kafka-console-consumer.sh --topic demo \
  --bootstrap-server localhost:9092 \
  --group my-consumer-group

# 查看消费者组列表
bin/kafka-consumer-groups.sh --list --bootstrap-server localhost:9092

# 查看消费者组详细信息
bin/kafka-consumer-groups.sh --describe --group my-consumer-group --bootstrap-server localhost:9092

# 查看消费者组消费状态
bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
  --describe \
  --group my-consumer-group \
  --verbose

# 重置消费者组offset
bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
  --group my-consumer-group \
  --reset-offsets \
  --to-earliest \
  --execute \
  --topic demo

4.4 Offset管理命令

4.4.1 查看Offset信息

bash 复制代码
# 查看topic的offset信息
bin/kafka-run-class.sh org.apache.kafka.tools.GetOffsetShell \
  --topic demo \
  --broker-list localhost:9092 \
  --time -1  # -1表示最新offset,-2表示最早offset

# 查看消费者组的offset情况
bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
  --describe \
  --group my-consumer-group

说明:

如果提示:错误: 找不到或无法加载主类 kafka.tools.GetOffsetShell可通过该命令找包路径:

bash 复制代码
jar tf libs/kafka-tools-3.8.0.jar | grep GetOffsetShell

4.5 分区管理命令

4.5.1 查看分区信息

bash 复制代码
# 查看topic分区信息
bin/kafka-topics.sh --describe --topic demo --bootstrap-server localhost:9092

# 查看所有topic的分区信息
bin/kafka-topics.sh --describe --bootstrap-server localhost:9092

4.5.2 手动分配分区

1. 创建主题列表文件

bash 复制代码
cat > topics-to-move.json <<EOF
{
  "topics": [
    {"topic": "demo"}
  ],
  "version": 1
}
EOF

topics-to-move.json 文件解释:

1. "topics" 数组:

  • 指定需要进行分区重分配的主题列表
  • 每个主题以对象形式表示:{"topic": "主题名"}
  • 如果需要重分配多个主题,只需添加更多对象

2. "version": 1:

  • 这是文件格式的版本号
  • Kafka 使用这个版本号确保正确解析文件
  • 当前 Kafka 版本使用版本 1

2. 生成重分配计划

bash 复制代码
bin/kafka-reassign-partitions.sh --bootstrap-server localhost:9092 \
  --topics-to-move-json-file topics-to-move.json \
  --broker-list "0,1,2" \
  --generate

说明:

  • --broker-list "0,1,2" 中的 0,1,2 是 Kafka 集群中 Broker 的唯一标识符(broker.id),表示将分区副本分配到这些 Broker 上。其中,broker.id 是 Kafka 配置文件(server.properties)中定义的参数。

输出示例

bash 复制代码
Current partition replica assignment
{"version":1,"partitions":[{"topic":"demo","partition":0,"replicas":[1],"log_dirs":["any"]},...]}
Proposed partition reassignment configuration
{"version":1,"partitions":[{"topic":"demo","partition":0,"replicas":[2],"log_dirs":["any"]},...]}

3. 保存建议配置

从输出中只提取 "Proposed partition reassignment configuration" 后的 JSON,保存为 reassignment.json:

bash 复制代码
echo '{"version":1,"partitions":[{"topic":"demo","partition":0,"replicas":[2],"log_dirs":["any"]},...]}'> reassignment.json

4. 执行重分配

bash 复制代码
bin/kafka-reassign-partitions.sh --bootstrap-server localhost:9092 \
  --reassignment-json-file reassignment.json \
  --execute

5. 验证重分配

bash 复制代码
bin/kafka-reassign-partitions.sh --bootstrap-server localhost:9092 \
  --reassignment-json-file reassignment.json \
  --verify

Kafka 分区重分配作用:

  • 集群扩容:添加新 Broker 后,需要将分区副本重新分配到新节点;
  • 负载均衡:某些 Broker 负载过高,需要重新分配分区;
  • 硬件维护:需要将分区从即将下线的 Broker 迁移;
  • 性能优化:根据数据访问模式调整分区分布。

4.6 配置管理命令

4.6.1 查看配置

bash 复制代码
# 查看topic配置
bin/kafka-configs.sh --describe --entity-type topics --entity-name demo --bootstrap-server localhost:9092

# 查看broker配置(0 为 broker.id)
bin/kafka-configs.sh --describe --entity-type brokers --entity-name 0 --bootstrap-server localhost:9092

4.6.2 修改配置

bash 复制代码
# 修改topic配置
bin/kafka-configs.sh --alter --entity-type topics --entity-name demo \
  --add-config retention.ms=86400000 \
  --bootstrap-server localhost:9092

# 删除topic配置(恢复默认值)
bin/kafka-configs.sh --alter --entity-type topics --entity-name demo \
  --delete-config retention.ms \
  --bootstrap-server localhost:9092

4.7 性能测试命令

4.7.1 生产者性能测试

bash 复制代码
# 生产者性能测试
bin/kafka-producer-perf-test.sh --topic demo \
  --num-records 100000 \
  --record-size 1000 \
  --throughput 10000 \
  --producer-props bootstrap.servers=localhost:9092

4.7.2 消费者性能测试

bash 复制代码
# 消费者性能测试
bin/kafka-consumer-perf-test.sh --bootstrap-server localhost:9092 \
  --topic demo \
  --messages 100000

4.8 日志和元数据命令

4.8.1 查看日志信息

bash 复制代码
# 查看topic分区日志信息
bin/kafka-run-class.sh kafka.tools.DumpLogSegments \
  --files /export/server/kafka/data/demo-1/00000000000000000000.log

说明:

files 的文件位置为配置文件 server.properties 的 log.dirs 配置项。

4.8.2 查看集群元数据

bash 复制代码
# 查看集群元数据
bin/kafka-broker-api-versions.sh --bootstrap-server localhost:9092

4.9 常用参数说明

4.9.1 通用参数

bash 复制代码
--bootstrap-server <host:port>  # 指定Kafka服务器地址
--topic <topic-name>            # 指定topic名称
--group <group-name>            # 指定消费者组名称
--from-beginning               # 从最早的消息开始消费
--timeout <timeout>            # 设置超时时间

4.9.2 生产者参数

bash 复制代码
--producer-property <prop=value>  # 设置生产者属性
--sync                           # 同步发送消息
--compression-codec <codec>      # 设置压缩编码

--compression-codec 支持的压缩算法:

算法 缩写 Kafka 版本支持 特点
GZIP gzip 早期版本 压缩率高,但速度较慢
Snappy snappy 早期版本 速度极快,压缩率较低
LZ4 lz4 早期版本 速度非常快,压缩率适中
Zstandard zstd Kafka 2.1.0+ 压缩率高,速度中等,可调优
None none 所有版本 不进行压缩

4.9.3 消费者参数

bash 复制代码
--consumer-property <prop=value>  # 设置消费者属性
--formatter <formatter_class>    # 设置消息格式化类
--max-messages <num>            # 最大消费消息数量

5. Kafka 连接工具(Kafka Tool)

安装地址:https://www.kafkatool.com/download.html

5.1 分组管理

5.1.1 创建分组

方式一:鼠标右键 Clusters,左键 Add Group...。

方式二:选中需要添加子分组的分组,再点击右下角的 Add Group。

5.1.2 修改/删除分组

方式一:鼠标右键 Clusters,左键 Rename Group(修改分组)/ Delete Group (删除分组)。

方式二:选中需要修改的分组,再点击右下角的 Rename Group(修改分组)/ Delete Group (删除分组)。

5.2 Kafka 管理

5.2.1 创建 Kafka 连接

方式一:File -> Add New Connection...,并填写信息。(只能在根分组里新增)

方式二:鼠标右键分组,鼠标左键 Add New Connection...,并填写信息。

方式三:选中分组,点击右下角的 Add Connection,并填写信息。

5.2.2 Kafka Tool 连接、重连、断连 Kafka,更新、删除 Kafka 信息

5.3 Topic 管理

5.3.1 添加 Topic

连接 Kafka,鼠标右键 Topics -> Create Topic,并填写信息。

5.3.2 删除 Topic

鼠标右键 Topics -> Delete。

5.3.3 查看 Topic 消息

  1. 连接 Kafka;
  2. 选中需要查看的 topic;
  3. 切换 Data 页面;
  4. 点击绿色启动按钮。

properties页面说明:

parations 分区页面信息:

config 配置信息页面:

5.3.4 向 Topic 添加消息

6. 生产者

Kafka 生产者采用主线程- Sender 线程的双线程架构,工作流程如下:

  • 主线程(用户线程)
    • 拦截器(Interceptors):在消息发送前进行预处理;
    • 序列化器(Serializer):将Java对象的Key和Value序列化为字节数组;
    • 分区器(Partitioner):决定消息应该被发送到Topic的哪个分区。
  • Sender线程(I/O线程)
    • 消息累加器(RecordAccumulator):缓存消息,实现批处理;
    • NetworkClient:将已满批次的消息批量发送到Kafka集群。

6.1 属性配置

见官网:https://kafka.apache.org/35/configuration/producer-configs/

6.1.1 必须配置的属性

配置属性 说明
bootstrap.servers Kafka集群地址(格式:host1:port1,host2:port2
key.serializer Key的序列化器类(如StringSerializer
value.serializer Value的序列化器类(如StringSerializer

6.1.2 常用可选配置

  1. 消息可靠性配置
配置属性 默认值 说明 生产环境建议
acks all 消息确认级别: - 0:不等待确认 - 1:等待Leader确认 - all(-1):等待所有ISR副本确认 根据数据场景进行设置
retries 2147483647 重试次数 0 或 2147483647(整型最大值)
retry.backoff.ms 100 重试间隔(毫秒)
enable.idempotence true 是否启用幂等性(保证消息不重复) 根据数据场景进行设置
  1. 批处理与性能配置
配置属性 默认值 说明 生产环境建议
batch.size 16384 批量发送大小(字节)。此大小会和数据最大估计值进行比较,取大值。估值=61+21+(keySize+1+valueSize+1+1)
linger.ms 0 等待更多消息填满batch的时间(毫秒)
buffer.memory 33554432 缓冲区大小(字节) 33554432(32MB)
max.in.flight.requests.per.connection 5 每个连接上可发送但未确认的最大请求数 小于等于5
  1. 分区与拦截器配置
配置属性 默认值 说明 生产环境建议
partitioner.class null 自定义分区器类 仅需自定义时配置
partitioner.ignore.keys false 是否忽略Key进行分区 true(无需Key分区时)
interceptor.classes "" 生产者拦截器类
  1. 其他重要配置
配置属性 默认值 说明
compression.type none 压缩类型:nonegzipsnappylz4zstd
request.timeout.ms 30000 请求超时时间(毫秒)
max.block.ms 60000 生产者等待缓冲区空间的最长时间

6.2 代码实现

6.2.1 添加依赖

版本为 Kafka 部署的版本。

XML 复制代码
<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>3.5.1</version>
</dependency>

6.2.2 发送消息

Kafka生产者支持三种发送模式:

6.2.2.1 方式一:异步发送
java 复制代码
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;

import java.util.HashMap;
import java.util.Map;

public static void main(String[] args) {
    // 配置参数
    Map<String, Object> configMap = new HashMap<>();
    configMap.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
    configMap.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
    configMap.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");

    // 创建生产者
    KafkaProducer<String, String> producer = new KafkaProducer<>(configMap);
    // 创建数据,第一个参数是主题,第二个参数是key,第三个参数是value
    ProducerRecord<String, String> record = new ProducerRecord<>("test", "key", "value");
    // 发送数据
    producer.send(record);
    // 关闭生产者
    producer.close();
}
6.2.2.2 方式二:同步发送
java 复制代码
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

public static void main(String[] args) throws ExecutionException, InterruptedException {
    // 配置参数
    Map<String, Object> configMap = new HashMap<>();
    configMap.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
    configMap.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
    configMap.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");

    // 创建生产者
    KafkaProducer<String, String> producer = new KafkaProducer<>(configMap);
    // 创建数据,第一个参数是主题,第二个参数是key,第三个参数是value
    ProducerRecord<String, String> record = new ProducerRecord<>("test", "key", "value");
    // 发送数据
    Future<RecordMetadata> future = producer.send(record);
    // 阻塞等待
    future.get();
    // 关闭生产者
    producer.close();
}
6.2.2.3 方式三:异步回调发送
java 复制代码
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;

import java.util.HashMap;
import java.util.Map;

public static void main(String[] args) {
    // 配置参数
    Map<String, Object> configMap = new HashMap<>();
    configMap.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
    configMap.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
    configMap.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");

    // 创建生产者
    KafkaProducer<String, String> producer = new KafkaProducer<>(configMap);
    // 创建数据,第一个参数是主题,第二个参数是key,第三个参数是value
    ProducerRecord<String, String> record = new ProducerRecord<>("test", "key", "value");
    // 发送数据
    producer.send(record, (metadata, e) -> {
        if (e != null) {
            e.printStackTrace();
        } else {
            System.out.println("发送成功:" + metadata.toString());
        }
    });
    // 关闭生产者
    producer.close();
}

6.2.3 消息分区

6.2.3.1 指定分区

在发送消息时,通过 ProducerRecord 明确指定了分区,示例:

java 复制代码
ProducerRecord<String, String> record = new ProducerRecord<>("test",2, "key", "value");

Kafka会直接将消息发送到指定的分区(2号分区)。

6.2.3.2 未指定分区

Kafka 未指定分区有三种分区分配策略。

6.2.3.2.1 DefaultPartitioner 默认分区策略
  • 如果未指定数据Key,或不使用Key选择分区,那么Kafka会采用优化后的粘性分区策略进行分区选择,详情见 UniformStickyPartitioner 粘性分区策略。
  • 如果指定了数据Key,且使用Key选择分区的场合,采用murmur2非加密散列算法(类似于hash)计算数据Key序列化后的值的散列值,然后对主题分区数量模运算取余,最后的结果就是分区编号。
6.2.3.2.2 UniformStickyPartitioner 粘性分区策略

无论是否有 Key,都采用粘性分区策略。

  • 可用分区<1:从所有分区中随机选择;
  • 可用分区=1:选择当前分区;
  • 可用分区>1:从所有可用分区中随机选择。
6.2.3.2.3 RoundRobinPartitioner 轮询分区策略

无论是否有 Key,都采用轮询策略:

  • 可用分区为空:从所有分区中轮询;
  • 可用分区不为空:从所有可用分区中轮询。
6.2.3.3 自定义分区
  1. 创建一个实现 Partitioner 的类:
java 复制代码
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;

import java.util.Map;

public class MyPartitioner implements Partitioner {
    /**
     * 分区函数,用于确定消息应该发送到哪个分区
     *
     * @param s       表示主题名称(topic)
     * @param o       表示消息的键(key)对象
     * @param bytes   表示消息键的字节数组形式
     * @param o1      表示消息的值(value)对象
     * @param bytes1  表示消息值的字节数组形式
     * @param cluster 表示Kafka集群的元数据信息
     * @return 返回分区索引号
     */
    @Override
    public int partition(String s, Object o, byte[] bytes, Object o1, byte[] bytes1, Cluster cluster) {
        return 0;
    }

    /**
     * 关闭分区器并释放相关资源
     * 为空表示不需要执行任何清理操作
     */
    @Override
    public void close() {

    }

    /**
     * 配置分区器
     * 通过传入的配置映射来设置分区器的相关属性
     *
     * @param map 包含配置参数的映射集合,键为字符串类型,值可以是任意类型
     */
    @Override
    public void configure(Map<String, ?> map) {

    }
}
  1. 配置分区器

添加 ProducerConfig.PARTITIONER_CLASS_CONFIG 属性,如下图所示:

6.2.4 拦截器

拦截器是Kafka生产者提供的一种可插拔的组件,允许我们在消息发送的不同阶段插入自定义的逻辑。拦截器可以在消息发送前、发送后、以及发送失败时执行一些操作,比如修改消息、记录日志、监控指标等。

注意:

  • 拦截器是可以配置多个的。执行时,会按照声明顺序执行完一个后,再执行下一个。
  • 某一个拦截器如果出现异常,只会跳出当前拦截器逻辑,并不会影响后续拦截器的处理。
6.2.4.1 自定义拦截器
  1. 实现 ProducerInterceptor 接口
java 复制代码
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;

import java.util.Map;

public class MyProducerInterceptor implements ProducerInterceptor<String, String> {
    /**
     * 拦截发送的消息记录
     * 在消息被实际发送到Kafka之前调用此方法
     *
     * @param producerRecord 发送的消息记录,包含主题、分区、键、值等信息
     * @return 处理后的消息记录
     */
    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> producerRecord) {
        return producerRecord;
    }

    /**
     * 处理消息确认回调
     * 当生产者收到消息发送结果的确认时调用此方法
     *
     * @param recordMetadata 记录的元数据信息,包含分区、偏移量、主题等信息
     * @param e              发送过程中可能发生的异常,如果发送成功则为null
     */
    @Override
    public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {

    }

    /**
     * 关闭拦截器并释放相关资源
     * 当生产者关闭时会调用此方法,用于执行清理操作
     */
    @Override
    public void close() {

    }

    /**
     * 配置拦截器
     * 通过传入的配置映射来设置拦截器的相关属性
     *
     * @param map 包含配置参数的映射集合,键为字符串类型,值可以是任意类型
     */
    @Override
    public void configure(Map<String, ?> map) {

    }
}
  1. 通过 ProducerConfig.INTERCEPTOR_CLASSES_CONFIG 配置。

6.3 消息可靠性

6.3.1 acks配置(确认机制)

代码的配置方式:

java 复制代码
configMap.put(ProducerConfig.ACKS_CONFIG, "all");

参数配置说明:

|----------|--------------------------------------------|
| 确认机制 | 说明 |
| acks=0 | 生产者在成功写入消息之前不会等待任何来自服务器的响应,消息有丢失的风险,但是速度最快 |
| acks=1 | 只要集群首领节点收到消息,生产者就会收到一个来自服务器的成功响应 |
| acks=all | 只有当所有参与赋值的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应 |

6.3.2 重试机制 (retries)

生产者从服务器收到的错误有可能是临时性错误,在这种情况下,retries 参数的值决定了生产者可以重发消息的次数,如果达到这个次数,生产者会放弃重试返回错误,默认情况下,生产者会在每次重试之间等待100ms。

代码中配置方式:

java 复制代码
configMap.put(ProducerConfig.RETRIES_CONFIG, 10);

6.3.3 数据幂等性

6.3.3.1 数据重复

Kafka通过ACK应答机制确保数据传输的可靠性,但这一机制在某些特定场景下反而会引发数据重复问题,形成一种典型的"可靠性与重复性"权衡困境。

比如,在初始阶段,生产者顺利将消息发送至 Kafka 集群,Leader 副本也确实成功将数据写入本地日志文件。问题出现在确认环节------当 Leader 尝试向生产者返回 ACK 确认响应时,网络发生瞬时故障或延迟,导致这个 ACK 信号在传输途中丢失。此时,生产者端开始计时等待,但始终无法收到预期的确认信号。随着时间推移,等待时间逐渐接近预设的请求超时阈值,生产者内部开始产生"本次发送可能失败"的判断。

当等待时间正式超过 request.timeout.ms 设定的阈值时,生产者的错误判定变为确定------它坚信本次消息发送已经失败。此时,内置的重试机制被自动激活。根据配置的 retries 参数,生产者会立即或在短暂延迟后,将完全相同的消息内容重新打包,发起新一轮的发送请求。这个重试过程可能发生一次,也可能根据配置重复多次,每次重试都加深了后续数据重复的程度。

当重试请求成功到达Kafka集群时,Leader副本并不会识别这是重复数据,而是将其视为一条全新的合法消息,再次执行完整的写入流程。于是,同一份业务数据被多次写入分区日志,每条内容相同但偏移量不同的记录并排存储。此时数据重复已成既定事实,且这个重复是隐性的------生产者以为只是成功补发了丢失的数据,消费者却会收到多条完全相同的消息,需要自行处理重复带来的业务影响。

生产者视角 Kafka实际状态 结果
超时未收到ACK → 认为发送失败 数据已成功写入Leader日志 认知错位
触发重试机制 收到重复数据 数据重复
记录发送失败指标 存储多条相同消息 数据不一致
6.3.3.2 数据乱序

数据重试功能除了可能会导致数据重复以外,还可能会导致数据乱序。

比如,初始顺序(生产者发送顺序):消息A → 消息B → 消息C → 消息D

实际发送过程:

  1. 消息A发送 → 成功 → 偏移量100
  2. 消息B发送 → 网络超时 → 未确认
  3. 消息C发送 → 成功 → 偏移量101
  4. 消息D发送 → 成功 → 偏移量102
  5. 消息B重试 → 成功 → 偏移量103

最终Kafka分区中的顺序:

  • 偏移量100: 消息A
  • 偏移量101: 消息C # 乱序开始
  • 偏移量102: 消息D
  • 偏移量103: 消息B # 本应在C和D之前
6.3.3.3 数据幂等性

为了解决Kafka传输数据时,所产生的数据重复和乱序问题,Kafka引入了幂等性操作,所谓的幂等性,就是Producer同样的一条数据,无论向Kafka发送多少次,kafka都只会存储一条。注意,这里的同样的一条数据,指的不是内容一致的数据,而是指的不断重试的数据。

默认幂等性是不起作用的,所以如果想要使用幂等性操作,只需要在生产者对象的配置中开启幂等性配置即可。

|-----------------------------------------------|-------------|-------------------------------|
| 配置项 | 配置值 | 说明 |
| enable.idempotence | true | 开启幂等性 |
| max.in.flight.requests.per.connection | 小于等于5 | 每个连接的在途请求数,不能大于5,取值范围为[1,5] |
| acks | all(-1) | 确认应答,固定值,不能修改 |
| retries | >0 | 重试次数,推荐使用Int最大值 |

Kafka 实现幂等性流程:

第一阶段:生产者初始化与PID获取

当生产者首次启动并配置了 enable.idempotence=true 时,会立即向Kafka集群发起一个特殊的初始化请求。这个请求的目标是向Broker申请一个全局唯一的 Producer ID(PID)。Broker 接收到请求后,会从内部的ID生成器中分配一个全新的 PID(例如:PID=42),并将这个 PID 连同初始序列号(通常为0)一起返回给生产者。

生产者将获得的 PID 持久化存储在本地内存中,这个 PID 将成为该生产者实例在整个生命周期内的唯一身份标识。同时,生产者内部会为每个可能发送到的主题分区维护一个序列号计数器,所有计数器的初始值都设置为0。

第二阶段:消息发送与序列号管理

当应用程序调用producer.send()方法发送一条消息时,生产者内部会执行以下操作:

首先,根据消息的Key和分区策略确定目标分区(例如:分区1)。接着,生产者检查该分区对应的本地序列号计数器(假设当前值为3),然后执行原子性递增操作,将序列号更新为4。这个递增操作是线程安全的,确保即使在并发发送场景下也不会出现序列号冲突。

然后,生产者将消息数据、分配的 PID(42)、分区号(1)和序列号(4)打包成一个完整的请求,通过TCP连接发送给对应分区的Leader Broker。值得注意的是,序列号信息是以请求头(Header)的形式附加在消息中的,对应用程序完全透明。

第三阶段:Broker端的幂等性验证

当Leader Broker接收到生产者的请求时,会启动严格的幂等性验证流程:

Broker首先从请求中提取PID(42)、分区号(1)和序列号(4)。然后,它查询本地维护的幂等性状态表,查找该PID在指定分区上记录的最后接受的序列号。

验证逻辑遵循三个严格的规则:

  1. 正常情况(序列号连续递增):如果当前序列号(4)正好比记录的最后序列号(3)大1,Broker判定这是一条合法的非重复消息。验证通过,Broker将消息追加到分区日志中,并更新状态表中该PID-分区对的序列号为4。
  2. 重复消息检测:如果当前序列号(4)小于或等于记录的最后序列号(例如:状态表显示最后序列号已经是4或5),Broker识别出这是一条重复发送的消息。它会静默丢弃这条重复数据,但为了阻止生产者继续重试,仍然会向生产者返回一个成功的ACK响应。
  3. 乱序异常处理:如果当前序列号(4)比记录的最后序列号(假设为2)大超过1(即序列号跳跃),这表明生产者可能出现了状态不一致或发生了数据丢失。Broker会拒绝这条消息,并向生产者返回一个 OutOfOrderSequenceException 异常。

第四阶段:ACK响应与生产者状态更新

一旦消息通过验证并被成功写入分区日志(或确认为重复),Broker会向生产者发送ACK确认响应。生产者收到ACK后,知道该序列号的消息已被Broker接受,此时可以安全地继续发送下一条消息。

如果生产者在预设的 request.timeout.ms 内没有收到ACK响应(可能由于网络问题),它会启动重试机制。重试时,生产者不会重新生成序列号,而是使用与原始请求完全相同的PID、分区号和序列号再次发送。这样即使多次重试,Broker也能根据相同的序列号识别出重复消息,确保数据只被持久化一次。

注意:

  • 幂等性的保证范围限于单个生产者会话。如果生产者崩溃后重启,它会向 Broker 申请一个新的 PID,旧的 PID 及其对应的序列号状态会在一段可配置的时间后由 Broker 自动清理。这意味着,基于 PID 的幂等性无法防止因生产者重启而导致的消息重复。
  • 幂等性无法跨分区实现全局幂等。

6.3.4 生产者事务

为了应对跨会话的幂等性问题,Kafka可以采用事务的方式解决。

6.3.4.1 核心组件

Kafka事务的实现依赖于两个核心内部组件:

  • **事务协调器:**这是Kafka集群中选举出来的一个特殊Broker。每个启用事务的生产者都会与一个唯一的事务协调器绑定。协调器负责管理事务的整个生命周期,包括开始、提交、回滚和状态恢复。
  • **事务日志:**这是一个名为 __transaction_state 的内部主题。协调器将所有事务的状态(如进行中、已提交、已中止)持久化记录在这个日志中。这确保了即使协调器自身发生故障重启,也能从事务日志中恢复所有未决事务的状态,从而保证事务的持久性。
6.3.4.2 事务工作流程

第一阶段:生产者初始化与协调器发现

当生产者配置了唯一的 transactional.id 并调用初始化方法后,它会向Kafka集群查询,找到负责管理它的那个事务协调器。协调器会为该生产者分配一个内部标识符(PID)并增加一个"纪元(Epoch)"编号。纪元号用于防止网络分区或延迟导致出现两个相同的生产者实例(称为"僵尸实例"),确保任何时候只有一个活跃的生产者能操作该事务。

第二阶段:事务开始与消息发送

生产者显式地开始一个事务。此后,所有通过该生产者发送的消息,虽然会正常发送到各自的目标分区,但都会被标记为"事务未提交"状态。对于配置了"读已提交"隔离级别的消费者而言,这些消息暂时是不可见的。

第三阶段:事务提交(两阶段提交协议)

这是事务的核心。当生产者完成所有消息发送并决定提交时,会触发一个两阶段提交协议,以确保所有参与分区的数据一致性。

  • 预提交阶段:生产者通知事务协调器准备提交。协调器首先将事务状态持久化为"预提交"到事务日志中。然后,它会向本事务涉及的所有数据分区的Leader Broker发送一个特殊的"预提交"标记。每个分区收到此标记后,会将其持久化到自己的日志中,但消息仍对"读已提交"的消费者保持不可见。
  • 提交阶段:一旦协调器确认所有分区都成功持久化了"预提交"标记,它就将事务状态在事务日志中最终更新为"已提交"。接着,协调器再次向所有参与分区发送一个"提交"标记。分区在收到"提交"标记后,才会让之前那些被阻塞的消息对"读已提交"的消费者可见。

第四阶段:事务中止(回滚)

如果在事务过程中发生错误,生产者可以发起中止操作。协调器会将事务状态标记为"已中止"并写入日志,然后向所有参与分区发送"中止"标记。各个分区在收到"中止"标记后,会直接丢弃那些处于"未提交"状态的消息,就像它们从未被发送过一样。

6.3.4.3 代码实现
java 复制代码
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.errors.ProducerFencedException;
import org.apache.kafka.common.serialization.StringSerializer;

import java.util.HashMap;
import java.util.Map;

public static void main(String[] args) {
    // 配置参数
    Map<String, Object> configMap = new HashMap<>();
    configMap.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
    configMap.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
    configMap.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
    // 添加事务ID
    configMap.put( ProducerConfig.TRANSACTIONAL_ID_CONFIG, "transaction-id");
    // 设置事务超时时间(毫秒)
    configMap.put( ProducerConfig.TRANSACTION_TIMEOUT_CONFIG, 60000);

    // 创建生产者
    KafkaProducer<String, String> producer = new KafkaProducer<>(configMap);
    try {
        // 初始化事务
        producer.initTransactions();
        // 开启事务
        producer.beginTransaction();
        // 创建数据,第一个参数是主题,第二个参数是key,第三个参数是value
        ProducerRecord<String, String> record = new ProducerRecord<>("test", "key", "value");
        // 发送数据
        producer.send(record, (metadata, e) -> {
            if (e != null) {
                e.printStackTrace();
            } else {
                System.out.println("发送成功:" + metadata.toString());
            }
        });
        // 提交事务
        producer.commitTransaction();
    } catch (ProducerFencedException e) {
        // 处理异常
        e.printStackTrace();
        // 中止事务
        producer.abortTransaction();
    } finally {
        // 关闭生产者
        producer.close();
    }
}

6.3.5 数据传输语义

语义类型 配置要点 数据丢失风险 数据重复风险
至多一次 acks=0retries=0 • 不启用幂等性 :Leader未写入就响应 :不重试,不重复
至少一次 acks=allretries>0 • 不启用幂等性 极低:所有副本确认 :网络重试导致重复
精确一次 enable.idempotence=true • 自动设置acks=allmax.in.flight.requests.per.connection≤5 :强确认机制 :PID+序列号去重

7. Kafka集群

7.1 Controller 选举

在 ZooKeeper 模式下,Kafka 依赖 ZooKeeper 存储集群元数据并协调 Controller 选举。为理解 Controller 的行为,需先了解 Kafka 在 ZooKeeper 中注册的关键路径。

|-----------------------------|------------|-----------------------------------------------------------------------------------------------------------------------|
| 节点 | 类型 | 说明 |
| /admin/delete_topics | 持久化节点 | 配置需要删除的topic,因为删除过程中,可能broker下线,或执行失败,那么就需要在broker重新上线后,根据当前节点继续删除操作,一旦topic所有的分区数据全部删除,那么当前节点的数据才会进行清理 |
| /brokers/ids | 持久化节点 | 服务节点ID标识,只要broker启动,那么就会在当前节点中增加子节点,brokerID不能重复 |
| /brokers/topics | 持久化节点 | 服务节点中的主题详细信息,包括分区,副本 |
| /brokers/seqid | 持久化节点 | seqid主要用于自动生产brokerId |
| /config/changes | 持久化节点 | kafka的元数据发生变化时,会向该节点下创建子节点。并写入对应信息 |
| /config/clients | 持久化节点 | 客户端配置,默认为空 |
| /config/brokers | 持久化节点 | 服务节点相关配置,默认为空 |
| /config/ips | 持久化节点 | IP配置,默认为空 |
| /config/topics | 持久化节点 | 主题配置,默认为空 |
| /config/users | 持久化节点 | 用户配置,默认为空 |
| /consumers | 持久化节点 | 消费者节点,用于记录消费者相关信息 |
| /isr_change_notification | 持久化节点 | ISR列表发生变更时候的通知,在kafka当中由于存在ISR列表变更的情况发生,为了保证ISR列表更新的及时性,定义了isr_change_notification这个节点,主要用于通知Controller来及时将ISR列表进行变更。 |
| /latest_producer_id_block | 持久化节点 | 保存PID块,主要用于能够保证生产者的任意写入请求都能够得到响应。 |
| /log_dir_event_notification | 持久化节点 | 主要用于保存当broker当中某些数据路径出现异常时候,例如磁盘损坏,文件读写失败等异常时候,向ZooKeeper当中增加一个通知序号,Controller节点监听到这个节点的变化之后,就会做出对应的处理操作 |
| /cluster/id | 持久化节点 | 主要用于保存kafka集群的唯一id信息,每个kafka集群都会给分配要给唯一id,以及对应的版本号 |

7.1.1 集群启动阶段

前提:Zookeeper集群(假设为3个节点)已先启动并正常运行。

Broker 1 启动

  • 向 Zookeeper 发起连接,建立会话。

  • 立即尝试在 Zookeeper 的 /controller 路径下创建一个临时节点。

  • 由于该路径尚不存在,创建成功。Broker 1 将自己的ID(brokerid=1)等信息写入节点数据。此时,Broker 1 当选为 Controller。

  • 同时,Broker 1 会去递增 /controller_epoch 节点,将其值设为1,标志着这是第一代控制器。

后续 Kafka 节点启动(稍晚或同时):

  • 同样连接 Zookeeper 后,尝试创建 /controller 临时节点。
  • Zookeeper 返回 "节点已存在" 的错误。
  • Broker 知道自己竞选失败。于是,它在已存在的 /controller 节点上注册一个Watch(监听器),以便在该节点消失时能第一时间得到通知。

此时状态:Controller = Broker 1。其他 Broker 为 Follower,并监听 Controller 的存亡。

7.1.2 稳定运行期

Controller(Broker 1)的工作:

  • 管理所有分区和副本的 Leader 选举。
  • 监听 Zookeeper 中 /brokers/ids 的变化,感知 Broker 的上下线。
  • 将任何元数据变更(如Topic创建)同步给其他 Broker。

其他 Broker 的工作:

  • 处理客户端对其上分区副本的读写请求。
  • 从 Controller(Broker 1)接收并更新元数据。
  • 维持与Zookeeper的会话,并保持对 /controller 节点的Watch。

7.1.3 故障与重新选举

假设 Broker 1(Controller)发生故障(崩溃或网络彻底隔离):

  1. **会话过期:**Zookeeper在预设的会话超时时间(如 session.timeout.ms)内收不到Broker 1的心跳,判定其会话失效。
  2. **节点自动删除:**根据Zookeeper临时节点的特性,与该会话关联的所有临时节点将被自动删除。这意味着 /controller 节点消失。
  3. Watch触发:/controller 节点的删除事件,立刻触发其他 Broker 之前注册的Watch。其他 Broker 收到通知:" Controller 节点已删除"。
  4. 新一轮选举:
    • 存活 Broker 并发尝试创建 /controller。
    • 假设 Broker 2 成功,写入自身 ID。
    • Broker 2 将 /controller_epoch 从 1 递增至 2。
  5. 新 Controller 履职:
    • 读取 /brokers/ids 获取当前存活 Broker 列表。
    • 读取 /brokers/topics 重建分区状态。
    • 开始处理积压事件(如下线 Broker 的分区迁移)。

集群恢复,Controller 切换为 Broker 2,controller_epoch = 2 确保旧指令失效。

7.2 Kafka 脑裂问题及解决方案

在 ZooKeeper 模式下,虽然 ZooKeeper 本身通过 ZAB 协议保证强一致性,但 Kafka Controller 与 Broker 之间的通信是异步的,仍存在短暂窗口可能导致"旧 Controller 未感知自己已失效,仍在发指令"。

7.2.1 场景示例

Controller 所在 Broker 网络卡顿(非完全宕机):

  • Broker 1 是 Controller,但因 GC 停顿或网络延迟,ZooKeeper 会话超时,/controller 被删除。
  • Broker 2 被选为新 Controller(epoch=2),开始下发新指令(如将分区 Leader 切给 Broker 3)。
  • 但 Broker 1 尚未意识到自己已失去 Controller 身份(因为还没收到本地会话超时通知),仍在向 Broker 3 发送旧指令(如"你仍是 Follower")。

如果 Broker 3 同时接受来自 epoch=1 和 epoch=2 的指令,就可能执行冲突操作,比如:

  • 一边被要求成为 Leader(来自新 Controller)
  • 一边被要求保持 Follower(来自旧 Controller)

这就构成了 逻辑上的脑裂:两个 Controller 同时"指挥"集群。

7.2.2 /controller_epoch 解决脑裂

Kafka 引入 controller_epoch(控制器纪元) 作为 全局单调递增的版本号 ,所有控制类请求(如 LeaderAndIsrRequest)都必须携带当前 epoch。接收方(其他 Broker)会校验该 epoch:只接受 epoch >= 当前已知最大 epoch 的 Controller 指令;否则直接拒绝。

具体机制:

1. 每次 Controller 切换,/controller_epoch 原子递增

  • 初始:epoch = 0
  • Broker 1 成为 Controller → epoch = 1
  • Broker 1 宕机,Broker 2 接管 → epoch = 2

2. 所有控制消息携带 epoch

3. Broker 本地维护 lastSeenControllerEpoch

  • 每次收到 Controller 消息,更新该值。
  • 如果收到 epoch = 1 的消息,但本地已知 epoch = 2 → 直接丢弃,并记录警告日志。

7.3 创建 Topic 流程

7.3.1 命令行提交创建指令

  1. 通过命令行提交指令,指令中会包含操作类型(--create)、topic的名称(--topic)、主题分区数量(--partitions)、主题分区副本数量(--replication-facotr)、副本分配策略(--replica-assignment)等参数。示例:
bash 复制代码
bin/kafka-topics.sh --create \
  --topic my-topic \
  --bootstrap-server localhost:9092 \
  --partitions 3 \
  --replication-factor 3
  1. 客户端参数校验:
  • 操作类型必须为 create;
  • 分区数量必须大于0;
  • 副本数量必须大于1且小于 Broker 数量;
  • 主题名称必须符合命名规则;
  • 检查主题是否已存在。
  1. 封装主题对象:
  • 将参数封装为 NewTopic 对象;
  • 使用默认配置(如果未指定):
    • num.partitions(默认1);
    • default.replication.factor(默认1)。
  • 向Controller发起请求:
    • 客户端通过 KafkaAdminClient 向 Controller 发起请求;
    • 请求标记为 CREATE_TOPICS;
    • 如果请求发送到非 Controller Broker,会返回 NOT_CONTROLLER 错误。

7.3.2 Controller接收创建主题请求

  1. **请求处理:**Controller接收到请求(通过Acceptor)将请求放入 requestQueue,由KafkaRequestHandler 处理请求。
  2. **请求转发:**请求被转发到 KafkaApis,调用 handleCreateTopicsRequest 方法。

7.3.3 Controller处理创建请求

  1. 参数验证:
  • 验证分区数和副本因子;
  • 检查副本因子是否超过 Broker 数量;
  • 检查主题是否已存在。
  1. 副本分配策略:
  • **如果指定了--replica-assignment:**使用指定的分配方案;
  • 如果未指定: 使用Kafka内部分配策略(根据机架信息分为两种策略):
    • 未指定机架信息的分配策略: 获取Broker列表(brokerIds),对Broker列表进行随机化(Collections.shuffle(brokerIds))
      • 算法原理:
        • **随机起始位置:**startIndex = random.nextInt(brokerList.size()),确保 Leader 副本不会集中在特定 Broker 上;
        • **分区分配:**第 i 个分区的第一个副本分配到 (startIndex + i) % brokerList.size();
        • **副本分配:**第 i 个分区的第 j 个副本分配到 (startIndex + i + j) % brokerList.size()。
    • 指定机架信息的分配策略: 获取机架交错的Broker列表(例如:rack1:0,5; rack2:3,4; rack3:1,2 → [0,3,1,5,4,2]),以轮询方式分配副本,确保每个分区的副本分布在不同机架。
      • 算法原理:
        • 按机架分组Broker:
          • rack1: [0,5]
          • rack2: [3,4]
          • rack3: [1,2]
        • 创建机架交错列表:
          • 顺序:rack1 的 Broker1, rack2 的 Broker1, rack3 的 Broker1, rack1 的 Broker2, rack2 的 Broker2, rack3 的 Broker2;
          • 结果:[0,3,1,5,4,2]。
  1. 更新ZooKeeper元数据:
  • 在 /config/topics 节点下创建主题节点;
  • 在 /brokers/topics 节点下创建主题相关节点;
  • 为每个分区创建 /brokers/topics/my-topic/partitions/0 节点;
  • 在分区节点中写入副本分配信息。
  1. 启动状态机:
  • Controller 初始化状态机:
    • 分区状态机(PartitionSM):NonExistentPartition → NewPartition → OnlinePartition
    • 副本状态机(ReplicaSM):OfflineReplica → OnlineReplica
  • 更新 ControllerContext 中的元数据
  1. 更新Broker元数据:
  • Controller 向所有 Broker 发送 UPDATE_METADATA 请求;
  • Broker 更新本地元数据缓存;
  • Broker 根据新元数据创建分区目录。

7.3.4 Broker处理元数据更新

  1. 创建分区目录:
  • 创建目录:/kafka/data/my-topic-0
  • 创建数据文件:
    • 00000000000000000000.log(数据文件)
    • 00000000000000000000.index(索引文件)
    • 00000000000000000000.timeindex(时间索引文件)
  1. 设置副本角色:
  • 从副本分配列表中选择第一个副本作为 Leader;
  • 其他副本作为 Follower;
  • 初始化 ISR(In-Sync Replicas)列表。

7.3.5 验证主题创建

Controller确认创建成功,向客户端返回 CREATE_TOPIC_SUCCESS 响应,响应包含主题名称、分区数、副本因子等信息。客户端验证命令:

bash 复制代码
bin/kafka-topics.sh --describe --topic my-topic --bootstrap-server localhost:9092

7.4 数据存储

Kafka的存储设计摒弃了传统消息队列在内存中维护复杂数据结构(如链表、树)的方式,而是直接将消息持久化到磁盘文件。它利用了磁盘顺序读写速度远超随机读写的特性,并将所有写操作转化为追加写,从而实现了极高的吞吐量。

Kafka 对应配置项:https://kafka.apache.org/35/configuration/broker-configs/

7.4.1 存储架构核心概念

  1. 主题(Topic)与分区(Partition):
  • 一个 Topic 是消息的逻辑分类。

  • 每个 Topic 可以分成多个分区(Partition),分区是 Kafka 并行处理和水平扩展的基本单位。

  • 消息存储的物理实体是分区。每个分区在物理上对应一个文件夹。

  1. 日志(Log):
  • 每个分区在存储层面就是一个仅追加(Append-Only)的日志文件。消息被顺序地追加到日志末尾。
  • 日志被分割成多个日志段(Log Segment),以方便管理和清理过期数据。
  1. 日志段(Log Segment):
  • 一个分区日志实际上由一系列顺序生成的、大小相等的日志段文件组成(第一个段的大小可能不同)。
  • 活跃的、正在写入的段称为活跃段(Active Segment)。只有活跃段可以被写入,之前的段都是只读的。
  • 每个日志段由两个核心文件组成(存储在对应的分区目录下,例如 topic1-0/):
    • .log 文件:存储实际的消息数据和批记录。
    • .index 文件:稀疏索引文件,用于将消息的偏移量(Offset) 映射到其在.log文件中的物理位置。
    • .timeindex 文件(可选):基于时间戳的索引,用于按时间查找消息。

日志段命名:

  • 日志段文件以该段第一条消息的偏移量命名(如 00000000000000000000)。这个偏移量是64位长整型,固定用20位数字表示,不足补零。
  • 这种命名使得根据偏移量查找对应的日志段文件非常高效(二分查找)。
    说明:

查看 .log、.index、.timeindex 日志文件的命令通过 4.8.1 章节查看。

7.4.2 索引(.index文件)的工作原理

  • **稀疏索引:**不会为每条消息建立索引,而是每隔一定数量的字节(如4KB, 由 index.interval.bytes 配置)在.log文件中建立一条索引。

  • **索引条目:**每个条目包含两个字段:(Offset, Position)。

    • Offset: 消息的偏移量。

    • Position: 该消息在对应的.log文件中的物理字节位置。

  • 查找过程:假设要查找偏移量为 100 的消息。

    • 在.index 文件中二分查找,找到小于等于 100 的最大索引条目,例如 (90, 1024)。

    • 从 .log 文件的 1024 字节位置开始顺序扫描,直到找到偏移量为 100 的消息。

    • 由于是顺序扫描,并且消息是紧凑存储的,这个查找过程仍然非常快。

7.4.3 消息保留与清理

Kafka不会永久保存数据,提供了两种清理策略(通过 log.cleanup.policy 配置):

  • **基于时间:**默认策略。删除超过指定保留时间(retention.ms, 默认7天)的日志段。
  • **基于大小:**删除超过分区总大小限制(retention.bytes)的旧日志段。
  • **压缩:**对于 Keyed 消息,只为每个 Key 保留最新的值。常用于变更日志(如 __consumer_offsets)。

日志压缩说明:

因为数据会丢失,所以这种策略只适用保存数据最新状态的特殊场景。

7.4.4 为什么这种设计如此高效

  • **顺序磁盘I/O:**写是纯追加,读是顺序扫描。磁盘顺序读写速度堪比内存随机访问。(每个分区日志文件内部的顺序写。)
  • **零拷贝优化:**使用 sendfile 系统调用,数据直接从页面缓存(Page Cache)发送到网络,绕过用户缓冲区,减少CPU拷贝和上下文切换。
  • **页面缓存:**依赖操作系统页面缓存管理内存,而不是在 JVM 堆中维护缓存,避免了 GC 开销,并且利用了操作系统的优秀缓存算法。
  • **批处理与压缩:**大幅减少了网络和磁盘的I/O压力。
  • **稀疏索引:**以极小的索引文件代价,实现了接近 O(1) 时间复杂度的消息查找。

7.4.5 副本同步机制

7.4.5.1 Leader 和 Follower
  • Leader副本: 每个分区在某一时刻只有一个 Leader 副本。所有生产者的写入请求和消费者的读取请求都必须发往该分区的 Leader。它负责处理所有读写 I/O,是"活跃"副本。
  • Follower副本: 每个分区可以有多个Follower副本。它们唯一的任务就是从 Leader 副本异步地拉取(Fetch)数据,保持与Leader数据的同步。Follower不处理客户端请求。

当Leader所在的Broker宕机时,Kafka会从保持同步的Follower中选举出一个新的Leader,继续提供服务,从而实现故障自动转移。

7.4.5.2 ISR(In-Sync Replicas, 同步副本集)

ISR 是一个与 Leader 副本保持基本同步的副本集合(包含 Leader 自己)。"基本同步"并不意味着完全实时,而是指 Follower 在一定时间和延迟范围内追上了 Leader。

判断一个 Follower 是否同步核心指标:

  • 时间: Follower 在过去 replica.lag.time.max.ms (默认10秒)内向Leader发送过拉取请求。
  • 进度: Follower的最后一条消息的偏移量与 Leader 的最后一条消息偏移量之间的差距,不能超过 replica.lag.max.messages (这个参数在高版本中已被废弃,主要使用时间判断)。

如果一个 Follower 在超过 replica.lag.time.max.ms 时间内没有向 Leader 发起拉取请求,或者拉取的进度差距过大,Leader 就会将其从 ISR 中移除。当这个 Follower 重新"赶上"后,又会被重新加入 ISR。

7.4.5.3 同步过程详解(Follower视角)
  • 拉取请求: Follower 会定期(或连续)向 Leader 发送拉取请求(Fetch Request),请求中携带了自己当前已复制的最大偏移量。

  • **Leader响应:**Leader 收到请求后,会从自己的日志文件中读取该偏移量之后的数据,返回给Follower。

  • Follower写入: Follower 收到数据后,将其顺序追加到自己的本地日志文件(同样是 .log 文件),并更新自己的高位水位线。

  • **更新拉取偏移量:**下一次拉取请求时,Follower 会使用新的偏移量,如此循环。

7.4.5.4 水位线(High Watermark, HW)与 LEO
  • LEO(Log End Offset): 日志末端位移。表示副本下一条待写入消息的偏移量。对于Leader,就是生产者下一条消息的写入位置;对于Follower,就是从Leader拉取到的下一条数据的写入位置。
  • HW(High Watermark): 高水位线。这是一个分区级别的、非常重要的概念。
    • 它代表已成功被所有 ISR 副本复制的最大消息偏移量。
    • 消费者最多只能消费到 HW 之前的消息。HW 之后的消息对消费者不可见,因为这部分消息可能尚未在所有 ISR 中完成复制,在 Leader 故障时可能丢失。
    • Leader负责计算和传播 HW。Follower 在拉取请求的响应中会从 Leader 获取当前的 HW,并更新自己的 HW。

示例:

  • Leader LEO = 10, 有2个Follower, F1 LEO=10, F2 LEO=8。那么ISR集合为 [Leader, F1], 此时 HW = 8 (因为所有 ISR 都复制了 0-8 的消息)。
  • 消费者只能消费到偏移量 8 的消息。偏移量 9 和 10 的消息虽然已写入 Leader,但因未完全同步,对消费者不可见。
  • 当 F2 也追赶到 LEO=10 时,ISR 恢复为 [Leader, F1, F2], HW 被推进到 10, 所有消息对消费者可见。

8. 消费者

8.1 属性配置

见官网:https://kafka.apache.org/35/configuration/consumer-configs/

8.1.1 必须配置的属性

属性名 说明
bootstrap.servers 用于建立到Kafka集群初始连接的Broker地址列表。消费者会通过这些地址发现集群的所有成员。
key.deserializer 用于反序列化消息键的类,必须实现 Deserializer 接口。
value.deserializer 用于反序列化消息值的类。
group.id 消费者所属的消费者组ID。这是启用消费者组协作和分区分配机制的关键。

8.1.2 常用可选配置

  1. 核心消费逻辑与偏移量管理
属性名 说明 默认值 影响与建议
auto.offset.reset 当消费者在分区中没有初始偏移量或偏移量无效时(如被删除)的行为。 latest earliest:从分区起始处开始消费。 latest:从最新产生的消息开始消费。 none:如果未找到偏移量则抛出异常。
enable.auto.commit 是否自动提交消费位移。 true true:由消费者后台线程定期提交,可能导致重复消费或丢失。 false:需要手动调用 consumer.commitSync()commitAsync()。生产环境推荐 false 以实现精确控制。
auto.commit.interval.ms 自动提交位移的时间间隔。 5000 (5秒) 仅在 enable.auto.commit=true 时生效。间隔越短,重复消费风险越低,但提交更频繁。
isolation.level 控制消费者可以读取到什么级别已提交的消息。 read_uncommitted read_uncommitted:默认。可以读取所有消息,包括未提交(属于HW以下但未完成ISR同步)的消息。 read_committed:只能读取已提交的事务性消息。用于配合生产者事务实现精确的消费语义。
max.poll.records 单次 poll() 调用返回的最大消息数量。 500 控制每次处理的数据量,影响处理吞吐量和延迟。如果消息处理很重,应调小此值以避免 poll() 超时。
max.poll.interval.ms 两次 poll() 调用之间的最大时间间隔。 300000 (5分钟) 如果消费者在此时间内未再次调用 poll(), 将被认为故障,触发再均衡。必须根据消息处理的最长时间来设置,并留有余量。
  1. 性能与网络调优
属性名 说明 默认值 影响与建议
fetch.min.bytes 消费者从Broker拉取数据时,Broker返回的最小数据量。 1 (字节) 如果设为 1024, Broker会等待有至少1KB的数据时才返回给消费者,减少网络往返,提升吞吐量,但会增加一些延迟。
fetch.max.wait.ms Broker等待 fetch.min.bytes 要求的满足时间的最大时间。 500 (毫秒) fetch.min.bytes 配合使用。达到两个条件之一即返回。
fetch.max.bytes 单次拉取请求返回的最大数据量。 52428800 (50MB) 限制消费者单次拉取的大小,防止内存溢出。
max.partition.fetch.bytes 为每个分区返回的最大数据量。 1048576 (1MB) 如果消费的主题分区数很多,单次 poll() 的总数据量可能 = max.partition.fetch.bytes * 分区数。需注意总内存。
connections.max.idle.ms 关闭空闲连接的时间。 540000 (9分钟) 对于云环境或经常需要重置连接的场景,可适当调低。
  1. 容错与再均衡相关
属性名 说明 默认值 影响与建议
session.timeout.ms 消费者与Broker会话的超时时间。 45000 (45秒) 消费者必须在此时间内向Broker发送心跳(poll() 调用或后台心跳线程)。超时则被视为离线,触发再均衡。在网络不稳定时可能需要调大。
heartbeat.interval.ms 消费者发送心跳给Broker的时间间隔。 3000 (3秒) 必须小于 session.timeout.ms, 通常为其1/3。保持心跳是消费者"存活"的标志。
partition.assignment.strategy 分区分配给消费者的策略类。 RangeAssignor RangeAssignor:按主题范围分配,可能导致不均衡。 RoundRobinAssignor:轮询分配,更均衡。 StickyAssignor:粘性分配器, 兼顾均衡性和在再均衡时最小化分区移动(避免"抖动")。生产环境推荐使用 StickyAssignor
metadata.max.age.ms 强制刷新元数据(如Topic、分区信息)的时间间隔。 300000 (5分钟) 如果主题分区数发生变化,消费者最长需要此时间才能感知。
  1. 安全配置(若集群启用安全协议)
属性名 说明 示例
security.protocol 使用的安全协议。 SASL_PLAINTEXT, SASL_SSL, SSL
sasl.mechanism SASL认证机制。 PLAIN, SCRAM-SHA-256, SCRAM-SHA-512
ssl.truststore.location SSL信任库文件路径。 /path/to/truststore.jks
sasl.jaas.config JAAS配置,包含用户名密码。 org.apache.kafka.common.security.plain.PlainLoginModule required username="admin" password="admin-secret";

8.2 代码实现

8.2.1 添加依赖

版本为 Kafka 部署的版本。

XML 复制代码
<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>3.5.1</version>
</dependency>

8.2.2 消费消息

java 复制代码
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;

import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

public static void main(String[] args) {
    // 配置参数
    Map<String, Object> configMap = new HashMap<>();
    configMap.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
    configMap.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
    configMap.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
    configMap.put(ConsumerConfig.GROUP_ID_CONFIG, "group1");

    // 创建消费者
    KafkaConsumer<String, String> consumer = new KafkaConsumer<>(configMap);
    // 订阅主题
    consumer.subscribe(Arrays.asList("test"));
    // 拉取数据
    ConsumerRecords<String, String> poll = consumer.poll(Duration.ofMillis(1000));
    for (ConsumerRecord<String, String> record : poll) {
        System.out.println(record.value());
    }

    // 关闭消费者
    consumer.close();
}

8.3 消费消息的基本原理

8.3.1 消费者组

消费者组是由一个或多个消费者实例(可以运行在不同机器、进程或线程上)组成的逻辑集合,这些实例共享一个唯一的 group.id。其中,一个分区在同一时间只能被同一个消费者组内的一个消费者实例消费。一个消费者实例可以同时消费多个分区。

说明:

Kafka 的 Consumer 采用拉取数据的方式。

8.3.2 分区分配

当消费者组启动时,组内的消费者会通过组协调器(Group Coordinator) 进行协调,决定每个分区由哪个消费者来消费。分配策略由 partition.assignment.strategy 配置决定。

8.3.2.1 RoundRobinAssignor(轮询分配策略)
  • 每个消费者组中的消费者都会含有一个自动生产的 UUID 作为 memberid。
  • 轮询策略中会将每个消费者按照 memberid 进行排序,所有 member 消费的主题分区根据主题名称进行排序。
  • 将主题分区轮询分配给对应的订阅用户,注意未订阅当前轮询主题的消费者会跳过。
8.3.2.2 RangeAssignor(范围分配策略)

对每个主题单独进行分配。对于每个主题,它按照以下步骤分配:

  1. 将消费者按名称排序
  2. 将分区按分区号排序
  3. 使用范围划分的方法将分区分配给消费者

对于每个主题分配算法:

  • 计算 n = 分区数 / 消费者数
  • 计算 m = 分区数 % 消费者数
  • 前 m 个消费者分配到 n+1 个分区,其余消费者分配到 n 个分区

缺点:单个Topic的情况下显得比较均衡,但是假如Topic多的话, member排序靠前的可能会比member排序靠后的负载多很多。

8.3.2.3 StickyAssignor(粘性分区)

StickyAssignor(粘性分配器)的设计目标是在保证分区分配均衡的同时,尽可能减少再均衡(Rebalance)引起的分区移动。这意味着在发生再均衡时,它会尽量保持之前分配的分区不动,只进行必要的调整以维持均衡。

  • **初次分配:**StickyAssignor 在第一次分配(没有之前的分配状态)时,会尽量生成一个均衡的分配方案。其算法类似于 RoundRobinAssignor,但做了一些优化,以保证后续再均衡时的粘性。
  • 再分配: 当发生再均衡且之前已经有分配状态时,StickyAssignor会尝试在保持最大粘性的基础上进行重新分配。它通过以下步骤实现:
    • 保留之前分配中仍然有效的部分(例如,消费者和分区都还在,且订阅关系没变)。
    • 将剩余的分区(如新增分区、因消费者离开而释放的分区)重新分配,尽量达到均衡。
8.3.2.4 CooperativeStickyAssignor(增量式再均衡)

在Kafka的早期版本中,再均衡采用的是Eager(急切)策略,即一旦消费者组中有成员发生变化(加入或离开),就会触发一次完整的再均衡,所有消费者都会重新分配分区。这个过程分为两个阶段:

  1. 所有消费者都停止消费,并释放已持有的分区。

  2. 重新分配分区,然后每个消费者重新获取分配到的分区。

这种方式会带来两个问题:

  • 再均衡期间,整个消费者组不可用,造成消费暂停。

  • 频繁的再均衡(比如在弹性伸缩环境中)会导致频繁的消费暂停,影响实时性。

因此,Kafka 2.4引入了增量式再均衡(Cooperative Rebalance),也称为"合作式再均衡"。

增量式再均衡的核心思想是:在再均衡时,消费者组中的成员不会立即释放所有分区,而是只释放需要重新分配的分区,同时保留其他分区继续消费。 这样,再均衡的影响范围被缩小,只有部分分区需要重新分配,从而减少了消费暂停的时间。

CooperativeStickyAssignor 的分配算法与 StickyAssignor 相同,都追求在再均衡时尽量减少分区的移动(即保持粘性),同时保证分配的均衡性。

不同之处在于再均衡的过程:

  • StickyAssignor 使用 Eager 再均衡,一次完成所有分区的重新分配。

  • CooperativeStickyAssignor 使用增量式再均衡,可能通过多轮完成。

8.3.3 偏移量 offset

Kafka中的偏移量(Offset)是消息在分区中的唯一标识,用于表示消息在分区中的位置。每个分区中的消息都会被分配一个连续的偏移量,从0开始递增。消费者通过管理偏移量来追踪自己已经消费到了哪个位置,以便在故障恢复或重新平衡后能够从正确的位置继续消费。

8.3.3.1 偏移量的作用
  • **唯一标识:**在同一个分区内,每个消息的偏移量是唯一的,且按顺序递增。

  • **消费进度跟踪:**消费者通过提交偏移量来记录消费进度。

  • **消费者位移:**消费者组为每个分区保存的已消费消息的偏移量,表示该组在该分区上已经消费到了哪个位置。

8.3.3.2 偏移量的存储
  • **消费者偏移量:**存储在Kafka的内部主题 __consumer_offsets 中,键为消费者组 ID、主题和分区,值为偏移量和其他元数据。
  • **分区日志中的偏移量:**每个消息在分区日志文件中都有其偏移量,但消息在日志文件中的存储位置(物理偏移量)与消息偏移量(逻辑偏移量)可能不同,因为Kafka会进行日志压缩和清理。
8.3.3.3 偏移量的管理
8.3.3.3.1 自动提交偏移量

默认情况下,消费者会自动提交偏移量,但可能会导致重复消费或丢失消息。

java 复制代码
configMap.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
configMap.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 5000);
8.3.3.3.2 手动提交偏移量

方式一:同步提交

方式二:异步提交

8.3.3.4 偏移量的重置
8.3.3.4.1 自动重置策略

当消费者组第一次启动或者偏移量无效(如被删除)时,可以通过auto.offset.reset配置重置策略。

  • earliest:从分区起始位置开始消费。

  • latest:从最新消息开始消费。

  • none:如果没有偏移量,则抛出异常。

8.3.3.4.2 手动重置偏移量
8.3.3.5 偏移量的存储与内部主题

偏移量提交的流程:

  1. 消费者向协调器发送提交偏移量的请求。

  2. 协调器将偏移量写入 __consumer_offsets 主题。

  3. 消费者也可以从该主题读取偏移量,以恢复消费进度。

__consumer_offsets 主题:

  • 作用:存储消费者组的偏移量。
  • 分区数:由 Broker 配置 offsets.topic.num.partitions 决定,默认50。
  • 副本数:由 Broker 配置 offsets.topic.replication.factor 决定,默认3。
8.3.3.6 偏移量与消费者组
  • 每个消费者组独立维护自己的偏移量,互不影响。

  • 同一个消费者组内的消费者共享偏移量,即组内一个消费者提交的偏移量会被组内其他消费者使用。

  • 再均衡期间,消费者可能会失去对某些分区的所有权,并在再均衡完成后获得新的分区。在失去分区时,消费者应该提交已经处理的消息的偏移量。在获得新分区时,消费者将从该分区上一次提交的偏移量处开始消费。

8.3.4 消费者事务

消费者处理流程:拉取消息 → 处理消息 → 提交偏移量 → 继续消费,如果在此期间:

  • 如果"提交偏移量"在"处理消息"之前:可能数据丢失;
  • 如果"处理消息"失败但"提交偏移量"成功:可能数据丢失;
  • 如果"处理消息"成功但"提交偏移量"失败:可能重复消费。

所以一般情况下,想要完成 Kafka 消费者端的事务处理,需要将数据消费过程和偏移量提交过程进行原子性绑定,也就是说数据处理完了,必须要保证偏移量正确提交,才可以做下一步的操作,如果偏移量提交失败,那么数据就恢复成处理之前的效果。

除此之外,还需要根据实际情况配置合适的隔离级别。Kafka 隔离级别通过 isolation.level 配置:

  • read_uncommitted(读未提交):
    • 消费者可以读取到所有消息,包括生产者尚未提交的事务消息。
    • 性能较高,因为不需要等待事务完成。
    • 但可能读取到未提交的消息,如果事务后续回滚,这些消息会被丢弃,导致消费者读取到脏数据。
  • read_committed(读已提交):
    • 消费者只能读取到已经提交的事务消息。
    • 对于未提交的事务消息,消费者会等待直到事务提交或中止。
    • 可以避免读取到脏数据,但会有一定的延迟,因为需要等待事务完成。
相关推荐
张彦峰ZYF1 小时前
Java+Python双语言开发AI工具全景分析与选型指南
java·人工智能·python
小北方城市网2 小时前
SpringBoot 集成 Redis 实战(缓存优化与分布式锁):打造高可用缓存体系与并发控制
java·spring boot·redis·python·缓存·rabbitmq·java-rabbitmq
步步为营DotNet2 小时前
深度解析.NET 中Nullable<T>:灵活处理可能为空值的类型
java·前端·.net
努力d小白2 小时前
leetcode49.字母异位词分组
java·开发语言
爱笑的rabbit2 小时前
Linux和Windows的word模板导出转PDF下载保姆级教程,含PDF图片处理
java·spring
weixin_462446232 小时前
【实战】Java使用 Jsoup 将浏览器书签 HTML 转换为 JSON(支持多级目录)
java·html·json·书签
Y_cheng_2 小时前
php环境配置与伪协议
开发语言·php
小北方城市网2 小时前
SpringBoot 集成 Elasticsearch 实战(全文检索与聚合分析):打造高效海量数据检索系统
java·redis·分布式·python·缓存
IMPYLH2 小时前
Lua 的 Table 模块
开发语言·笔记·后端·junit·游戏引擎·lua