第 28 章 / 共 57 章 · 约 5 分钟
关键技术:Kafka
Kafka 是一个分布式的、只追加的日志。生产者把消息写到日志末尾,消费者按自己的进度往后读,消息读完不会被删掉,保留期内可以反复重放。系统设计里只要出现“把事件可靠地交给多个下游”“削峰”“事后重算”,Kafka 基本都是第一候选。
1. 核心模型
- Topic 和分区(partition):一个 topic 拆成多个分区,每个分区是一段有序、不可修改的日志,每条消息有一个递增的 offset。
- 按 key 分区:生产者对消息 key 做哈希来选分区,同一个 key 的消息总在同一个分区里,因此顺序只在分区内成立,跨分区没有全局顺序。
- 复制:每个分区有一个 leader 和若干 follower,跟得上 leader 的副本组成 ISR(in-sync replicas)。读写都走 leader。
- 消费者组(consumer group):同一组里,一个分区同一时刻只分给一个消费者。组内消费者数超过分区数,多出来的会闲着,所以分区数就是一个组的并行度上限。不同的组互不影响,各自记自己的 offset。
- 保留:按时间或大小删除旧数据(常见默认 7 天);也可以开日志压缩(compaction),每个 key 只保留最新一条,适合当作“最新状态表”。
图表可横向滚动,展开后可缩放查看。
查看图表源码
flowchart LR
accTitle: Kafka 分区与消费者组
p["生产者:按 key 哈希选分区"] --> p0["分区 0"]
p --> p1["分区 1"]
p --> p2["分区 2"]
p0 --> a1["消费者组 A:消费者 1"]
p1 --> a1
p2 --> a2["消费者组 A:消费者 2"]
p0 --> b1["消费者组 B:一个消费者读全部分区"]
p1 --> b1
p2 --> b1
2. 什么时候用
- 事件分发:订单创建后,库存、通知、搜索索引、数据仓库各自消费同一份事件,互不拖累。
- 削峰填谷:上游突发流量先写进 Kafka,下游按自己的速度消费,不会被打垮。
- 可重放:下游有 bug,修好后把 offset 拨回去重新处理;新上线的服务可以从头回填历史数据。
- 流处理的输入输出:点击日志、位置更新这类高吞吐数据流,交给 Flink 之类的引擎做实时聚合。
- 变更数据捕获(CDC):把数据库的 binlog/WAL 变更写进 Kafka,用来同步缓存、搜索索引。
3. 什么时候不适合
- 简单的任务队列:需要逐条确认、可见性超时、延迟投递、优先级的场景,SQS、RabbitMQ 这类队列更顺手。Kafka 只按 offset 前进,单条消息处理失败不好“只重试这一条”。
- 请求/响应:同步调用就用 RPC,绕一圈 Kafka 只会增加延迟和复杂度。
- 大消息:单条消息默认上限约 1 MB。图片、视频放对象存储,Kafka 里只传对象的 key。
- 很小的规模:每秒几十条消息、一个消费者,用数据库表或 Redis 列表就够了,不值得运维一个集群。
4. 关键限制和数量级
| 维度 | 要记住的事 |
|---|---|
| 吞吐 | 单分区大约每秒几 MB 到几十 MB;单个 broker 每秒几百 MB;集群可以到每秒百万条以上 |
| 分区数 | 按“目标吞吐 ÷ 单分区吞吐”和“消费者并行度”取较大值,再留余量。分区可以加不能减,而且加分区会改变 key 到分区的映射 |
| 持久性 | 副本数 3、acks=all、min.insync.replicas=2:已确认的写入在一台 broker 挂掉时不丢。acks=1 在 leader 挂掉时可能丢已确认的数据 |
| 语义 | 默认“至少一次”:处理完再提交 offset,崩溃后会重复处理。Kafka 事务能做到 Kafka 内部的读-处理-写恰好一次,但写外部数据库、发邮件这类副作用仍要靠消费者幂等 |
| 延迟 | 端到端通常是几毫秒到几十毫秒,批量和压缩会拿延迟换吞吐 |
| 元数据 | 新版本用内置的 Raft 元数据集群(KRaft),4.0 起不再依赖 ZooKeeper |
5. 面试中常被追问的点
- 怎么保证同一个用户的消息有序? 用 user_id 当 key,同一用户落在同一分区;消费者内部也要按分区串行处理,不能把一个分区的消息丢进线程池乱序执行。
- 消费者处理到一半崩溃怎么办? 重启后从上次提交的 offset 继续,会重复处理一部分,所以下游写入要幂等:用消息里的业务 id 做唯一键,或者记录已处理的 id。
- 一条消息反复失败(毒消息)怎么办? 重试几次后转到重试 topic 或死信 topic,不要卡住整个分区。
- 热点分区:某个 key 流量特别大,会压垮一个分区。可以给 key 加随机后缀打散,代价是这个 key 不再整体有序,下游需要再合并。
- 消费者跟不上:看 consumer lag;扩消费者不能超过分区数,所以分区要一开始就留够。
- 重放会不会把下游打爆:回放时限速,或者先写到新的表里再切换。
6. 和其他技术的取舍
| 选项 | 适合 | 代价 |
|---|---|---|
| Kafka | 高吞吐事件流、多个下游、需要重放 | 运维重;分区和顺序要提前设计 |
| SQS / RabbitMQ | 任务队列:逐条确认、超时重投、延迟、死信 | 消息消费后就没了,不能重放给新下游 |
| Kinesis | 托管的类 Kafka 流,按 shard 计费 | 单 shard 写入约 1 MB/s 或每秒 1000 条,读取约 2 MB/s |
| Redis Streams / Pub/Sub | 轻量场景、低延迟 | 受内存限制;Pub/Sub 不持久,没人订阅时消息直接丢 |
面试时一句话的选择理由:需要多个下游各自消费、能重放、吞吐高,就用 Kafka;只是把任务交给一组 worker,就用队列。
7. 本站案例
- 案例 1:短链接服务:点击事件写入 Kafka,再异步做统计。
- 案例 3:信息流:发帖事件进入 Kafka,由扇出 worker 写入粉丝的时间线。
- 案例 5:网约车:司机位置流写入 Kafka,留给计费、风控和分析。
- 案例 6:通知系统:按优先级分 topic,配合 outbox 和死信处理。
- 案例 11:搜索自动补全:查询日志经 Kafka 做聚合和热词检测。