Kafka 是一个开源的分布式事件流平台,被数千家公司用于高性能数据管道、流式分析、数据集成和关键业务应用。它的核心是一个发布/订阅(pub/sub)系统:生产者把消息写入主题(topic),消费者从它们关心的主题读取。它远远超出了 ActiveMQ 或 RabbitMQ 这类传统消息队列。

⚡ 速览要点
  • 是日志,不是队列——消息被读取后仍然保留;每个消费者跟踪自己的 offset。
  • 分区(partition)带来横向扩展和按 key 的有序性。
  • 消费者组(consumer group)带来并行读取——每个成员一个分区。
  • acks + ISR + 幂等把持久性一路调到 exactly-once。
  • 之所以快:顺序写、页缓存(page cache)、零拷贝(zero-copy)。
tldr

Kafka 是一个持久、分区、带副本的提交日志(commit log)。生产者把记录追加到分区,消费者按自己的节奏拉取,复制 + acks 给你可调的持久性——从 fire-and-forget 到 exactly-once。

主要用途

发布/订阅模型

不同类别的消息从各种生产者流入不同的主题(topic)。与传统队列不同,消息在被消费后仍然保留——Kafka 独立于任何单个消费者管理生命周期,所以多个消费者可以在不同的 offset 读同一个主题。

生产者架构

生产者用一个 ProducerRecord(消息包装)调用 .send()。流程:

消费者模型

Kafka 用拉(pull)模型——消费者按自己的节奏请求数据,比推(push)更解耦。许多独立消费者可以读同一个主题,但同一个消费者组内的消费者不能共享一个分区:整个组表现为一个逻辑订阅者,每个分区恰好被一个成员拥有。

分区与副本

并行来自把一个主题拆成分散在多台服务器上的分区DefaultPartitionerhash(key) % num_partitions;自定义实现重写 partition()

每个分区是一个追加写日志,按 1 GB 分段,每 4 KB 一条稀疏索引记录以便快速查找。保留策略(log.retention.hours/minutes/ms,默认 7 天)根据 log.cleanup.policy 删除或压实旧数据。复制通过 leader/follower 方案防范单点故障。

系统可靠性

acks

这个生产者侧设置控制一条记录在被视为写入成功前必须被多少个 broker 接收——核心的持久性/吞吐取舍:

设置行为
acks=0记录一发送就算成功——不等 broker 响应。最快,最不持久。
acks=1一旦 leader 收到记录就算成功。
acks=all (-1)只有所有同步副本(ISR)确认后才算成功。若 ISR 掉线,流式会阻塞。min.insync.replicas=n 保证最少的副本数。
可靠传输

要持久投递:acks=-1num_partitions > 1min.insync.replicas > 1

重试与投递语义

Kafka ≥ 2.1 的 RETRIES_CONFIG 默认是 MAX_INT。重试若不加管理会有重复风险:

副本

多个 follower 从 leader 同步。生产者只写 leader;follower 从它拉取。ISR 列出同步中的 follower;落后超过 replica.lag.time.max.ms 的 follower 移到 OSR。ISR + OSR = 已分配副本(AR)。leader 故障时,从 ISR 选出新 leader。每个副本跟踪自己的 LEO(Log End Offset)以衡量同步进度。

高速读写

ZooKeeper

分布式系统的协调服务。对 Kafka,它跟踪集群节点状态和主题/消息元数据。五项主要功能:

CLI 示例

创建一个 2 分区、复制因子 3 的主题:

bash
# test.config → bootstrap.servers=localhost:9092
kafka-topics \
  --bootstrap-server localhost:9092 \
  --command-config ./test.config \
  --topic test1 \
  --create \
  --replication-factor 3 \
  --partitions 2

生产带 key 的消息,然后从头消费:

bash
# 生产者
kafka-console-producer \
  --topic test1 \
  --broker-list localhost:9092 \
  --property parse.key=true \
  --property key.separator=:
# > key:{"val":0}

