3. Kafka 核心概念:Topic、Partition、Consumer Group,本地单机部署

好,咱们进入实时特征平台的第三站——Kafka。说实话,Kafka 是整个实时数据流的主动脉。你想想看,所有业务日志、埋点数据、数据库变更,最终都要汇到这里,再由下游的 Flink 消费处理。如果 Kafka 没搞明白,后面搭建特征平台就是空中楼阁。

这一章,我会带你吃透 Kafka 的三个核心概念:Topic、Partition、Consumer Group。然后咱们亲手在本地跑一个单机实例,验证这些概念到底是怎么工作的。

3.1 为什么是 Kafka?

先聊个背景。我早年做实时数仓时,团队用的是 RabbitMQ 做消息中转。业务量一上来,吞吐瓶颈、消息堆积、回溯困难,各种问题接踵而至。后来换成 Kafka,这些问题基本迎刃而解。

Kafka 的设计哲学很纯粹:用顺序读写 + 磁盘存储 + 分布式分区,实现高吞吐、可持久化、可回溯的消息系统。它不像传统消息队列那样消费完就删除消息,而是保留一段时间,允许消费者从任意位置重新读取。

核心优势一句话总结:Kafka 是一个分布式的、基于发布/订阅模式的、可持久化的、高吞吐的消息系统。

3.2 三大核心概念

3.2.1 Topic:逻辑上的消息分类

Topic 是 Kafka 中最基础的概念。你可以把它理解成一个消息的类别或者数据流的名字。比如,用户点击日志发到 user_click Topic,订单数据发到 order Topic。

生产者往 Topic 里写消息,消费者从 Topic 里读消息。Topic 本身不存储数据,数据是存在 Partition 里的。

我的习惯:Topic 命名尽量带上业务域和数据类型,比如 ods_user_click、dwd_order_detail。这样后期维护时,看一眼名字就知道数据来源和用途。

3.2.2 Partition:真正的存储单元

一个 Topic 可以分成多个 Partition(分区)。每个 Partition 是一个有序的、不可变的、追加写入的日志文件。消息在 Partition 内部有一个唯一的偏移量(Offset),用来标识位置。

为什么要分区?说白了就是为了水平扩展。单台机器的磁盘和带宽有限,分区后可以把不同 Partition 分布到不同机器上,实现并行读写。

概念 说明 我的理解
Topic 逻辑分类,类似数据库的表 消息的「文件夹」
Partition 物理存储单元,有序日志 文件夹里的「文件」
Offset 消息在 Partition 内的位置编号 文件里的「行号」
Replica Partition 的副本,保证高可用 文件的「备份」

这里有个关键点:同一个 Partition 内的消息是有序的,但不同 Partition 之间不保证全局有序。如果你的业务要求严格的消息顺序,比如金融交易流水,那就得把相关消息路由到同一个 Partition。

我曾经踩过的坑:有个项目需要按用户 ID 排序处理事件,我一开始没指定分区策略,结果同一个用户的消息散落在不同 Partition,下游消费时顺序全乱了。后来用 userId.hashCode() % partitionNum 做分区键,才解决问题。

3.2.3 Consumer Group:消费的负载均衡

Consumer Group 是 Kafka 实现点对点和发布/订阅两种模式的关键。一组消费者属于同一个 Group ID,它们共同消费一个 Topic 的消息。

规则很简单:

  • 一个 Partition 只能被同一个 Group 内的一个消费者消费。
  • 一个消费者可以消费多个 Partition。
  • 如果消费者数量大于 Partition 数量,多余的消费者会闲置。

举个例子:Topic 有 4 个 Partition,Group A 有 3 个消费者。那么分配可能是:消费者1 消费 Partition 0、1,消费者2 消费 Partition 2,消费者3 消费 Partition 3。如果 Group A 有 5 个消费者,那就有 1 个消费者永远拿不到消息。

核心结论:Consumer Group 是实现「水平扩展消费能力」和「故障自动转移」的基础。一个 Group 内的消费者数量最好等于 Partition 数量,这样负载最均衡。

3.3 本地单机部署

理论说完了,咱们动手。本地部署 Kafka 其实很简单,我习惯用 Docker 一键启动,省去环境配置的麻烦。

3.3.1 使用 Docker Compose 启动

新建一个 docker-compose.yml 文件:

