Apache Kafka:构建高吞吐分布式消息系统的终极指南

0 阅读12分钟

在当今的微服务架构与大数据时代,系统之间的解耦、异步通信以及流量削峰成为了核心需求。Apache Kafka 作为一款开源的分布式事件流平台,凭借其高吞吐、低延迟、可扩展和持久化的特性,已然成为实时数据处理管道的首选方案。

本文将带你从零开始,深入理解 Kafka 的核心架构、部署运维、底层原理以及 Java 开发实战。

1. Kafka 概述与核心架构

1.1. 什么是消息队列

在介绍 Kafka 之前,我们先理解一下“消息队列”。简单来说,它就像是快递柜。生产者(Producer)把包裹(消息)存进去,消费者(Consumer)在合适的时候取出来。这种机制实现了“发布/订阅”模式,解决了以下痛点:

解耦:生产者和消费者不需要直接交互,互不影响。

异步:生产者发送消息后无需等待消费者处理,直接返回,提升响应速度。

削峰填谷:在流量洪峰时,消息先积压在队列中,消费者按照自己的处理能力慢慢消费,防止系统崩溃。

1.2. 消息队列两种模式

1.2.1. 点对点模式

一对一,消费者主动拉取数据,消息收到后消息清除

消息生产者生产消息发送到 Queue 中,然后消息消费者从 Queue 中取出并且消费消息。

消息被消费以后,Queue 中不再有存储,所以消息消费者不可能消费到已经被消费的消息。

Queue 支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费。

1.2.2. 发布/订阅模式

一对多,消费者消费数据之后不会清除消息

消息生产者(发布)将消息发布到 Topic 中,同时有多个消息消费者(订阅)消费该消息。

和点对点方式不同,发布到 Topic 的消息会被所有订阅者消费。

1.3. Kafka 的组成

Kafka 的架构设计非常经典,主要包含以下几个核心组件:

组件名称说明
Producer (生产者)消息的发送方,负责将数据推送到 Kafka 集群。
Consumer (消费者)消息的接收方,从 Kafka 集群拉取数据进行处理。
Broker (服务代理)Kafka 集群中的单个服务器节点,负责消息的存储和转发。
Topic (主题)消息的逻辑分类,类似于数据库中的表。
Partition (分区)Topic 的物理分片,一个 Topic 可以分为多个 Partition,分布在不同 Broker 上,是实现高吞吐的关键。
Replica (副本)分区的备份,分为 Leader 和 Follower,用于保证数据的高可用性。
Zookeeper协调服务(注:Kafka 3.x 后逐渐引入 KRaft 模式以去 Zookeeper 化),负责管理集群元数据和控制器选举。

1.4. Kafka 的基础架构

image.png

说明:

  • 为了方便扩展,提高吞吐量,一个 Topic 分为多个 Partition 分区,每个分区存储不同的数据;
  • 为了配合分区设计,提出消费者组的概念,每个消费者组有多个消费者,并行消费;
  • 一个消费者消费一个 Topic 的一个 Partition;
  • 提高可用性,为每个 Partition 增加若干个副本,副本不会提供消费者消费;
  • 每个 Topic 中的消息,消费者只能消费一次;
  • Topic 的主分区和从分区,主、从分区一定不会在同一个节点上;

2. Kafka 入门:部署与快速上手

2.1. 官网

Kafka 官网地址:kafka.apache.org/

Kafka 下载地址:kafka.apache.org/downloads

2.2. 环境准备

• Java 环境:JDK 1.8 或更高版本。

Kafka 下载:前往 Apache Kafka 官网下载二进制包(如 )。

这里我们下载的是kafka_2.12-3.0.0版本,接下来我们部署一个三节点Kafka集群。

2.3. 安装部署

  1. 解压tar包,并修改Kafka目录名。
# 解压
tar -zxvf kafka_2.12-3.0.0.tgz -C /opt/module/

2. 编辑并修改config/server.properties配置文件。