# 消费者
kafka-console-consumer \
  --topic test1 \
  --bootstrap-server localhost:9092 \
  --property print.key=true \
  --from-beginning

消费者组与再均衡

消费者组是 Kafka 并行消费的单位。组里每个消费者实例被分到一个互不相交的分区子集,整个组一起消费完整主题。给一个 5 分区的主题加上第 6 个消费者,那个消费者会闲着——没有多余分区可分。这个约束故意保持简单:它意味着每个分区在每个组里恰好有一个权威 offset,进度永远不含糊。

再均衡协议

只要组成员变化就触发再均衡(rebalance):有新消费者加入、一个现有的崩溃或超过 session.timeout.ms、主题加了新分区,或应用调了 unsubscribe()。再均衡期间,Group Coordinator(为每个消费者组选出的一个 broker)停止所有消费、撤销分区分配,然后重新分配。代价是一次暂停——有时几秒——期间不处理消息。因此尽量减少再均衡的频率和时长是一个重要的运维关注点。

properties
# 调这些来减少不必要的再均衡
session.timeout.ms=45000        # 静默的消费者多久后被判定为死
heartbeat.interval.ms=15000     # 必须 < session.timeout.ms / 3
max.poll.interval.ms=300000    # 两次 poll() 之间的最大间隔,超过则被踢出
partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor

Eager vs. Cooperative 再均衡

经典的 eager 再均衡(RangeAssignorRoundRobinAssignor 用的)在任何重分配开始前撤销所有分区分配——一次全局 stop-the-world 暂停。Kafka 2.4+ 引入了协作式粘性再均衡协议(CooperativeStickyAssignor):消费者只撤销需要移动的分区,保留它继续持有的那些。这通常对不涉及重分配的消费者完全消除暂停,并让分区分配保持"粘性"以最小化不必要的状态迁移。

max.poll.interval.ms 是隐形杀手。如果你的 poll() 循环耗时超过它(比如消息处理下游有个慢的数据库写),broker 会以为消费者死了并触发再均衡。修法:要么调大超时,要么更好地把慢工作移到单独线程、让 poll 循环保持轻快。

静态组成员

Kafka 2.3+ 加了 group.instance.id,给消费者一个跨重启的持久身份。有了静态成员,一个崩溃后在 session.timeout.ms 内重新加入的消费者无需再均衡就能收回它原来的分区——对容器化负载(部署时 pod 频繁重启)是个重要优化。

日志压实(Log Compaction)

Kafka 的保留有两种口味。默认的 delete 策略只是删除比 log.retention.hours 旧(或超过大小阈值)的段。日志压实(log compaction)根本不同:它不按时间丢弃消息,而是只保留每个 key 的最新消息,形成一个行为像最终一致键值存储的压实日志。

bash
# 为用户画像变更日志创建一个压实主题
kafka-topics \
  --bootstrap-server localhost:9092 \
  --topic user-profiles \
  --create \
  --partitions 12 \
  --replication-factor 3 \
  --config cleanup.policy=compact \
  --config min.cleanable.dirty.ratio=0.1 \
  --config segment.ms=86400000    # 每天滚动一个新段

日志清理器怎么工作

broker 的后台日志清理器(log cleaner)线程扫描每个分区的"脏"(最近写入)部分。它们在内存建一个 offset 映射(key → 最新 offset),然后只把脏部分里每个 key 的最新记录拷进一个新的"干净"段,丢弃较旧的重复。结果:压实日志只随不同的 key 增长,而不随时间增长。

一条特殊的墓碑消息(tombstone)——key 非空、value 为 null 的记录——表示删除。压实后,连墓碑最终也会被移除(在 delete.retention.ms 之后),所以读压实日志的消费者看到该 key 被彻底抹掉。

用例

日志压实驱动 Kafka Streams 的状态存储changelog 主题。当一个 KTable 在崩溃后重建时,应用只重放压实日志——而非完整历史——来重建当前状态。数据库 CDC 主题(Debezium)也用压实,让下游消费者能从每个主键的最新行镜像引导。

KRaft——没有 ZooKeeper 的 Kafka