version: '3.8'
services:
  zookeeper:
    image: confluentinc/cp-zookeeper:7.4.0
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000
    ports:
      - "2181:2181"

  kafka:
    image: confluentinc/cp-kafka:7.4.0
    depends_on:
      - zookeeper
    ports:
      - "9092:9092"
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1

然后在终端执行:

docker-compose up -d

等几秒钟,Kafka 就启动好了。你可以用 docker ps 确认两个容器都在运行。

注意:单机部署时,KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR 必须设为 1,否则 Kafka 会因为找不到足够的副本而报错。

3.3.2 验证部署

进入 Kafka 容器,用内置命令行工具验证:

docker exec -it <kafka-container-id> /bin/bash
cd /bin

创建一个 Topic,指定 3 个 Partition:

kafka-topics --create \
  --topic test-topic \
  --bootstrap-server localhost:9092 \
  --partitions 3 \
  --replication-factor 1

查看 Topic 详情:

kafka-topics --describe \
  --topic test-topic \
  --bootstrap-server localhost:9092

输出类似这样:

Topic: test-topic  PartitionCount: 3  ReplicationFactor: 1
  Topic: test-topic  Partition: 0  Leader: 1  Replicas: 1  Isr: 1
  Topic: test-topic  Partition: 1  Leader: 1  Replicas: 1  Isr: 1
  Topic: test-topic  Partition: 2  Leader: 1  Replicas: 1  Isr: 1

看到 3 个 Partition 都正常,说明部署成功。

3.4 核心流程可视化

为了让你更直观地理解 Producer、Topic、Partition、Consumer Group 之间的关系,我画了一张流程图:

Kafka 消息流转核心流程 Producer Topic user_click Partition 0 Partition 1 Partition 2 Consumer Group Consumer 1 Consumer 2 Offset: 0,1,2,... Offset: 0,1,2,... Offset: 0,1,2,... 图例说明 • 生产者写入 Topic • Topic 分发到 Partition • Consumer 从 Partition 拉取

这张图展示了完整的消息流转路径:Producer 将消息发送到 Topic,Topic 根据分区策略将消息分配到不同的 Partition,Consumer Group 内的消费者各自负责消费一个或多个 Partition 的消息。

3.5 实战:生产与消费消息

咱们用命令行模拟一下生产消费,加深理解。

启动生产者:

kafka-console-producer --topic test-topic --bootstrap-server localhost:9092

输入几条消息:

hello kafka
this is partition test
实时特征平台

按 Ctrl+C 退出生产者。

启动消费者:

kafka-console-consumer --topic test-topic --bootstrap-server localhost:9092 --from-beginning

你会看到刚才输入的三条消息依次打印出来。加上 --from-beginning 参数,消费者会从 Partition 的起始位置开始读取所有消息。

验证分区效果:启动两个消费者,属于同一个 Group:

# 终端1
kafka-console-consumer --topic test-topic --bootstrap-server localhost:9092 --group my-group

# 终端2
kafka-console-consumer --topic test-topic --bootstrap-server localhost:9092 --group my-group

然后往 Topic 里发 6 条消息。你会发现每条消息只会被其中一个消费者消费,不会重复。这就是 Consumer Group 的负载均衡效果。

3.6 避坑指南

最后分享几个我实际项目中遇到的坑:

  • Partition 数量不能随意减少:Kafka 只支持增加 Partition,不支持减少。一旦创建,Partition 数量就固定了。所以初期规划时,我建议根据未来半年的数据量预估,留出余量。
  • Consumer Group 的 Rebalance 会短暂中断消费:当消费者加入或退出 Group 时,Kafka 会触发 Rebalance,重新分配 Partition。这个过程可能持续几秒到几十秒。对于延迟敏感的场景,要控制好 Rebalance 频率。
  • 消息大小有限制:默认单条消息最大 1MB。如果你要传输大图片或长文本,记得调整 max.message.bytes 参数。

我曾经踩过的坑:有一次线上 Kafka 集群突然消费变慢,排查了半天,发现是一个消费者进程挂了,但 Group 没感知到,导致 Partition 长时间未被消费。后来我养成了习惯:给每个 Consumer Group 配置监控告警,一旦消费者数量变化就立刻通知。

好了,这一章的内容就到这里。Kafka 的核心概念和本地部署你已经掌握了。接下来,你可以试着用代码往 Kafka 里写数据、读数据,为后面的实时特征计算做准备。