4. 消息队列选型:Kafka核心概念与增量链路中的角色
聊到增量计算,消息队列是绕不开的基础设施。而在众多消息队列中,Kafka 几乎成了事实标准。我这些年经手的实时项目,底层清一色都是 Kafka。为什么大家都选它?说白了,就三个字——扛得住。
这一章,我们聚焦 Kafka 的三个核心概念:Topic、Partition、Offset。然后看看它在增量链路中到底扮演什么角色,最后聊聊它的高可用机制是怎么保证不丢数据的。
4.1 三个核心概念:Topic、Partition、Offset
这三个概念,是理解 Kafka 的基石。我见过不少新手,上来就写代码,结果连数据是怎么存的都搞不清楚。嗯,我们先把这个基础打牢。
4.1.1 Topic:逻辑上的消息分类
Topic 是 Kafka 里最上层的逻辑概念。你可以把它理解成一个消息的「类别」或「主题」。比如,主数据变更的消息,我习惯放在一个叫 mdm_data_change 的 Topic 里;业务日志放在 biz_log 里。生产者往 Topic 里写,消费者从 Topic 里读,彼此不干扰。
mdm_customer_update,一看就知道是客户主数据的更新事件。别用 topic1、test 这种名字,后期维护起来很痛苦。
4.1.2 Partition:物理上的分片与并行单元
一个 Topic 可以分成多个 Partition。Partition 是 Kafka 真正存储数据的地方,也是并行处理的基本单位。
为什么要分 Partition?为了水平扩展。你想想看,如果只有一个 Partition,那所有读写都压在一个节点上,性能天花板很低。分成多个 Partition 后,每个 Partition 可以分布在不同的 Broker 上,读写压力就分散了。
我举个例子。假设我们有一个 mdm_customer Topic,设置了 6 个 Partition。那么这 6 个 Partition 可以分布在 3 台 Broker 上,每台负责 2 个。生产者写数据时,可以指定 key,Kafka 根据 key 的哈希值决定写到哪个 Partition。这样,同一个客户 ID 的变更事件,永远会落到同一个 Partition 里,保证了顺序性。
4.1.3 Offset:消息在 Partition 内的唯一编号
每个 Partition 里的消息,都有一个唯一的递增编号,叫 Offset。你可以把它理解成消息的「身份证号」。消费者读取消息时,就是通过 Offset 来定位的。
Offset 由 Kafka 自动管理,消费者可以手动提交 Offset,也可以自动提交。我个人的建议是:在增量链路中,尽量手动提交 Offset。为什么?因为自动提交可能会丢数据。比如,你消费了一条消息,业务逻辑还没处理完,Offset 就自动提交了。这时候程序挂了,重启后 Offset 已经往前跳了,那条没处理完的消息就丢了。
我曾经在一个实时同步项目里,就因为用了自动提交,导致几千条主数据变更没同步过去。排查了半天才发现是 Offset 提交时机的问题。从那以后,所有核心链路我都改成了手动提交。
| 概念 | 层级 | 作用 | 关键特性 |
|---|---|---|---|
| Topic | 逻辑 | 消息分类 | 一个 Topic 可包含多个 Partition |
| Partition | 物理 | 数据分片与并行 | 内部有序,可水平扩展 |
| Offset | 消息级 | 消息唯一编号 | 递增,用于消费定位 |
4.2 Kafka 在增量链路中的角色
在增量计算架构里,Kafka 不是主角,但它是那个「承上启下」的关键角色。我把它总结为三个核心作用:
- 解耦生产者和消费者:上游的 CDC 组件(比如 Canal、Debezium)只管往 Kafka 里写,下游的 Flink 任务只管从 Kafka 里读。两边互不依赖,各自独立部署和扩缩容。
- 削峰填谷:主数据变更的流量往往有波峰波谷。比如白天业务高峰期,变更事件可能每秒几千条;晚上可能只有几十条。Kafka 作为缓冲区,能把波峰的数据暂存下来,让下游按自己的节奏消费。
- 数据重放与回溯:这是 Kafka 最强大的能力之一。因为消息会持久化到磁盘,并且保留一段时间(默认 7 天),所以你可以随时从任意 Offset 开始重新消费。这在数据修复、逻辑变更后重新计算时特别有用。
我举个例子。有一次,我们下游的 Flink 任务因为一个 bug 挂了 3 个小时。修复后,直接从 Kafka 里重置 Offset 到故障前的位置,重新消费了一遍,数据就全补回来了。如果没有 Kafka 的持久化能力,这 3 小时的数据就彻底丢了。
下面这张图,展示了 Kafka 在增量链路中的位置:
4.3 Kafka 高可用机制
在增量链路中,Kafka 一旦挂了,整个数据管道就断了。所以高可用是必须的。Kafka 的高可用,主要靠三个机制:
4.3.1 副本机制(Replication)
每个 Partition 都可以设置副本数。比如,副本数设为 3,那么每个 Partition 的数据会在 3 个 Broker 上各存一份。其中一个副本是 Leader,负责读写;另外两个是 Follower,负责同步。
当 Leader 挂了,Kafka 会从 Follower 中选举一个新的 Leader。这个过程是自动的,对生产者和消费者基本透明。我见过一个场景:某台 Broker 的磁盘坏了,Kafka 集群自动把它的 Leader 角色转移到了其他 Broker 上,整个过程不到 10 秒,业务无感知。
4.3.2 ISR 机制(In-Sync Replicas)
ISR 是 Kafka 保证数据一致性的关键。它指的是「与 Leader 保持同步的副本集合」。只有 ISR 里的 Follower,才有资格在 Leader 挂掉后成为新的 Leader。
Kafka 的 Leader 会跟踪每个 Follower 的同步进度。如果一个 Follower 落后太多(比如网络延迟大、机器负载高),Leader 会把它踢出 ISR。等它追上进度了,再重新加回来。
我曾经遇到过一个问题:某个 Follower 因为 GC 停顿,落后了 Leader 几万条消息。结果 Leader 挂了,ISR 里只剩 Leader 自己,选不出新 Leader。后来我们调整了 replica.lag.time.max.ms 参数,给了 Follower 更多追赶时间,才解决了这个问题。
4.3.3 ACK 机制:生产者的确认策略
生产者写入消息时,可以设置 ACK 参数,控制消息的确认级别:
| ACK 值 | 含义 | 可靠性 | 性能 |
|---|---|---|---|
| 0 | 不等待确认,直接发下一条 | 最低,可能丢数据 | 最高 |
| 1 | 等待 Leader 确认 | 中等,Leader 挂了可能丢 | 中等 |
| all(或 -1) | 等待所有 ISR 副本确认 | 最高,不丢数据 | 最低 |
在增量链路中,我强烈建议使用 acks=all。主数据变更不能丢,哪怕慢一点也值得。我曾经在一个金融项目里,因为用了 acks=1,结果 Leader 挂了,有几条消息没同步到 Follower,导致主数据不一致。后来审计查出来,花了好大功夫才补上。
acks=all 后,如果 ISR 里只剩一个副本,那其实跟 acks=1 没区别。所以一定要保证 ISR 里有足够多的副本。建议设置 min.insync.replicas=2,这样即使 Leader 挂了,至少还有一个 Follower 能顶上。
4.3.4 消费者端的容错:手动提交 Offset
前面提到了,手动提交 Offset 是增量链路中的最佳实践。这里再补充一点:消费逻辑和 Offset 提交要放在同一个事务里。比如,你用 Flink 消费 Kafka,处理完数据后写入目标数据库,然后提交 Offset。如果写入数据库失败了,Offset 就不提交,下次重启会重新消费。
我见过一个典型的错误:先提交 Offset,再处理业务逻辑。结果业务逻辑处理到一半挂了,Offset 已经提交了,数据就丢了。嗯,这个顺序千万别搞反。