ZooKeeper 多年作为 Kafka 的集群协调层:controller 选举、broker 成员、主题元数据。但它引入了一个独立的运维依赖、独立的扩展关注点,以及集群启动时的瓶颈。KRaft(Kafka Raft Metadata 模式),Kafka 2.8 引入、从 3.3 起生产可用,用 Kafka 自带的 Raft 共识实现完全替代了 ZooKeeper。

架构转变

在 KRaft 下,一部分 broker 被指定为 controller。它们运行一个专用的元数据分区(主题 __cluster_metadata),通过 Raft 协议复制。活跃的 controller 就是 Raft leader。所有集群元数据——broker 注册、主题配置、分区 leader——都活在这个单一日志里,而非散布在 ZooKeeper znode 中。

properties
# KRaft server.properties(broker+controller 合一节点)
process.roles=broker,controller
node.id=1
controller.quorum.voters=1@kafka1:9093,2@kafka2:9093,3@kafka3:9093
listeners=PLAINTEXT://:9092,CONTROLLER://:9093
inter.broker.listener.name=PLAINTEXT
controller.listener.names=CONTROLLER
log.dirs=/var/lib/kafka/data

KRaft 的好处

迁移路径

Kafka 3.5+ 支持迁移模式,让你不停机就把现有的基于 ZooKeeper 的集群转成 KRaft:过渡期同时运行两个协调器,等所有元数据都在 KRaft 后再下线 ZooKeeper。生产中绝不要跳过这个双写阶段。

性能调优参数

Kafka 的默认值是保守的。真实世界的调优瞄准三个维度:生产者吞吐消费者延迟(lag)持久性保证。下面这些参数是最关键的杠杆。

生产者调优

properties
# 吞吐优化的生产者
acks=all                   # 持久写到所有 ISR 副本
enable.idempotence=true    # 去重重试,生产者会话内 exactly-once
linger.ms=5               # 最多等 5ms 攒批;0 = 立即发送
batch.size=65536          # 64 KB 批;越大往返越少、延迟越高
compression.type=lz4     # lz4 CPU/压缩比最佳;snappy 也常见
buffer.memory=67108864   # 64 MB 内存缓冲;高量生产者调大
max.in.flight.requests.per.connection=5  # 开了幂等时 >1 也没问题

消费者调优

properties
fetch.min.bytes=1024         # 等到有 1 KB 可用;减少 fetch 往返
fetch.max.wait.ms=500        # 等待 fetch.min.bytes 满足的最大时间
max.poll.records=500         # 每次 poll() 的记录数;处理慢就调小
auto.offset.reset=earliest  # 首次运行:从最旧开始;latest = 只读尾部
enable.auto.commit=false    # 手动提交以实现 exactly-once 处理语义

Broker 与主题调优

properties
# broker 级
num.io.threads=8             # I/O 线程数;设成磁盘数
num.network.threads=3       # 网络请求线程
socket.send.buffer.bytes=102400
socket.receive.buffer.bytes=102400
log.flush.interval.messages=10000  # 每 1 万条 fsync;交给 OS 性能最佳

# 通过 kafka-configs 做主题级覆盖
kafka-configs --bootstrap-server localhost:9092 \
  --alter --entity-type topics --entity-name orders \
  --add-config retention.ms=604800000,min.insync.replicas=2

Kafka vs. RabbitMQ vs. Apache Pulsar

选对消息系统是你能做的最高杠杆的架构决策之一。Kafka、RabbitMQ、Pulsar 各占设计空间里不同的一点。

