ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Kafka入门实战:从核心概念到生产部署与问题排查

Kafka入门实战:从核心概念到生产部署与问题排查

Kafka 这个词,在 Java 后端圈子里几乎无人不知。很多人第一次看到它,是在招聘 JD 里那句“熟悉 Kafka、RocketMQ 等消息中间件优先”,于是开始搜教程,结果被一堆分布式术语砸得头晕:Leader、Follower、ISR、HW、Offset、Consumer Group……今天这篇不是说教式的概念罗列,我尽量用一个经历过各种生产问题的一线开发者的视角,把 Kafka 从“听名字”到“能上手”这个阶段真正需要的知识,一次性讲清楚。

这篇内容不要求你有任何消息队列基础,只要能写 Java 代码、会用命令行,就能跟着跑通一个完整的 Kafka 环境。除了安装部署和基础命令,我还会重点解释几个入门阶段最容易被绕晕的设计点:分区和 Offset 是怎么回事、消费者组如何工作、消息为什么会丢或重复。顺手把我踩过的坑和排查思路也整理进去,希望能帮你少走几周弯路。

1. 先搞清楚 Kafka 到底解决了什么问题

1.1 消息队列是个什么玩意

如果你没接触过消息队列,可以把 Kafka 想象成一个“邮局 + 大型储物柜”的组合体。上游系统把消息当成信件投递进来,下游系统按自己的节奏去取件,两边不需要同时在线,也不需要知道对方的存在。这就是“解耦”的核心含义。

举个例子,你做一个电商系统,用户下单后要发短信、发邮件、加积分、更新推荐结果。如果在下单接口里挨个调用这些服务,接口响应会非常慢,而且任何一个下游服务出问题,用户下单都会失败。引入 Kafka 后,下单接口只负责把“订单创建成功”这条消息发到 Kafka,其余服务各自订阅这个主题,互不干扰。订单服务不再关心下游死活,下游服务也可以根据自己的处理能力消费消息,这就是 Kafka 最常见的价值——异步解耦。

Kafka 还有一个关键词叫“高吞吐”。它经过专门设计,单机就能支撑每秒几十万条消息的写入。这个性能在入门阶段你可能感受不明显,但当你需要处理每天几个亿的日志或埋点数据时,就明白这有多重要了。

1.2 Kafka 与其他消息中间件的关键差异

很多人学 Kafka 之前会问:和 RabbitMQ、RocketMQ 有什么区别?其实这几个工具在定位上有明显侧重。

RabbitMQ 是典型的消息队列,功能全面、管理界面友好,但吞吐量相对有限,适合中小规模场景。RocketMQ 是阿里开源的消息中间件,在事务消息、延迟消息等业务特性上做得更丰富,国内电商场景用得多。而 Kafka 从出生起就奔着“海量数据、高吞吐、持久化”去的,它的消息可以保存几天甚至更久,消费者可以随时回溯消费,这是很多消息队列做不到的。

这些差异决定了选型:如果你只需要一个简单的任务队列,RabbitMQ 上手更快;如果团队已经在 Java 技术栈且有强一致的事务需求,RocketMQ 也合适;但如果你要处理日志、埋点、行为数据这类海量流式数据,或者要做实时数仓、流式处理,Kafka 基本是首选。

1.3 从整体架构看 Kafka 的核心设计理念

先看一眼全局。Kafka 集群由多个 Broker(服务节点)组成,每个 Broker 就是一个 Kafka 服务进程。生产者把消息发往集群,消费者从集群拉取消息。集群负责存储消息、管理副本、分配分区等脏活累活。

关键点在于,Kafka 没有采用“消息即发即删”的模式,而是把消息落盘保存一段可配置的时间。这个设计在早期看起来有点奢侈,但恰恰是它最大的优势:消息消费完不会立刻消失,新消费者可以从头重新读,旧数据还能用来跑离线分析。理解了这一点,你后面理解 Offset、消费回溯、日志清理策略就会顺畅很多。

2. 核心概念扫盲:写代码前必须搞懂的几个词

2.1 Topic、Partition 与 Offset 到底是啥关系

Topic(主题)是最基本的信息分类单位,比如“订单通知”是一个 Topic,“用户行为日志”是另一个 Topic。但一个 Topic 的数据量太大,单个文件存不下、单节点处理不过来,所以 Kafka 把一个 Topic 分成多个 Partition(分区)。