# broker的全局唯一编号,不能重复
broker.id=0
# 删除topic功能使能
delete.topic.enable=true
# 处理网络请求的线程数量
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运行日志存放的路径  
log.dirs=/opt/module/kafka_2.12-3.0.0/logs
# topic在当前broker上的分区个数
num.partitions=1
# 用来恢复和清理data下数据的线程数量
num.recovery.threads.per.data.dir=1
# segment文件保留的最长时间,超时将被删除
log.retention.hours=168
# 配置连接Zookeeper集群地址
zookeeper.connect=hadoop102:2181,hadoop103:2181,hadoop104:2181

3. 配置环境变量(这里我们将环境变量统一配置到/etc/profile.d/myEnv.sh文件中)。

#KAFKA_HOME
export KAFKA_HOME=/opt/module/kafka_2.12-3.0.0
export PATH=$PATH:$KAFKA_HOME/bin

4. 将Kafka分发到其他机器中。 5. 修改其他机器上的config/server.properties配置文件。

# 将broker.id配置,分别修改为 1 和 2,三台机器的borker.id不重复即可。

6. 集群启动与关闭

# 启动Kafka集群
./bin/kafka-server-start.sh --daemon config/server.properties

# 停止Kafka集群
./bin/kafka-server-stop.sh

2.4. Kafka的命令行操作

2.4.1. 查看topic

./bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --list

2.4.2. 创建topic

./bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create --replication-factor 2 --partitions 2 --topic first
Created topic first.

参数说明:

  • --topec:定义topic名;
  • --replication-factor:定义副本数;
  • --partitions:定义分区数;

2.4.3. 删除topic

./bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --topic first --delete

注意:

删除topic,需要配置文件server.properties中设置delete.topc.enable=true,否则,删除只是标记删除。

2.4.4. 查看topic详情

./bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --topic first --describe

# 返回如下内容:
Topic: first    TopicId: HCmd897LR-i6wHuTEJ8oIQ PartitionCount: 2   ReplicationFactor: 2    Configs: segment.bytes=1073741824
   Topic: first    Partition: 0    Leader: 1   Replicas: 1,2   Isr: 1,2
   Topic: first    Partition: 1    Leader: 0   Replicas: 0,1   Isr: 0,1

2.4.5. 修改分区数

./bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --alter --topic first --partitions 3

重新查看topic详情:

./bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --topic first --describe
Topic: first    TopicId: HCmd897LR-i6wHuTEJ8oIQ PartitionCount: 3   ReplicationFactor: 2    Configs: segment.bytes=1073741824
   Topic: first    Partition: 0    Leader: 1   Replicas: 1,2   Isr: 1,2
   Topic: first    Partition: 1    Leader: 0   Replicas: 0,1   Isr: 0,1
   Topic: first    Partition: 2    Leader: 0   Replicas: 0,2   Isr: 0,2

2.4.6. 发送消息

./bin/kafka-console-producer.sh --bootstrap-server hadoop102:9092 --topic first
> hello world
> aaa 121

2.4.7. 消费消息

./bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first --from-beginning

hello world
aaa 121

2.5. 快速实战:发送与接收消息

我们先不编写代码,直接使用Kafka自带的命令行工具,实现一个“Hello World”示例,来了解Kafka的基本使用。

  1. 创建用于发送与接收消息的Topic。
./bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create --replication-factor 2 --partitions 2 --topic hello
Created topic hello.

2. 查看创建的Topic。

./bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --list
hello

3. 发送消息。

./bin/kafka-console-producer.sh --bootstrap-server hadoop102:9092 --topic hello
> hello world
> hello kafka
> ni hao!!!

4. 消费消息。

./bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first --from-beginning

hello world
hello kafka
ni hao!!!

注意:也可以先打开消费端,开启消费后,再发送数据,这样就可以看到实时发送与消费。

3. Kafka 架构深入:高性能的秘密

3.1. 为什么 Kafka 这么快

