跳转到内容

第 28 章 · Kafka

一句话:分布式的消息日志,生产者把消息追加到主题的分区里,消息按保留时间留在磁盘上,各消费者组自己记读到哪儿,读过的消息不删,可以重读。 用它还是用别的:订单、点击这类事件要给好几个服务各读一份,新服务上线要从头重读,或者量大到每秒上万条,用 Kafka;处理完就删的任务队列、按类型路由,用 RabbitMQ(第 27 章);已经有 Redis 且量不大,用 Redis Streams(第 26 章)。 核对版本:Kafka 4.0(随书 compose 镜像 apache/kafka:4.0.0),2026-10

本书用 Docker 跑 Kafka,单节点、KRaft 模式。在随书的 compose/ 目录下启动,Compose 的用法详见第 13 章:

终端窗口
docker compose --profile mq up -d

这一条同时起 RabbitMQ 和 Kafka。Kafka 有两个地址:宿主机上的程序连 localhost:9092,同一个 compose 里的其他容器连 kafka:19092。数据在命名卷 kafkadata 里。

命令行工具都在容器的 /opt/kafka/bin/ 下,前面加 docker compose exec kafka。在容器里用 localhost:9092 连的是同一个 broker 的宿主机监听器,所以本章命令一律写 --bootstrap-server localhost:9092。查版本,返回 4.0.0:

终端窗口
docker compose exec kafka /opt/kafka/bin/kafka-topics.sh --version

不想用 Docker、要在本机解压运行的,见 28.7 的“快速开始”。

  • 主题与分区(topic、partition):主题是一类消息的名字,如 orders。一个主题分成几个分区,每个分区是一份只追加的日志,分区之间并行读写,只有同一分区内保证顺序。生产者带键(key)发送时,同键总进同一个分区;不带键时,生产者把一批消息写进同一个分区,攒满一批再换下一个分区,长期看分散到各分区,短时间内发的几条常落在同一个分区。
  • 偏移量(offset):消息在分区里的序号,从 0 开始递增。消费者组提交偏移量,记下每个分区读到哪儿;重启后从提交处接着读,把偏移量改小就能重读。
  • 消费者组(consumer group):一个分区同一时刻只分给组内一个成员,所以组内干活的成员最多和分区数一样多。不同的组互不影响,各读一份完整的消息。积压、提交偏移量的通用说法见第 26 章 26.2。
  • 副本(replica):每个分区可以在几个 broker 上各存一份,一台挂了由别的副本接替;随书单节点只有一份,副本数是 1。
  • 保留策略(retention):消息到期就删,默认留 7 天,与有没有被消费无关;也可以按大小删。
  • KRaft:Kafka 用自己的 controller 管理集群元数据,不再需要 ZooKeeper,4.0 起已不支持 ZooKeeper 模式。随书 compose 里同一个进程既是 broker 又是 controller。

下图是一个三分区的主题:同键的消息总在同一个分区;组 g1 有两个成员,分区分给它们;组 g2 只有一个成员,三个分区都归它。

生产者按键把消息写进主题 orders 的三个分区,分区内偏移量递增;消费者组 g1 的 c1 读分区 0 和 1、c2 读分区 2;消费者组 g2 的唯一成员读全部三个分区

脚本在容器里,表中省略前缀 docker compose exec kafka /opt/kafka/bin/。

任务 命令 说明
主题
建主题 kafka-topics.sh --bootstrap-server localhost:9092 --create --topic orders --partitions 3 不写 --partitions 用 num.partitions,默认 1
列出主题 kafka-topics.sh --bootstrap-server localhost:9092 --list __ 开头的是内部主题
看分区和副本 kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic orders 每个分区一行,含 Leader、Replicas、Isr
加分区 kafka-topics.sh --bootstrap-server localhost:9092 --alter --topic orders --partitions 6 只能加不能减;加了之后同一个键可能换到别的分区
收发
发消息 kafka-console-producer.sh --bootstrap-server localhost:9092 --topic orders 每输入一行发一条,Ctrl+C 退出
带键发 kafka-console-producer.sh --bootstrap-server localhost:9092 --topic orders --property parse.key=true --property key.separator=: 输入 u1:paid,冒号前是键
从头收 kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic orders --from-beginning 不加 --from-beginning 只收启动之后的新消息
以组的身份收 kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic orders --group g1 自动提交偏移量,下次接着读
消费者组
列出组 kafka-consumer-groups.sh --bootstrap-server localhost:9092 --list 已经没有成员在线的组也列出
看积压 kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group g1 LAG 是 LOG-END-OFFSET 减 CURRENT-OFFSET
重置偏移量 kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group g1 --topic orders --reset-offsets --to-earliest --execute 组内消费者要先全部停掉;换成 --dry-run 只预览
保留时间
改一个主题的保留时间 kafka-configs.sh --bootstrap-server localhost:9092 --alter --entity-type topics --entity-name orders --add-config retention.ms=86400000 留 1 天,覆盖 broker 的 log.retention.hours
看主题改过的配置 kafka-configs.sh --bootstrap-server localhost:9092 --describe --entity-type topics --entity-name orders 只列出单独设过的项

