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 之间的关系,我画了一张流程图:
这张图展示了完整的消息流转路径: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 里写数据、读数据,为后面的实时特征计算做准备。