维度KafkaRabbitMQPulsar
消息模型持久日志——消费后保留;消费者可在任意 offset 重放队列——确认后消息被删除日志 + 队列统一:主题由持久日志支撑,队列作为游标抽象
吞吐靠顺序磁盘 I/O 每集群每秒百万级消息每队列约 5 万–10 万 msg/s(受内存限制)靠 Apache BookKeeper 存储,与 Kafka 相当
延迟调好批处理后个位数 ms;开 linger 更高p50 亚毫秒(无批处理开销)通常 5–15 ms;地理复制更高
路由主题 + 分区 key;无基于内容的路由通过 exchange 丰富路由(direct、fanout、topic、headers)命名空间 + 主题层级;无 exchange 路由
重放原生——在保留窗口内 seek 到任意 offset不支持——消费过的消息没了原生——由 BookKeeper ledger 支撑
多租户命名空间(有限)虚拟主机一等公民:租户 → 命名空间 → 主题,带配额
地理复制MirrorMaker 2(异步)Shovel / Federation 插件内建、同步的跨数据中心复制
运维复杂度中等(KRaft 去掉 ZK 依赖)低——优秀的管理 UI、易上手高——历史上 Kafka + ZooKeeper + BookKeeper 三件套
最适合事件流、CDC、审计日志、高吞吐管道任务队列、RPC 模式、复杂路由、低延迟任务多租户 SaaS、原生地理复制、PB 级分层存储
选型指南

需要重放、高吞吐、CDC 或事件溯源时选 Kafka。需要复杂路由逻辑、任务队列或亚毫秒延迟时选 RabbitMQ。需要内建跨数据中心地理复制,或带每命名空间配额隔离的真多租户时选 Pulsar——代价是显著更高的运维复杂度。

Kafka Streams 与 Kafka Connect

Kafka 不只是消息代理——它是整个流处理生态的基础。两个组件把它延伸得最远:Kafka Streams 在你的 JVM 应用内做流处理,Kafka Connect 做即插即用的数据管道连接器。

Kafka Streams

Kafka Streams 是一个轻量客户端库(不是集群),用于构建流处理应用。不像 Apache Flink 或 Spark Streaming,没有单独的处理集群要管——库跑在你的应用进程里,从输入主题读、把结果写到输出主题。容错靠把状态(窗口聚合、连接表)存在磁盘上的 RocksDB,并由一个 Kafka changelog 主题支撑,用于崩溃后恢复。

java
StreamsBuilder builder = new StreamsBuilder();

// 从 "orders" 主题读,按 userId 分组,在 5 分钟窗口内计数
KStream<String, Order> orders = builder.stream("orders");
orders
  .groupByKey()
  .windowedBy(TimeWindows.ofSizeWithNoGrace(Duration.ofMinutes(5)))
  .count()
  .toStream()
  .to("orders-per-user-5min");  // 把结果写回 Kafka

KafkaStreams streams = new KafkaStreams(builder.build(), props);
streams.start();

Kafka Streams 的关键概念:

Kafka Connect

Kafka Connect 是一个可扩展、容错的框架,用于在 Kafka 和外部系统(数据库、对象存储、搜索引擎、SaaS API)之间搬数据,无需写自定义代码。连接器有两种:source 连接器从外部系统读、写入 Kafka(如 Debezium PostgreSQL CDC、S3 source),sink 连接器从 Kafka 读、写入外部系统(如 Elasticsearch sink、Snowflake sink、JDBC sink)。

json
{
  "name": "postgres-orders-cdc",
  "config": {
    "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
    "database.hostname": "db.prod.internal",
    "database.port": "5432",
    "database.user": "debezium",
    "database.dbname": "orders",
    "table.include.list": "public.orders,public.inventory",
    "topic.prefix": "cdc",
    "plugin.name": "pgoutput",
    "slot.name": "debezium_orders",
    "publication.name": "dbz_publication"
  }
}

Connect 作为一个 worker 集群运行,把连接器任务分摊到各 worker。若一个 worker 失败,它的任务被重新分配给存活的 worker——和消费者组再均衡完全相同的机制。这意味着 Connect 靠加 worker 横向扩展,并在节点故障时无需运维介入就能存活。

Schema Registry 与 Avro