以下命令都省略前缀 docker compose exec kafka /opt/kafka/bin/,收发要两个终端。

  1. 建一个三分区的主题:kafka-topics.sh --bootstrap-server localhost:9092 --create --topic orders --partitions 3。
  2. 看详情,PartitionCount: 3、ReplicationFactor: 1,三个分区的 Leader 都是 1:kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic orders。
  3. 在终端一打开生产者,在 > 提示符后依次输入 1001、1002、1003,每行一条:kafka-console-producer.sh --bootstrap-server localhost:9092 --topic orders。
  4. 在终端二从头收,三条都出现,不带键时几条可能落在不同分区,顺序不一定是输入的顺序:kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic orders --from-beginning。
  5. 在终端一再输入 1004,终端二马上收到;退出消费者,提示 Processed a total of 4 messages:Ctrl+C。

接着用上面的 orders:

  1. 以组 g1 读一遍,新组第一次读也要加 --from-beginning,收完按 Ctrl+C:kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic orders --group g1 --from-beginning。
  2. 在生产者里再发 1005、1006、1007 三条:kafka-console-producer.sh --bootstrap-server localhost:9092 --topic orders。
  3. 看积压,各分区 LAG 加起来是 3,第一行提示 Consumer group 'g1' has no active members.:kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group g1。
  4. 预览把偏移量拨回最早,NEW-OFFSET 列全是 0:kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group g1 --topic orders --reset-offsets --to-earliest --dry-run。
  5. 执行重置,组内有消费者在线会报错,要先停掉:kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group g1 --topic orders --reset-offsets --to-earliest --execute。
  6. 再看,CURRENT-OFFSET 都是 0,LAG 等于全部 7 条:kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group g1。
  7. 以 g1 再读,不加 --from-beginning 也从第一条开始,7 条全部重来:kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic orders --group g1。

只重放最近一段,把 --to-earliest 换成 --shift-by -2(每个分区退 2 条)或 --to-datetime 2026-10-08T00:00:00.000。

按键分区,让同一用户的消息有序

Section titled “按键分区,让同一用户的消息有序”
  1. 建主题:kafka-topics.sh --bootstrap-server localhost:9092 --create --topic user-events --partitions 3。
  2. 打开带键的生产者,冒号前是键:kafka-console-producer.sh --bootstrap-server localhost:9092 --topic user-events --property parse.key=true --property key.separator=:。
  3. 依次输入 u1:login、u2:login、u1:pay、u3:login、u1:logout,输完退出;某一行漏了冒号,生产者报 No key separator found 并退出:Ctrl+C。
  4. 收的时候显示分区和键,u1 的三条都在同一个 Partition: 后面,且按 login、pay、logout 的顺序:kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic user-events --from-beginning --property print.partition=true --property print.key=true。
  5. 只读 u1 所在的那个分区(把 1 换成上一步看到的号):kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic user-events --partition 1 --from-beginning。

程序里也一样:发送时把用户 ID 或订单号作为键,不要自己指定分区号。

broker 的配置文件是 server.properties,一行一个 键=值。位置与附录 C 一致:

配置项 路径 说明
配置文件(解压安装) config/server.properties 在 Kafka 解压目录下,改完重启 broker
Docker 镜像内 /etc/kafka/docker/ 镜像自带的单节点默认配置;传了 KAFKA_ 环境变量就不再用它,所以随书 compose 把必需的项都写全了