Kafka 的吞吐量可以达到百万级,主要得益于以下设计:

  • 顺序写磁盘:Kafka 将消息追加写入到 Partition 的日志文件末尾。磁盘的顺序写性能远高于随机写,甚至可以媲美内存写入。
  • 零拷贝:在数据发送过程中,利用 Linux 的 系统调用,数据直接从页缓存传输到网卡,避免了在内核态和用户态之间多次拷贝,极大降低了 CPU 开销。
  • 批量发送与压缩:生产者可以将多条消息打包成一个批次发送,并支持 GZIP、Snappy 等压缩算法,减少网络 IO。

3.2. Kafka存储机制

Kafka 的消息是以日志的形式存储在磁盘上的。

image.png

  • Topic — Partition — Segment:一个 Topic 包含多个 Partition,每个 Partition 物理上对应一个目录,目录下又分为多个 Segment(日志段)。
  • Segment 文件:每个 Segment 包含实际消息数据、稀疏索引和时间戳索引。这种结构使得 Kafka 在查找特定消息时非常高效。

4. Kafka 分区策略:数据流转的交通规则

分区是 Kafka 实现高吞吐和并行处理的基石。生产者发送消息时,消息究竟会落入哪个分区?这由分区策略决定。合理的策略能避免数据倾斜,甚至保证消息的局部有序性。

4.1. 分区规则

Kafka 生产者的分区逻辑主要遵循以下流程:

  1. 指明 partition 的情况下,直接将指明的值直接作为 partiton 值;
  2. 没有指明 partition 值,但有 key 的情况下,将 key 的 hash 值与 topic 的 partition 数进行取余得到 partition 值;
  3. 既没有 partition 值又没有 key 值的情况下,Kafka 会采用轮询策略(旧版本)或粘性分区策略(新版本),将消息均匀分发到各个分区,以实现负载均衡;

4.2. 分区好处

  1. 便于合理使用存储资源;
  2. 提高并行度;

4.3. 分区策略配置

  1. Kafka 默认使用的是 Range 策略;
  2. 配置 RoundRobin 策略,可以修改配置文件:
partition.assignment.strategy=org.apache.kafka.clients.consumer.RoundRobinAssignor

或者代码中直接设置,具体如下:

properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, "org.apache.kafka.clients.consumer.RoundRobinAssignor");

4.4. 自定义分区

当默认策略无法满足业务需求时(例如需要根据用户 ID 的奇偶性分发,或者根据地理位置路由),我们可以实现自定义分区器。

只需实现 org.apache.kafka.clients.producer.Partitioner 接口:

public class CustomPartitioner implements Partitioner {
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        int numPartitions = partitions.size();
        
        // 自定义逻辑:例如根据 value 的长度取模
        if (valueBytes == null) {
            return 0;
        }
        return Math.abs(valueBytes.length) % numPartitions;
    }

    @Override
    public void close() {}

    @Override
    public void configure(Map<String, ?> configs) {}
}

配置使用自定义分区器,只要在 Producer 配置中设置:properties.put("partitioner.class", "com.example.CustomPartitioner");

5. 数据可靠性保证:消息去哪了

在生产环境中,我们最怕消息丢失。Kafka 通过多副本机制和确认机制来确保数据不丢。

5.1. 副本机制

每个分区都可以配置多个副本,分布在不同的 Broker 上。

  • Leader:负责处理所有的读写请求。
  • Follower:被动从 Leader 同步数据。
  • ISR:与 Leader 保持同步的副本集合。只有 ISR 中的副本才有资格被选举为新的 Leader。

5.2. 确认机制

生产者发送消息后,需要等待 Broker 的确认。这个确认级别由 acks 参数控制;

Ack配置含义可靠性性能场景
0不等待确认极高日志采集,允许少量丢失
1只要 Leader 写入成功即返回一般业务,折中方案
all等待 ISR 中所有副本写入成功金融、订单等核心业务数据