分区的作用有两个:一是并行,二是顺序。Kafka 只在分区内部保证消息顺序,所以同一个业务实体的消息,必须通过 key 路由到同一个分区,才能保证严格的先后顺序。以订单为例,如果同一个订单的创建、支付、完成消息散落在不同分区,消费者看到的消息顺序就是乱的,这在实际业务中是致命的。

每个分区内的消息都会有一个递增的编号,这个编号就叫 Offset(偏移量)。它不是全局唯一的,只是分区内部唯一。消费者读取消息时,本质上就是在提交“我已经读到哪个偏移量了”。

为了方便理解,我打个比方:Topic 是一本大书,Partition 是书中的独立章节,Offset 是章节里的页码。Kafka 允许不同读者从任意页码开始读,也允许读者记住自己读到哪一页,下次接着读。

2.2 Producer、Consumer 与 Consumer Group 的工作方式

消息生产者(Producer)负责把消息发到指定的 Topic。它并不直接指定发送到哪个分区,而是根据 key 做哈希,或者用轮询策略选择一个分区。写入时,生产者可以设置不同的确认级别(acks)来控制可靠性,这个后面代码部分详细说。

消费者(Consumer)从分区拉取消息。这里的“拉取”是 Kafka 的一个重要设计:不是 Broker 把消息推给消费者,而是消费者主动去拉。好处是消费者可以根据自己的处理能力控制节奏,不会因为推送太快而被打垮。

多个消费者可以组成一个 Consumer Group(消费组)。Kafka 保证同一个分区只会被组内的一个消费者处理。这带来一个问题:如果组内有 5 个消费者,但 Topic 只有 3 个分区,就一定会有 2 个消费者闲着。很多人一开始在这上面栽跟头,以为加消费者就能提速,结果发现消费者数量超过分区数后完全没效果。这是一个需要牢牢记住的规则:分区数是消费者并行度的上限。

2.3 Broker、副本机制与 ZooKeeper 的变迁

Broker 就是 Kafka 集群中的一台服务节点,通常一台机器只跑一个 Broker 实例。生产环境一般用多台机器组成集群,某个节点挂了,其他节点能继续提供服务。

副本机制是 Kafka 高可用的核心。每个分区可以配置多个副本,这些副本分布在不同的 Broker 上。其中一个是 Leader,负责处理读写请求,其余是 Follower,负责同步数据。当 Leader 挂了,Kafka 会从 Follower 里选一个新的 Leader。说到这必须提醒一句:副本多不代表消息一定不丢,还需要生产端和消费者端配合,后面生产环境部分会展开。

老版本 Kafka 依赖 ZooKeeper 来管理集群元数据、选主等,很多入门教程会让你先装 ZooKeeper,这步配置比较折腾。但 Kafka 从 3.0 开始逐渐用 KRaft 模式替代 ZooKeeper,新版本(比如 3.7、3.8)已经可以完全不依赖 ZooKeeper 运行。如果你是刚入门,建议直接用新版,省掉一个中间件,学习和排障都会轻松不少。

3. 快速上手:本地搭建一个可以跑的 Kafka

3.1 环境准备与版本选择

开始动手前,先确认你本地有 Java 8 或更高版本。在终端输入 java -version 就能看到。没有的话先装 JDK,否则 Kafka 脚本会报错。

然后是下载 Kafka。直接去 Apache 官网下载 Binary 版本,贴着最新稳定版走。我建议选 3.5 以上版本,因为 KRaft 模式已经比较成熟,不需要额外装 ZooKeeper,入门最顺手。注意选带 src 的源码包没用,要选 bin 那个二进制包。

下载完之后解压,得到一个目录,后面我把这个目录称为$KAFKA_HOME。如果你用的是 Linux 或 Mac,确保目录下的 bin 脚本有执行权限。Windows 的话尽量用 WSL,虽然 Windows 也可以跑,但脚本体验比较别扭,我在 Windows 上踩过不少 shell 脚本的坑,后来统一在 Linux 环境操作。

3.2 配置与启动:用 KRaft 模式跳过 ZooKeeper

第一步是生成一个集群 ID 并格式化存储目录。这一步骤对应的是 Kafka 内部元数据存储的初始化。在$KAFKA_HOME下执行:

bin/kafka-storage.sh random-uuid

会输出一串 UUID,比如9b7GX3x5T9yLhGhS2t2w0g。接着用这个 UUID 格式化:

bin/kafka-storage.sh format -t 9b7GX3x5T9yLhGhS2t2w0g -c config/server.properties