随书 compose 不挂配置文件,用 kafka 服务 environment: 里的环境变量设。配置项名换成环境变量的规则:. 换成 _,原有的 _ 换成 __,- 换成 ___,全部大写,前面加 KAFKA_。对照如下:

配置项 随书 compose 的环境变量 当前值与说明
log.retention.hours 未设,要改就加 KAFKA_LOG_RETENTION_HOURS 默认 168(7 天)
num.partitions 未设,要改就加 KAFKA_NUM_PARTITIONS 默认 1;建主题不写 --partitions 和自动建主题时用
listeners KAFKA_LISTENERS PLAINTEXT://:19092,HOST://:9092,CONTROLLER://:9093,在容器里监听的端口
advertised.listeners KAFKA_ADVERTISED_LISTENERS PLAINTEXT://kafka:19092,HOST://localhost:9092,告诉客户端以后连哪个地址
log.dirs KAFKA_LOG_DIRS /var/lib/kafka/data,挂在卷 kafkadata 上;不设时镜像用 /tmp/kraft-combined-logs

客户端第一次连上 broker 后,broker 回给它 advertised.listeners 里对应监听器的地址,之后的收发都连这个地址。所以监听器要分两个:宿主机和容器看到的 broker 地址不一样。

宿主机程序经端口映射连 HOST 监听器 9092,broker 回给它 localhost;同一 compose 里的容器连 PLAINTEXT 监听器 kafka,broker 回给它 kafka

改了环境变量,再执行一次 docker compose --profile mq up -d 会重建容器,数据在卷里不丢。对应的 server.properties 写法如下,前三项与随书 compose 相同,后三项是示例值,随书 compose 未设,# 开头的行是注释:

# 两个客户端监听器加一个 controller 监听器,在容器里监听的端口
listeners=PLAINTEXT://:19092,HOST://:9092,CONTROLLER://:9093
# 公布给客户端的地址:容器之间用 kafka:19092,宿主机用 localhost:9092
advertised.listeners=PLAINTEXT://kafka:19092,HOST://localhost:9092
# 数据目录,随书 compose 挂在卷 kafkadata 上
log.dirs=/var/lib/kafka/data
# 不写分区数时建 3 个分区
num.partitions=3
# 消息留 3 天,与有没有被消费无关
log.retention.hours=72
# 单个分区最多留 1GB,与时间限制谁先到按谁删
log.retention.bytes=1073741824
  • 日志反复报 Connection to node 1 (localhost/127.0.0.1:9092) could not be established。 原因:容器里的程序连了 kafka:9092,broker 回的地址是 localhost:9092,指向它自己。处理:容器连 kafka:19092,宿主机连 localhost:9092。
  • 消费者启动后什么也收不到,之前发的消息看不见。 原因:新消费者组默认 auto.offset.reset=latest,只从最新处开始读。处理:命令行加 --from-beginning,程序设 auto.offset.reset=earliest;已提交过偏移量的组用 --reset-offsets 拨回。
  • 同一组起了 4 个消费者,只有 3 个在处理消息。 原因:主题只有 3 个分区,一个分区只分给组内一个成员,多出的闲着。处理:--describe --members 查各成员分到几个分区;按 28.3 的“加分区”加到 6 个。
  • 同一用户的“已退出”先于“已登录”被处理。 原因:没带键或键不一致,几条消息落到了不同分区,各分区并行读。处理:用用户 ID 作键;加分区会改变键到分区的映射,加完之后的一段时间顺序可能乱。
  • 磁盘被占满,broker 报 No space left on device 后退出。 原因:默认保留 7 天,与是否消费无关,写入量大时卷被撑满。处理:用 kafka-configs.sh 给大主题设 retention.ms,或在 compose 里加 KAFKA_LOG_RETENTION_HOURS、KAFKA_LOG_RETENTION_BYTES。
  • Kafka 4.0 文档:官方文档首页,设计、配置、运维、安全、KRaft 都从这里进。
  • 快速开始:下载解压或用 Docker 起一个单节点,建主题、发、收消息的最短路径;本机不用 Docker 时照这里装。
  • apache/kafka Docker 镜像:官方镜像的标签、单节点启动示例,以及用 KAFKA_ 环境变量覆盖配置的规则。
  • broker 配置参考:server.properties 全部配置项的含义、类型和默认值,log.retention.hours、num.partitions、advertised.listeners、log.dirs 都在这里。