随着 schema 演化,Kafka 主题里的裸字节会变成维护噩梦。Confluent Schema Registry(或 AWS Glue Schema Registry)存储 Avro、Protobuf 或 JSON Schema 定义,并给每个版本分配一个数字 ID。生产者通过在载荷前缀嵌入 4 字节 schema ID 来序列化消息;消费者按 ID 查 schema 来反序列化。这实现了带兼容性检查的 schema 演化:registry 拒绝与已注册版本向后或向前不兼容的新 schema 版本,防止独立部署的服务之间悄悄的数据损坏。

avro
{
  "type": "record",
  "name": "OrderEvent",
  "namespace": "com.example.orders",
  "fields": [
    { "name": "orderId",  "type": "string" },
    { "name": "userId",   "type": "string" },
    { "name": "totalCents", "type": "long" },
    { "name": "currency", "type": "string", "default": "USD" }
  ]
}

常见坑与最佳实践

分区数——选对它

分区是 Kafka 的并行单位,创建后不能减少(只能增加)。选太少会封顶你的消费者吞吐上限;选太多浪费内存并增加再均衡时间。一个粗略起点:按你 broker 的磁盘带宽,每分区瞄准 1–3 MB/s 吞吐,并大致每个你预期峰值运行的消费者实例配 1 个分区。一个常见错误是为"保持简单"用单分区建主题——这让组里除一个外的每个消费者都闲着。

Offset 管理

enable.auto.commit=true 时,offset 会被周期性提交,而不管消费者是否真的处理了消息。在提交和下游写之间崩溃意味着消息被永久跳过。用 enable.auto.commit=false,只在你的处理完成且持久化后才提交 offset。要 exactly-once,把下游写和 offset 提交包进一个 Kafka 事务。

消费者 lag 监控

消费者 lag——最新生产 offset 和消费者已提交 offset 之间的差距——是 Kafka 消费者的首要运维健康指标。持续增长且不回落的 lag 表示要么消费者慢,要么生产突然飙升。用 Kafka 内建的 consumer group describe、Confluent 的 Kafka Consumer Lag Exporter,或 LinkedIn 的 Burrow 监控它。

bash
# 查看一个消费者组所有分区的 lag
kafka-consumer-groups \
  --bootstrap-server localhost:9092 \
  --group order-processing-group \
  --describe

# 示例输出
# GROUP                   TOPIC  PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG
# order-processing-group  orders  0          1023440         1023441         1
# order-processing-group  orders  1          998772          999201          429  ← lag 在涨!

避免常见反模式

真实使用场景

理解组织为何选 Kafka——而非更简单的队列——能巩固你在面试里何时推荐它。

总结

把 Kafka 想成一个带副本的日志,不是队列。分区给你横向扩展和按 key 的有序;消费者组给你并行读;acks + ISR + 幂等让你把持久性从 fire-and-forget 一路调到 exactly-once。KRaft 把 ZooKeeper 从方程里去掉;日志压实把主题变成持久的 KV 变更日志;Kafka Streams 和 Connect 补齐一个无需外部依赖的完整流式生态。

🎯 面试速答

Kafka 怎么保证 exactly-once?幂等生产者(PID + 序列号防止重试时重复)加上把 produce + offset 提交原子地包进 beginTransaction()/commitTransaction() 的事务。
Kafka 为什么这么快?顺序追加写 + OS 页缓存(避免双重缓冲)+ 零拷贝 sendfile 系统调用 + 批量压缩。
什么触发消费者组再均衡?成员加入/离开、超过 session 超时(session.timeout.ms)、超过 max poll 间隔(max.poll.interval.ms),或分区数变化。用 CooperativeStickyAssignor 让再均衡增量化而非 stop-the-world。
日志压实 vs 保留?保留按时间/大小删旧段;压实只保留每个 key 的最新记录——实质是一个持久的 KV 变更日志。空值墓碑表示删除。
Kafka vs RabbitMQ?Kafka 用于高吞吐事件流、重放和 CDC;RabbitMQ 用于复杂路由、任务队列和亚毫秒延迟需求。
KRaft 是什么、为何重要?Kafka 内建的 Raft 元数据层,替代 ZooKeeper,去掉独立依赖、支持百万级分区、把集群启动从分钟降到秒。

← 返回
技术栈