Kafka 与流处理实战
Kafka 不只是消息队列,它是事件流平台——日志、流处理、事件驱动的基石。本文讲清 Kafka 的核心模型(topic/partition/offset/consumer group)、为什么它吞吐这么高、消费组与分区的关系、Exactly-Once 与顺序保证,以及什么时候该上 Kafka。
本文要点
- 主题聚焦:DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
Kafka 与流处理实战
先说一个反常识的事:
很多团队把 Kafka 当成"高级版 RabbitMQ"在用——发消息、收消息、ack、完事。然后某天业务方问:"能不能把用户行为实时算一下,做实时画像?"大家面面相觑,又开始搭一套 Flink。
这种尴尬的根因在于:Kafka 从来就不是消息队列,它是分布式事件流平台。如果你只把它当 MQ 用,等于买了一台挖掘机来送外卖——能用,但浪费了 90% 的能力,还多花了不少运维成本。
这篇文章我尽量讲透 Kafka 的底层模型、为什么吞吐这么恐怖、消费组怎么玩、顺序和 Exactly-Once 怎么保证,以及——什么时候你真的需要它。
一、Kafka 是什么:不是 MQ,是事件流平台
理解 Kafka,要从"日志(Log)"这个词入手。
Kafka 的核心数据结构不是队列(Queue),而是追加写、可回放、按 offset 读取的有序日志。这个设计来自 LinkedIn 内部的数据基础设施需求:他们需要把活动数据(页面访问、监控指标、用户行为)以高吞吐、低延迟、可重放的方式,从生产端送到多个消费端。
队列和日志的区别是根本性的:
队列:消费即删除,消息只有一次生命
日志:消费只是移动 offset,消息保留到过期,可被多次重放
这一个差异决定了 Kafka 的所有特性:
- 可重放:消费方挂了、逻辑改了、新增下游,都能把历史数据重新消费一遍——这是流处理的基础。
- 多订阅者独立:不同消费组各自维护自己的 offset,互不干扰。
- 高吞吐:顺序追加磁盘 + 批量读写,把磁盘当顺序设备用。
Kafka 解决的是三类问题:
- 日志聚合:应用日志、访问日志、监控指标统一收集
- 事件流:业务事件(下单、支付、库存变更)的发布订阅总线
- 事件驱动 / 流处理:实时 ETL、实时数仓、实时风控、CDC 同步
一句话定位:Kafka 是为"事件流"而生的高吞吐、可持久化、可重放的分布式日志系统。
二、核心模型:Topic / Partition / Offset / Consumer Group
四个概念串起来,就懂了 Kafka 一大半。
Topic(主题)
逻辑上的消息分类,比如 order-events、user-clicks。对开发者来说就是"往哪儿发、从哪儿读"的地址。
Partition(分区——并行的灵魂)
每个 Topic 被切成多个 Partition,每个 Partition 是一段有序的、不可变的、不断追加的日志。
Topic: order-events (3 个分区)
P0: [m0][m1][m2][m3][m4] ...
P1: [m0][m1][m2][m3] ...
P2: [m0][m1][m2][m3][m4][m5] ...
Partition 是 Kafka 的并行单元。吞吐横向扩展靠的就是加分区:一个分区只能被一个 Broker 上的 leader 处理写入,分区多了,就能分布在多个 Broker 上并行承压。
Offset(消费位置)
每个 Partition 内,每条消息有一个单调递增的整数 offset。消费者通过"我读到 offset=X 了"来标记进度。
Offset 由消费者自己维护(存在 Kafka 内部 topic __consumer_offsets 里)。这意味着消费者可以前进、后退、停留——重放历史、跳过坏数据、回滚到某个时间点,都是改个 offset 的事。
Consumer Group(消费组)
这是 Kafka 区别于普通 pub/sub 的关键。一条消息在同一个消费组里只被一个消费者消费,但不同消费组各自消费全量。
Topic 3 个分区,消费组 G1 里有 3 个消费者:
P0 → C0
P1 → C1
P2 → C2
再加一个消费者 C3 进 G1?C3 闲置——分区不够分了。
再来一个独立的消费组 G2?G2 里的消费者各自收到全量消息。
所以 Kafka 同时支持两种语义:
- 队列语义:一个消费组内多个消费者分担负载(提高吞吐)
- 发布订阅语义:多个消费组各自独立消费(广播)
三、为什么这么快:顺序写、零拷贝、批量、分区并行
Kafka 单机就能扛住十万、百万级 TPS,不是魔法,是几件事做对了的叠加。
1. 顺序写盘
磁盘的随机写慢得像 1990 年代,但顺序写非常快——某些场景下甚至比内存随机访问还快。Kafka 只追加写日志,从不修改,天然契合磁盘的顺序特性。
2. 批量与压缩
Producer 不是来一条发一条,而是按 batch 攒一批再发。Batch 还可以做端到端压缩(Snappy / LZ4 / Zstd),网络和磁盘都省字节。生产环境吞吐调优,核心就是调 batch.size 和 linger.ms。
3. 零拷贝(sendfile)
消费者读消息时,Kafka 用 Linux 的 sendfile 系统调用,数据从页缓存直接走 DMA 到网卡,完全不进用户态。
传统: 磁盘 → 内核页缓存 → 用户态 → Socket 缓冲 → 网卡 (4 次拷贝)
零拷贝: 页缓存 → 网卡 (2 次拷贝,DMA 完成)
消费者读的数据大部分还在页缓存(热数据)时,Kafka 几乎是在做内存到网络的转发,性能爆炸。
4. 分区并行
单 Partition 的吞吐有上限(主要是顺序 IO 上限),但分区是水平切分的,3 个分区就能把吞吐近似放大 3 倍。加上副本机制(replication),高可用和扩展性都拿到了。
四、消费组与分区:并行靠分区,rebalance 是双刃剑
记住这条铁律:
同一个消费组内,一个 Partition 只能被一个消费者消费。
所以你想提高消费并行度,只能加分区,不能光加消费者。消费者数量超过分区数,多出来的就是摆设。
分区数 = 6 → 消费组里最多 6 个消费者能干活
加到 10 个消费者?4 个永远 idle
这也是为什么分区数在创建 Topic 时就要规划好——后期加分区会破坏 key 的路由一致性(同 key 可能被分到不同分区,顺序就乱了)。
Rebalance(再均衡)
消费组里的消费者会变动:扩容、缩容、宕机、网络抖动。这时 Kafka 要把分区重新分配给活着的消费者,这个过程叫 rebalance。
Rebalance 是个 "stop the world" 操作——进行期间整个消费组所有消费者都暂停消费。频繁 rebalance 是 Kafka 消费端最常见的事故源:
消费者处理慢 → 心跳超时 → 被踢出 → rebalance → 堆积更多 → 处理更慢 → 又超时……死循环。
对策:合理设置 session.timeout.ms / heartbeat.interval.ms / max.poll.interval.ms,长任务调大 max.poll.interval,别让单批处理时间超过心跳预算。
五、顺序与投递语义:分区内有序,Exactly-Once 怎么保证
顺序保证
Kafka 的顺序模型一句话:分区内有序,跨分区无序。
如果你需要"同一个订单的事件按顺序处理",就把 orderId 作为消息 key,Producer 会按 key 哈希到固定分区,保证同 key 进同一分区,自然保序。
Producer 按 key 取 hash → 固定 Partition
orderId=o1 → P0 (o1 的所有事件按写入顺序排好)
orderId=o2 → P1
不要追求全局有序——那意味着只有一个分区,等于把并行度砍到 1,吞吐也砍到 1。工程上几乎从不需要全局有序,需要的是"业务实体级别有序"。
投递语义
三种语义,由配置决定:
| 语义 | 实现 | 风险 |
|---|---|---|
| At-Most-Once(最多一次) | acks=0 或不重试 | 可能丢 |
| At-Least-Once(至少一次) | acks=all + 重试(默认) | 可能重复 |
| Exactly-Once(恰好一次) | 幂等 producer + 事务 | 重 |
At-Least-Once + 业务幂等是工程上最务实的方案,99% 的场景这个组合就够了——重复消息靠业务唯一键去重,代价远小于上事务。
Exactly-Once 是 Kafka 0.11 之后才支持的,需要:
- 幂等 producer(
enable.idempotence=true):producer 给每条消息带序列号,broker 端去重,解决单 producer 重试导致的重复 - 事务:把"消费-处理-生产"的整条链路包进一个事务,要么全成功要么全回滚——主要给 Kafka Streams 和"消费 Kafka 写 Kafka"的场景用
注意一个关键认知:Exactly-Once 是 Kafka 内部的语义,不是端到端的。你消费 Kafka 写 MySQL,这个"恰好一次"还得靠业务幂等兜底。别被营销话术骗了。
六、流处理:Kafka Streams 与 ksqlDB
既然 Kafka 本身就是事件流,在它上面直接做流处理是顺理成章的事。
Kafka Streams
Kafka 自带的流处理库(注意是库,不是独立的计算集群)。它跑在你的应用进程里,把"消费 Kafka → 转换 → 写回 Kafka"做成了编程模型:
StreamsBuilder builder = new StreamsBuilder();
builder.stream("user-clicks")
.filter((k, v) -> v.region().equals("cn"))
.groupBy((k, v) -> v.userId())
.count()
.toStream()
.to("user-click-counts");
特点:无独立集群、天然 Exactly-Once、状态管理用 RocksDB、容错靠 changelog topic。轻量级流处理首选,比上 Flink 简单太多。
ksqlDB
如果你想用 SQL 写流处理,ksqlDB 是官方答案:
CREATE STREAM clicks (userId VARCHAR, region VARCHAR)
WITH (KAFKA_TOPIC='user-clicks', VALUE_FORMAT='JSON');
CREATE TABLE click_counts AS
SELECT userId, COUNT(*) AS cnt
FROM clicks WINDOW TUMBLING (SIZE 5 MINUTES)
GROUP BY userId;
适合实时大屏、简单 ETL、监控告警这类声明式场景。复杂窗口、自定义聚合还是 Kafka Streams / Flink 更灵活。
什么时候选 Flink 而不是 Kafka Streams?
- 需要复杂窗口语义(会话窗口、迟到数据处理)
- 流批一体(同一套代码跑流和批)
- 跨多种数据源(Kafka + 数据库 + 文件)
- 大规模集群的统一调度
简单 "Kafka 进 Kafka 出" 的实时 ETL,Kafka Streams 性价比更高。
七、什么时候该上 Kafka,什么时候别上
这是最重要的章节。很多团队上 Kafka 是"听说大厂都用",而不是"我真的需要"。
该上 Kafka
- 日志/指标/埋点聚合:多机房、多服务、TB 级数据,典型 Kafka 主场
- 事件驱动微服务:业务事件总线,多个下游订阅同一事件
- 实时数据管道:CDC → Kafka → 数仓 / 湖 / Flink
- 流处理:实时风控、实时推荐、实时大屏
- 需要消息回放:消费逻辑会变、要做 A/B 重算
别上 Kafka(用错了比不用更糟)
- 简单任务队列:发邮件、跑定时任务,RabbitMQ / Redis 一把梭
- 复杂路由:按 header 路由、优先级队列、死信回溯——RabbitMQ 是王者
- 低延迟小消息:Kafka 优化方向是吞吐不是延迟,几十字节的消息用 Kafka 是浪费
- 中小业务 + 运维薄弱:Kafka 集群运维不轻,Broker、Zookeeper/KRaft、副本、监控、磁盘扩容都是事。3 人小团队上 Kafka,大概率被运维反噬
选型速查
| 场景 | 推荐 |
|---|---|
| 日志 / 流 / 事件总线 | Kafka |
| 业务消息 + 复杂路由 | RabbitMQ |
| 事务消息(订单 / 支付) | RocketMQ |
| 已有 Redis,简单异步 | Redis Streams |
| 超大吞吐 + 复杂计算 | Kafka + Flink |
一句话:别用 Kafka 解决 RabbitMQ 的问题,也别用 RabbitMQ 撑 Kafka 的吞吐。
小结
几条要点浓缩一下:
- Kafka 是事件流平台,不是消息队列。底层是追加写的有序日志,可重放、多订阅者独立,这决定了它的所有特性。
- Partition 是并行的灵魂:吞吐扩展靠加分区,消费并行度受分区数限制——分区数要在创建 Topic 时就规划好。
- 快不是因为魔法,是工程叠加:顺序写、批量、零拷贝、分区并行,四件事一起做对,才有了百万 TPS。
- 顺序选分区顺序,投递选 At-Least-Once + 幂等。Exactly-Once 是 Kafka 内部语义,端到端一致性还得业务自己兜底。
- Kafka Streams 适合轻量 ETL,Flink 适合复杂计算。别一上来就 Flink,先把 Kafka Streams 跑顺。
最后送一句金句:
Kafka 的本质,是把"日志"这种最朴素的数据结构,放大到了分布式系统的尺度。它快的秘密不在算法,而在它尊重了磁盘、网络、并行的物理规律——所有的"高级架构",最后都是对物理规律的诚实利用。
下一篇我们聊聊"流处理进阶:Flink 与 Kafka Streams 怎么选",看看流计算到底怎么落到业务里。
Related
相关推荐
文章
Docker 部署 Next.js 社区项目的注意事项
围绕 DevOps、Next.js 的技术趋势、工程实践和独立开发视角。
文章
OpenTelemetry 如何帮助你观察 AI 应用?
围绕 DevOps、AI 的技术趋势、工程实践和独立开发视角。
文章
长运行工具调用:Operation、Poll 与 Cancel 的终态协议
初始请求超时后,工具可能仍在服务端运行;发出取消也不等于副作用已停止。本文为单次长运行工具调用建立 operation 身份、轮询水位、取消竞态和唯一终态接受点。
文章
Agent 记忆删除传播:从用户遗忘请求到可验证清除
删除主表一行不会清掉向量、摘要、缓存和恢复后的备份。本文用删除状态机、代际墓碑、派生血缘与端到端 canary 验证真正遗忘。