这里-c指向 Kafka 的配置文件。默认配置里日志目录在/tmp/kafka-logs,如无特殊需求可以先用默认值。格式化这个动作,很多初次上手的人会忘记,结果启动时报错说数据目录没有格式化。如果你看到类似这样的提示,别慌,把这两条命令重新执行一遍就好。

启动服务:

bin/kafka-server-start.sh config/server.properties

看到Kafka Server started字样,说明启动成功。注意,这个启动命令是前台运行的,终端会一直挂着日志。你可以在配置里把log.dirs改成你磁盘上真实存在的路径,避免因为/tmp被清理导致数据丢失。

3.3 创建 Topic 与命令行收发消息

启动完成后,打开一个新终端,创建一个测试 Topic:

bin/kafka-topics.sh --create --topic quickstart-events --partitions 3 --replication-factor 1 --bootstrap-server localhost:9092

这里--partitions 3表示新建的 Topic 有 3 个分区,--replication-factor 1表示副本数为 1(本地单节点只能配 1,配多了会报错)。--bootstrap-server localhost:9092是 Kafka 的接入地址。

查看 Topic 描述信息:

bin/kafka-topics.sh --describe --topic quickstart-events --bootstrap-server localhost:9092

你会看到每个分区的 Leader 是哪一个,ISR 列表是什么。ISR 的意思是“同步中的副本集合”,单节点环境下一般就一个 Broker,ISR 也只有那一个。

然后启动一个生产者,往 Topic 里发消息:

bin/kafka-console-producer.sh --topic quickstart-events --bootstrap-server localhost:9092

输入几行内容,比如:

hello kafka 第一条测试消息

再启动一个消费者,验证能否收到:

bin/kafka-console-consumer.sh --topic quickstart-events --from-beginning --bootstrap-server localhost:9092

用--from-beginning表示从最早的消息开始读。另一个终端里,你刚才输入的内容会立刻出现。如果消费者终端什么也没打印,大概率是没加这个参数,默认只会消费启动之后新产生的消息。这是命令行入门阶段最常遇到的“假故障”。

4. 用代码捅破窗户纸:第一个 Producer 与 Consumer

命令行测试只能验证环境,真正开发不能靠手动输入。这一节我用 Java 写一个最小可用示例,把生产者和消费者串起来。工程推荐用 Maven 或 Gradle,这里以 Maven 为例。

4.1 引入依赖与 Producer 代码

新建一个 Java 工程,在pom.xml里加依赖:

<dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-clients</artifactId> <version>3.7.0</version> </dependency>

这个依赖只包含客户端,不需要在本地起任何 Kafka 服务也能编译,这一点很多人误以为必须连上服务器才能写代码。

编写生产者:

import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.clients.producer.RecordMetadata; import java.util.Properties; import java.util.concurrent.Future; public class ProducerDemo { public static void main(String[] args) throws Exception { Properties props = new Properties(); // Kafka 集群地址,多个用逗号分隔 props.put("bootstrap.servers", "localhost:9092"); // key 和 value 的序列化器,Kafka 只会存储字节数组 props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); // acks 有三个常用值:0、1、all,后面细说 props.put("acks", "all"); KafkaProducer<String, String> producer = new KafkaProducer<>(props); for (int i = 0; i < 10; i++) { ProducerRecord<String, String> record = new ProducerRecord<>( "quickstart-events", "key-" + i, "value-" + i ); // 同步发送,获取元数据信息 Future<RecordMetadata> future = producer.send(record); RecordMetadata meta = future.get(); System.out.printf("消息写入分区 %s,偏移量 %s%n", meta.partition(), meta.offset()); } producer.close(); } }

这里有几个点值得展开。key.serializer和value.serializer是必须配的,因为 Kafka 不关心消息内容是什么,它只存字节。如果你发的是对象,可以自定义序列化器,但推荐用 JSON 字符串。

acks=all是可靠性要求最高的配置,意思是消息要等到所有副本都写入成功才返回成功。如果追求性能且能接受少量丢失,可以设置acks=1(Leader 写入成功即返回)。acks=0是发完即忘,性能最高但丢数据的概率也最大,生产环境除非是日志采集这类场景,不然别这么用。

跑一下代码,控制台会输出每条消息进了哪个分区、得到了什么偏移量。如果你给每条消息配了相同的 key,会发现它们都进了同一个分区——这就是 Kafka 依靠 key 保证同一实体顺序性的机制。

4.2 Consumer 代码与消费组初体验

消费者代码比生产者稍微绕一点,因为要处理“从哪里开始读”和“读完干了什么”两个问题。

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 java.time.Duration; import java.util.List; import java.util.Properties; public class ConsumerDemo { public static void main(String[] args) { Properties props = new Properties(); props.put("bootstrap.servers", "localhost:9092"); props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); props.put("group.id", "demo-group"); // 从最早的消息开始消费,前提是当前组没有已提交的偏移量 props.put("auto.offset.reset", "earliest"); // 开启自动提交偏移量,默认每 5 秒提交一次 props.put("enable.auto.commit", "true"); props.put("auto.commit.interval.ms", "1000"); KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props); // 订阅 Topic,可以订阅多个,用逗号分隔 consumer.subscribe(List.of("quickstart-events")); try { while (true) { // poll 是拉取动作,这里最多阻塞 1 秒 ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1)); for (ConsumerRecord<String, String> record : records) { System.out.printf("分区=%s, 偏移量=%s, key=%s, value=%s%n", record.partition(), record.offset(), record.key(), record.value()); } } } finally { consumer.close(); } } }

group.id这里值得多说一句。同一个消费组内的多个消费者实例,消费同一 Topic 时,各自负责不同的分区,消息不会重复发给组内多个人。但如果你再启动一个消费者,用不同的group.id,那它会把所有消息重新完整消费一遍。这个机制很好用:一个组做实时处理,另一个组做离线分析,互不影响。

auto.offset.reset表示当消费组在服务端没有已提交的偏移量时,该从哪里开始读。earliest是指从最早消息开始读,latest是指只读启动后新消息。这个参数在生产上要特别小心,换了新的 group.id 并配置 latest,可能导致攒了很久的积压消息全部被跳过,这种案例在工作中碰过不止一次。

4.3 代码里的常见疑惑点

小白写消费者代码时,最大的困惑是poll返回的 records 数量到底是多少。Kafka 的poll会在一次调用中尽量拉取可用的消息,但不会无限等待。每处理完一批,再调用下一次poll时,消费者会自动向 Broker 发送拉取请求。

这样设计的好处是消费速率完全由消费端控制,坏处是如果你的处理逻辑很慢,poll的调用频率会降低,可能导致消费者被判定为“假死”,触发 Rebalance(分区重新分配)。所以不要在poll循环里做耗时太长的同步操作。如果需要,可以考虑把消息先丢到内存队列,由另一组线程处理。

还有一个坑是自动提交偏移量。enable.auto.commit=true时,消费者每隔一段时间提交一次当前消费到的偏移量。如果在提交前你的程序崩溃了,重启后会重复消费最近一批消息。排查消息重复的时候,先看是不是这个原因。更稳妥的做法是改成手动提交,等业务处理成功后再提交偏移量,代价是可能重复,好处是不易丢消息,这个取舍下面专门讲。

5. 生产环境必须想清楚的几个设计问题

5.1 分区数到底怎么定

入门阶段创建 Topic 可能随便填个数字就行,但生产上分区数这个参数很难事后修改。分区太少,消费者并行能力受限;分区太多,又会带来文件句柄消耗、分区同步负载和 Rebalance 耗时增加等问题。

我的经验是,分区数不需要一开始定得太大。可以从业务流量和消费者并行度两个角度估算:假设单个消费者的处理能力是每秒 1000 条,业务高峰期每秒需要消费 5000 条,那至少需要 5 个消费者并行,分区数至少要达到 5。再留一点余量,比如 8 到 10 个。特别大的数据量可以后续通过扩容增加分区,但 Kafka 不支持减少分区数,这一步只能往前加、不能往后退,所以拍脑袋定得太高也麻烦。

5.2 副本因子与数据可靠性

生产环境replication-factor至少要设成 2,一般建议 3。意思是每个分区的数据有多个副本存在不同机器上,一台机器宕机了,其他副本还能顶上。

但这里有一个让很多人误解的点:副本数并不等于消息百分百不丢。假设生产端acks=0,消息刚发给 Leader 这条网络就断了,消息没写入任何副本,副本再多也没用。可靠性是一个环环相扣的体系:生产端的 acks、Broker 端的最小同步副本数量(min.insync.replicas)、消费端的偏移量提交策略,每一环都要匹配。

举一个组合:要求高可靠,可以这样配置。生产端acks=all,Topic 的min.insync.replicas=2,副本因子为 3。这样即使某个副本挂了,消息仍然需要写到至少两个副本才返回成功。如果只剩一个副本在线,Kafka 会直接报错给生产者,而不是牺牲一致性。用这个组合,虽然性能和可用性都有所牺牲,但数据安全的底线守住了。

5.3 消费者并发度与分区数的匹配关系

继续深入一点:消费者并发度想要提升,不是代码里开多线程就行,分区数才是真正的瓶颈。前面说过,同一个分区在同一时刻只能被一个消费者实例处理,这是 Kafka 保证分区内有序性的前提。

假设一个 Topic 有 6 个分区,你有 3 个消费者进程在同一个消费组里,那每个消费者处理 2 个分区。如果再启动第 4 个消费者,它不会分到任何分区,处于空闲状态。反过来,如果消费者有 8 个而分区只有 6 个,一定有 2 个消费者空转,浪费资源。

当消费者只是在做 CPU 计算或简单的数据库写入时,一个消费者进程处理多个分区是完全正常的。但当单个分区的消息流里夹杂着耗时操作,比如调用下游 HTTP 接口,你就需要重新考虑分区数或者把处理逻辑改成异步线程池。很多消费堆积问题,根因不是代码写得慢,而是分区数太少导致没法水平扩展。

6. 典型使用场景与选型建议

6.1 日志采集和指标监控

Kafka 最经典的场景是日志采集。App 或业务后端把日志不断写入 Kafka,通过独立的消费者程序实现日志存储和检索。日志数据的特点是大、多、允许偶发丢失,对顺序性要求不高,Kafka 的高吞吐在这里体现得淋漓尽致。

有一个实践建议:日志类 Topic 的acks可以设置成 1 甚至 0,消息保留时间可以缩短到 1~3 天,这样尽可能提升吞吐、控制磁盘占用。我在搭日志管道时,习惯把不同来源的日志拆到不同 Topic,比如 access-log、biz-log、error-log,这样消费侧可以根据主题灵活订阅,互不干扰,排查问题时也清晰很多。

6.2 异步解耦与流量削峰

再回到电商场景。秒杀、大促这类业务,流量短时间猛增,直接打到数据库基本必挂。Kafka 可以做缓冲池:前端请求先把消息发到 Kafka,后端服务按自己的速度消费处理。即使瞬间流量是平时的几十倍,后端也不会被打垮,只是消费堆积量暂时上升,随着流量回落逐渐消化。

在实现异步解耦时,我有一条红线:涉及到资金、订单状态这类核心数据,不能简单发了消息就不管。核心操作需要有自己的兜底机制,比如本地消息表、定时对账、失败重试队列。Kafka 不是事务型数据库,它无法保证消息一定被准确消费且只消费一次,所以在架构设计时别把全部身家押在消息中间件上。

6.3 什么时候不要用 Kafka

不是所有场景都适合 Kafka。如果你只需要给后台任务发一个简单通知,或者只有日均几千条消息,引入 Kafka 纯属过度设计。集群维护、Topic 管理、监控告警都是有成本的,杀鸡不用牛刀。

另外,如果你对消息的“精确一次”有刚需,比如金融交易流水、账务调整这类场景,直接用 Kafka 会非常痛苦。虽然 Kafka 生态里有事务和幂等的支持,但使用门槛和排查成本都不低。这种情况下,用关系型数据库本地事务,或者选择在业务层面做幂等处理的方案,往往更可控。Kafka 擅长的是高吞吐、可回溯、多消费者复用的场景,选型时想清楚这几点,能少走很多弯路。

7. 常见问题和排查经验实录

7.1 消费者一直不消费,命令也没报错

这是入门期出现频率最高的问题。通常有两类原因。

第一,消费组偏移量配置不对。默认latest时,消费者只会消费启动之后新到达的消息。线上排查时可以先创建一个临时消费组,指定--from-beginning去读一遍,确认消息在 Broker 上是否真实存在。如果临时消费组能读到数据,就说明不是集群问题,而是原消费组的 Offset 已经推进到最新位置,旧消息不会再被读到。

第二,订阅的 Topic 生产端没有消息进入。可以看一下生产者的日志,或者用命令行消费者实时观察。我习惯先查 Topic 的describe输出,确认分区数和 Leader 状态正常,再分别验证生产和消费两侧,按链路排查比瞎猜有效得多。如果消费者代码在poll循环里没有打印任何日志,可以先确认下group.id有没有和其他环境共用,避免多个环境互相抢分区。

7.2 消息到底是“丢了”还是“重复了”

从事后看,消息问题往往表现为两类,但根因完全不同。

消息丢失的常见原因:生产者设置了acks=0或acks=1,Leader 写入失败但生产者不知道;或者消费者开启了手动提交 Offset,但逻辑里先提交后处理,处理过程抛异常,消息实际没被消费;还有可能是 Broker 端log.retention.hours设置太短,消息过期被删除了。排查时优先看生产者返回的成功回调、消费者处理日志,以及 Topic 的保留时间配置。

消息重复的常见原因:消费者处理完消息但还没提交 Offset,进程重启后重新消费;或者网络抖动导致消费者与 Broker 的会话超时,触发 Rebalance,分区被分配给其他消费者,原来的消费者正在处理的那批消息可能被重新消费。解决思路是让消费逻辑具备幂等性,例如用消息里的业务 ID 去重,或者在数据库写入前先查重。Kafka 本身提供了幂等生产者,但消费者侧的“精确一次”需要依靠事务等机制,入门阶段只需要明确:重复不可避免,业务代码要做好幂等兜底。

7.3 消费堆积和磁盘占用怎么应对

消费堆积是最常见的生产问题。如果消费组的 Lag(未消费消息积压量)持续增长,说明消费速度跟不上生产速度。处理手段分几步:先看消费者日志和线程状态,排除poll循环里有没有阻塞性的同步调用;然后评估分区数是否足够,如果分区数已经是上限,可以临时扩容分区,再增加消费者实例;最后考虑下游瓶颈,比如数据库连接池不够或者外部接口响应慢,都会拖慢整体消费。

磁盘占用方面,Topic 消息默认保留 7 天,如果数据量巨大,磁盘会很快被打满。我建议提前建立 Topic 级别的容量评估表,按“单条消息大小 × 每秒写入条数 × 保留时长”估算磁盘需求,并配合监控及时预警。对不需要长期保存的数据,把retention.ms调到 1~2 天可以显著节省成本。绝大部分日志类和统计类 Topic 都不需要保留 7 天,这个参数值得反复斟酌。

7.4 几个容易忽略的线上小习惯

最后分享几个我在实践中慢慢养成的习惯。

第一,统一管理 Topic 配置。不要把 Topic 的创建权限散落在开发手里,让每个人随便建。生产环境应该有统一的申请流程,包括分区数、副本数、保留时间的评审。很多线上事故就是某位同事图方便,把一个没设置min.insync.replicas的 Topic 直接发了几亿条数据。

第二,关注消费者 Rebalance 日志。Kafka 的消费者客户端在发生分区重分配时会打印日志,如果你发现 Rebalance 频繁发生,说明消费组的稳定性有问题。频繁 Rebalance 会让消费者反复暂停消费,加剧消费延迟。原因可能是单个消费者处理太慢导致会话超时,也可能是max.poll.interval.ms设置太短,还有可能是心跳线程被poll循环阻塞。我建议先把max.poll.interval.ms调大到 5~10 分钟,给业务处理留出余量,同时优化消费者处理时间。

第三,用好消息 key。不要所有消息都发到同一个分区,也不要完全不指定 key 随机发送。业务上需要顺序保证的,用业务 ID 做 key;不要求顺序的,可以不指定 key 或者设置一个随机 key,让 Kafka 把消息分散到多个分区,提高并行度。这个选择每个 Topic 都需要单独想一想,毕竟分区顺序和负载均衡是有取舍的。

第四,保留监控数据。生产环境的监控指标比你想象中更能说明问题。至少要看生产者的发送成功率、消费者的消费速率、消费组 Lag、Broker 磁盘使用率。没有监控就谈不上排查,等用户反馈出问题再去看日志,往往已经晚了。


我在实际使用 Kafka 的过程中,最深的一点体会是:这个系统不难用,但它的很多概念是相互关联的,只盯着某一个点学很容易迷惑。分区数影响消费者并行度,消费者组影响消息重复和顺序,acks 和副本数又共同决定数据可靠性,这些设计是一套完整的逻辑链条,不能割裂理解。如果你能把文章里这些点串起来,在写代码之前想清楚“我的消息要保序吗”“能接受重复吗”“最多能丢多少数据”,那 Kafka 对你来说就从一个陌生名词变成了一个真正可控的工具。后面如果再往下深入,可以慢慢看 Kafka 的存储机制、事务、流处理这些内容,但核心骨架你已经搭好了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进