最佳实践:为了达到消息写入所有同步副本,通常配合以下配置:

  • ack = all:确保消息写入所有同步副本;
  • min.insync.replicas = 2:设置最小同步副本数。如果 ISR 副本数少于2个,写入会失败,防止数据在少数派节点少“裸奔”;
  • replication.factor = 3:设置副本数为3,容忍最多1个节点故障;

6. 幂等性与事务:拒绝重复消息

即使有了可靠性保证,网络重试仍可能导致消息重复发送。为了解决这个问题,Kafka 引入了幂等性。

6.1. 什么是幂等性

幂等性是指无论生产者发送多少次相同的数据,Broker 最终只持久化一条。这对于防止因网络抖动导致的重复扣款、重复下单至关重要。

6.2. 实现原理

Kafka 的幂等性依赖于生产者 ID 和序列号。

  • PID:每个生产者启动时会获得一个唯一的 ID。
  • Sequence Number:生产者给每条消息分配一个单调递增的序列号。
  • Broker 端去重:Broker 会检查 <PID, Partition, SequenceNumber>,如果发现序列号不连续或重复,就会拒绝或丢弃该消息。

6.3. 如何开启

只需要在 Producer 配置中添加发下:

enable.idempotence=true

开启后,Kafka 会自动将acks设置为all,并将重试次数设为无限次,确保消息既可靠又不重复。

6.4. 事务支持

如果需要跨分区、跨 Topic 的原子性操作(即“精确一次”语义),则需要使用 Kafka 事务。事务基于两阶段提交协议,保证一系列操作要么全部成功,要么全部失败。

// Java 事务示例
producer.initTransactions();
try {
    producer.beginTransaction();
    producer.send(record1);
    producer.send(record2);
    producer.commitTransaction();
} catch (Exception e) {
    producer.abortTransaction();
}

7. Kafka API实战:Java与SpringBoot集成

7.1. Java 原生客户端集成

  1. 首先,在pom.xml中引入依赖;
<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>3.6.0</version>
</dependency>

2. 生产者代码示例;

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

KafkaProducer<String, String> producer = new KafkaProducer<>(props);

// 发送消息
ProducerRecord<String, String> record = new ProducerRecord<>("test-topic", "key1", "Hello Java Kafka");
producer.send(record, (metadata, exception) -> {
    if (exception == null) {
        System.out.println("发送成功: " + metadata.offset());
    }
});

producer.close();

3. 消费者代码示例;

7.2. SpringBoot 集成

  1. 引入依赖;
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

2. 修改application.yml配置文件;

spring:
  kafka:
    bootstrap-servers: localhost:9092
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
    consumer:
      group-id: my-group
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      auto-offset-reset: earliest
      # 建议关闭自动提交,使用手动提交保证数据一致性
      enable-auto-commit: false

3. 生产者发送消息;

@Autowired
private KafkaTemplate<String, String> kafkaTemplate;

public void send(String msg) {
    kafkaTemplate.send("test-topic", msg);
}

4. 消费者消费消息,或者说监听消息;

@KafkaListener(topics = "test-topic", groupId = "my-group")
public void listen(String message) {
    System.out.println("收到消息: " + message);
    // 业务逻辑处理...
}

8. Kafka 监控与运维

在生产环境中,监控是保障系统稳定运行的关键。我们需要关注 Broker 的健康状态、Topic 的积压情况(Lag)、网络流量等指标。

主流监控工具有:

  1. Kafka Eagle

    • 开源 Kafka 集群管理工具,功能强大;
    • 支持多集群管理、Topic 管理、消费者组监控、消息积压告警等等;
    • 提供可视化仪表盘,展示消息趋势和系统健康度;
  2. Prometheus + Grafana

    • Prometheus,负责采集 Kafka 的 JMX 指标;
    • Grafana,负责数据可视化,可以导入现成的 Kafka 监控模板,展示极其精美的图表,如每秒消息注入量、分区分布图等;
  3. Kafka Manager

    • Yahoo 开源工具,主要用于集群管理和分区重新分配,界面相对简洁;