面试 | Kafka

文章目录

Kafka 面试完全指南(Go 校招向)


一、Kafka 是什么

Kafka 是 LinkedIn 开源的分布式消息队列,现由 Apache 维护。核心定位:

复制代码
生产者 → Kafka集群(持久化存储) → 消费者

解决三个核心问题

  • 异步解耦:订单服务下单后直接返回,不等库存/通知等下游处理
  • 流量削峰:瞬时高并发写入 Kafka,消费者按自身速度消费
  • 数据管道:日志收集、埋点数据、CDC(数据库变更同步)

与 RabbitMQ 的核心区别

维度 Kafka RabbitMQ
模型 拉取(Pull) 推送(Push)
吞吐量 百万级/秒 万级/秒
持久化 默认持久化,可回溯 消费后删除
消费方式 消费者组并行消费 竞争消费
适用场景 大数据、日志、高吞吐 业务解耦、任务队列

二、核心概念(必须烂熟于心)

架构全景

复制代码
Producer                    Kafka Cluster                    Consumer
                    ┌─────────────────────────┐
                    │  Broker 1               │
                    │  ┌─────────────────┐    │
写入 ──────────────▶│  │ Topic: order    │    │──────────────▶ Consumer Group A
                    │  │  Partition 0 [Leader] │    │           (消费者1 拉取 P0)
                    │  │  Partition 1 [Leader] │    │──────────▶ Consumer Group B
                    │  └─────────────────┘    │           (消费者2 拉取 P1)
                    │  Broker 2               │
                    │  ┌─────────────────┐    │
                    │  │  Partition 0 [Follower]│  │
                    │  │  Partition 1 [Follower]│  │
                    │  └─────────────────┘    │
                    └─────────────────────────┘
                              │
                           ZooKeeper / KRaft
                        (元数据 & Leader 选举)

Topic & Partition(分区)

Topic:消息的逻辑分类,类似数据库的表名。

Partition :Topic 的物理分片,是 Kafka 并行度的基本单位

复制代码
Topic: order
  ├── Partition 0: [msg0] [msg1] [msg4] [msg7] ...
  ├── Partition 1: [msg2] [msg5] [msg8] ...
  └── Partition 2: [msg3] [msg6] [msg9] ...
  • 每个 Partition 内部消息有序 ,Partition 间无序
  • Partition 数量决定最大消费并发数(消费者数 ≤ Partition 数才有意义)
  • 消息写入哪个 Partition 由分区策略决定

Offset(偏移量)

每条消息在 Partition 内有唯一的自增编号,即 Offset。

复制代码
Partition 0:
  offset=0: {"order_id": 1001}
  offset=1: {"order_id": 1002}
  offset=2: {"order_id": 1003}  ← 消费者当前消费到这里
  offset=3: {"order_id": 1004}  ← 待消费
  • 消费进度由消费者自己维护 ,存储在 Kafka 内部 Topic __consumer_offsets
  • 消费者崩溃重启后,从上次提交的 Offset 继续消费
  • 支持重置 Offset,实现消息回溯(RabbitMQ 不支持)

Broker & 集群

  • Broker:Kafka 集群中的单个节点(一个 JVM 进程)
  • Controller:集群中特殊的 Broker,负责 Leader 选举、Partition 分配
  • ZooKeeper / KRaft
    • Kafka 2.x 之前:依赖 ZooKeeper 存储元数据、做 Controller 选举
    • Kafka 3.x 之后:KRaft 模式,去掉 ZooKeeper 依赖,Controller 内置

Consumer Group(消费者组)

复制代码
Topic: order (3个Partition)

Consumer Group A(订单处理):
  Consumer1 → Partition 0
  Consumer2 → Partition 1
  Consumer3 → Partition 2

Consumer Group B(数据统计):
  Consumer1 → Partition 0, 1, 2  (只有一个消费者,消费所有分区)

核心规则

  • 同一 Consumer Group 内,一个 Partition 只能被一个消费者消费
  • 不同 Consumer Group 之间互相独立,都能消费同一 Topic 的全量数据
  • 消费者数 > Partition 数时,多余的消费者空闲(浪费资源)

Replica(副本)与 ISR

每个 Partition 有多个副本,分布在不同 Broker 上:

复制代码
Partition 0:
  Leader副本   → Broker1(负责读写)
  Follower副本 → Broker2(同步数据,不提供读写)
  Follower副本 → Broker3(同步数据,不提供读写)

ISR(In-Sync Replicas):与 Leader 保持同步的副本集合。

  • Follower 落后 Leader 超过阈值(replica.lag.time.max.ms,默认30s)→ 踢出 ISR
  • Leader 宕机时,只有 ISR 中的 Follower 才有资格被选为新 Leader
  • 这是 Kafka 高可用的核心保障

GroupCoordinator(消费者组协调器)

每个 Consumer Group 由集群中某个 Broker 担任 GroupCoordinator,负责:

  • 管理消费者的加入/退出
  • 触发和执行 Rebalance
  • 维护 Offset 提交(写入 __consumer_offsets

如何找到自己组的 Coordinator

复制代码
hash(group.id) % __consumer_offsets 的 Partition 数(默认50)
= 该 Partition 的 Leader 所在的 Broker
= 该 Group 的 GroupCoordinator

消费者启动时,先向任意 Broker 发 FindCoordinator 请求,找到协调者后再发 JoinGroup


Kafka 为什么不支持读写分离(面试高频)

Redis、MySQL 都支持从节点读,Kafka 的 Follower 为什么只同步不提供读?

原因一:强一致性要求

复制代码
Producer 写入 Leader(offset=100)
Follower 还在同步中(offset=98)

如果此时消费者从 Follower 读,会读到"还不存在"的消息
或者读到的消息后来因 Leader 宕机被截断 → 数据不一致

原因二:消息队列的消费模式不需要读写分离

  • 数据库需要读写分离是因为读远多于写(OLTP 场景)
  • Kafka 的消费者直接并行消费不同 Partition,每个 Partition 只有一个 Leader
  • 增加并发只需增加 Partition 数量,水平扩展天然支持,无需读写分离

原因三:Partition 分散在不同 Broker,已经是分布式读

复制代码
Topic: order(3个Partition,3个Broker)
  Partition 0 Leader → Broker1  ← Consumer1 读
  Partition 1 Leader → Broker2  ← Consumer2 读
  Partition 2 Leader → Broker3  ← Consumer3 读

不同消费者读不同 Broker,已经实现了负载分散,不需要额外的读写分离。

面试答法:Kafka 不支持读写分离,因为 Follower 与 Leader 存在同步延迟,从 Follower 读会造成数据不一致。且 Kafka 通过多 Partition + 多 Broker 已天然实现了并行读,消费端只需增加 Partition 数就能水平扩展,不需要读写分离来提升读性能。


三、生产者原理

消息发送流程

复制代码
业务代码调用 send()
      │
      ▼
  拦截器(可选)
      │
      ▼
  序列化(key/value → bytes)
      │
      ▼
  分区器(决定写哪个 Partition)
      │
      ▼
  RecordAccumulator(消息缓冲区,按 Partition 分批)
      │          ← 达到 batch.size 或 linger.ms 触发
      ▼
  Sender 线程 → 网络发送 → Broker

分区策略

go 复制代码
// 1. 指定 Partition:直接写入目标分区
producer.send(new ProducerRecord("order", 0, key, value))

// 2. 指定 Key:对 key 做 hash % partitionCount,相同 key 一定进同一分区
producer.send(new ProducerRecord("order", "user_123", value))

// 3. 不指定 Key:轮询(Round-Robin)各分区,均匀分布
producer.send(new ProducerRecord("order", value))

面试常问 :如何保证同一用户的消息有序?

答:用用户 ID 作为 Key,相同 Key 的消息落同一 Partition,Partition 内有序。


ACK 机制(消息可靠性)

acks 参数控制 Producer 收到 Broker 确认的级别:

acks 含义 可靠性 性能
0 不等待任何确认,发完即走 最低,可能丢消息 最高
1 Leader 写入本地日志后确认 中,Leader 宕机可能丢消息
-1/all ISR 中所有副本都写入后确认 最高,不丢消息 最低

生产环境通常用 acks=-1 + min.insync.replicas=2(ISR 至少2个副本才确认)。


幂等生产者与事务

幂等生产者enable.idempotence=true):

  • Producer 携带 ProducerIDSequenceNumber
  • Broker 检测到重复的序列号,直接丢弃,不重复写入
  • 解决:网络重试导致的消息重复写入问题
  • 注意:只保证单 Partition 内的幂等

事务(跨 Partition / 跨 Topic 的原子写入):

复制代码
begin transaction
  写入 Partition A
  写入 Partition B
  提交 consumer offset
commit / abort

用于"精确一次(Exactly Once)"语义,性能开销较大。


四、消费者原理

拉取模式(Pull)

Kafka 消费者主动拉取消息,而非 Broker 推送:

go 复制代码
for {
    records := consumer.Poll(timeout)  // 主动拉取
    for _, record := range records {
        process(record)
    }
    consumer.CommitSync()  // 提交 offset
}

Pull 的优势:消费者按自身处理能力拉取,不会被压垮;支持批量拉取,提高吞吐。


Offset 提交方式

方式 说明 风险
自动提交(enable.auto.commit=true 定时自动提交,简单 可能丢消息(处理中崩溃,但 offset 已提交)
手动同步提交 CommitSync() 处理完成后手动提交 可能重复消费(处理完但提交前崩溃)
手动异步提交 CommitAsync() 异步提交,不阻塞 重试顺序问题,需配合回调处理

生产最佳实践:手动提交 + 业务幂等处理,接受"至少一次"语义。


Rebalance(重平衡)

触发条件

  • 消费者加入/离开 Consumer Group
  • Topic 的 Partition 数量变化
  • 消费者心跳超时(session.timeout.ms,默认45s)

Rebalance 过程

复制代码
1. 所有消费者停止消费(Stop The World)
2. 向 GroupCoordinator 发送 JoinGroup 请求
3. Leader 消费者制定分配方案
4. 所有消费者执行新的分配,从新 Partition 继续消费

Rebalance 的问题 :Rebalance 期间所有消费者停止消费,影响吞吐量。

减少 Rebalance 的方法

  • 调大 session.timeout.msheartbeat.interval.ms
  • 调大 max.poll.interval.ms(处理时间上限)
  • 使用 静态成员group.instance.id),重启后沿用原分配,避免触发 Rebalance

五、存储机制(高性能的基础)

日志文件结构

每个 Partition 对应磁盘上的一个目录,内部按**日志段(LogSegment)**存储:

复制代码
/data/kafka/order-0/          ← Partition 目录
  00000000000000000000.log    ← 消息数据文件
  00000000000000000000.index  ← 稀疏索引(offset → 文件位置)
  00000000000000000000.timeindex ← 时间索引
  00000000000001234567.log    ← 新的日志段(达到1GB后滚动)
  00000000000001234567.index

文件名 = 该 Segment 第一条消息的 Offset


消息查找过程

查找 offset=1234568 的消息:

复制代码
1. 二分查找文件列表:1234568 >= 1234567 → 找到对应 Segment
2. 查 .index 文件:找到最近的索引项(稀疏索引,不是每条都记录)
3. 从索引指向的位置开始,顺序扫描 .log 文件,找到目标消息

四大高性能核心原理

1. 顺序写磁盘
复制代码
随机写磁盘:~100 次/秒(磁头寻道时间)
顺序写磁盘:~600 MB/s(接近内存速度)

Kafka 的 .log 文件只追加写(Append Only),永不修改,充分利用顺序 I/O。

2. 页缓存(Page Cache)

Kafka 写消息时,先写操作系统的页缓存(内存),由 OS 异步刷盘:

复制代码
Producer → Page Cache(内存) → (异步)磁盘
Consumer ← Page Cache(命中则不读磁盘)

热数据直接从内存读取,冷数据才读磁盘,大幅降低 I/O。

3. 零拷贝(Zero Copy)

传统文件传输:

复制代码
磁盘 → 内核缓冲区 → 用户空间 → Socket缓冲区 → 网卡(4次拷贝)

Kafka 使用 sendfile 系统调用:

复制代码
磁盘 → 内核缓冲区 → 网卡(2次拷贝,跳过用户空间)

减少 CPU 拷贝次数和上下文切换,消费者拉取消息时性能提升显著。

4. 批量压缩

Producer 将多条消息打包成一个 RecordBatch 发送:

  • 减少网络请求次数
  • 支持 GZIP / Snappy / LZ4 / ZSTD 压缩,减少网络和存储开销

六、高可用机制

Leader 选举

Leader 宕机时,Controller 从 ISR 列表中选第一个 Follower 为新 Leader:

复制代码
ISR: [Broker1(Leader), Broker2, Broker3]
Broker1 宕机 →  新 ISR: [Broker2, Broker3]
               → Broker2 成为新 Leader

极端情况:ISR 全部宕机,只剩非 ISR 副本。此时:

  • unclean.leader.election.enable=false(默认):等待 ISR 副本恢复,保一致性,牺牲可用性
  • unclean.leader.election.enable=true:允许非 ISR 副本当 Leader,保可用性,可能丢消息

消息保留策略

Kafka 消息不会消费完就删除,按策略保留:

yaml 复制代码
# 按时间(默认7天)
log.retention.hours=168

# 按大小
log.retention.bytes=1073741824  # 1GB

# 按 Offset(compact 模式,只保留每个 key 最新的一条)
log.cleanup.policy=compact

七、三大消息语义

语义 含义 如何实现
At Most Once(至多一次) 可能丢消息,不重复 acks=0,自动提交 offset
At Least Once(至少一次) 不丢消息,可能重复 acks=-1,手动提交,业务幂等
Exactly Once(精确一次) 不丢不重 幂等Producer + 事务,性能最低

生产环境通常选 At Least Once + 业务幂等,性价比最高。


八、生产环境常见问题

问题一:消息丢失

复制代码
丢失场景:
  Producer 侧:acks=0/1,网络故障或 Leader 宕机
  Broker 侧:  页缓存未刷盘时宕机(同步刷盘 flush.ms=0 可避免但性能差)
  Consumer 侧:自动提交 offset 后,消息处理失败

解决方案:
  Producer:acks=-1 + min.insync.replicas=2 + 重试
  Broker:  replication.factor≥3,副本跨机架部署
  Consumer:手动提交 offset,处理成功再提交

问题二:消息重复消费

复制代码
原因:
  消费者处理完消息,提交 offset 前崩溃
  重启后从上次 offset 重新消费 → 消息被重复处理

解决方案(幂等处理):
  方案1:数据库唯一键(insert ignore / on conflict do nothing)
  方案2:Redis SET NX,记录已处理的消息 ID
  方案3:业务状态判断(如订单已是"已支付"状态,不再处理支付消息)

问题三:消息顺序性

复制代码
问题:Kafka 只保证单 Partition 内有序,多 Partition 无序

场景:用户操作日志,必须按顺序处理(注册→登录→下单)

解决方案:
  1. 按业务 key(如 user_id)分区,同一用户消息进同一 Partition
  2. 该 Partition 只分配一个消费者(Consumer Group 内)
  3. 消费者单线程处理该 Partition 的消息

注意:多线程消费同一 Partition 会破坏顺序,需要串行处理或按 key 投递线程池

问题四:消息积压

复制代码
原因:消费速度 << 生产速度

排查步骤:
  1. 看 Lag(积压量)= 最新 Offset - 消费者已提交 Offset
  2. 判断是消费慢还是生产突然变多

处理方案:
  临时:增加消费者实例数(不能超过 Partition 数)
       或临时扩 Partition 数(注意:扩后 key 的 hash 分布会变化)
  根本:优化消费者处理逻辑(批量处理、异步写库等)
  紧急:将积压消息转移到容量更大的临时 Topic,扩容消费

问题五:重复消费 vs 漏消费(提交时机)

go 复制代码
// 危险:先提交再处理 → 处理失败,消息漏消费
consumer.CommitSync()
process(record)   // 如果这里 panic,消息丢了

// 危险:自动提交 → 提交时业务可能还未处理完
enable.auto.commit=true

// 正确:先处理,处理成功后再提交
process(record)          // 业务处理
consumer.CommitSync()    // 确认提交
// 崩溃重启后会重复消费,需要业务幂等兜底

九、Go 中使用 Kafka(sarama / kafka-go)

生产者示例(sarama)

go 复制代码
import "github.com/IBM/sarama"

config := sarama.NewConfig()
config.Producer.RequiredAcks = sarama.WaitForAll    // acks=-1
config.Producer.Idempotent = true                    // 幂等
config.Producer.Return.Successes = true
config.Net.MaxOpenRequests = 1                       // 幂等模式必须为1

producer, err := sarama.NewSyncProducer(brokers, config)
defer producer.Close()

msg := &sarama.ProducerMessage{
    Topic: "order",
    Key:   sarama.StringEncoder(userID),   // 按 userID 分区
    Value: sarama.StringEncoder(payload),
}
partition, offset, err := producer.SendMessage(msg)

消费者示例(sarama ConsumerGroup)

go 复制代码
type Handler struct{}

func (h *Handler) Setup(_ sarama.ConsumerGroupSession) error   { return nil }
func (h *Handler) Cleanup(_ sarama.ConsumerGroupSession) error { return nil }
func (h *Handler) ConsumeClaim(session sarama.ConsumerGroupSession,
    claim sarama.ConsumerGroupClaim) error {

    for msg := range claim.Messages() {
        if err := process(msg); err != nil {
            // 处理失败:记录日志,不提交 offset,等待重试
            log.Printf("process failed: %v", err)
            continue
        }
        // 处理成功后标记提交(异步批量提交)
        session.MarkMessage(msg, "")
    }
    return nil
}

client, _ := sarama.NewConsumerGroup(brokers, "order-group", config)
client.Consume(ctx, []string{"order"}, &Handler{})

十、面试高频问题速答

Q:Kafka 为什么高性能?

四个原因:① 顺序写磁盘,避免随机 I/O;② 页缓存,读热数据不走磁盘;③ 零拷贝(sendfile),减少数据拷贝次数;④ 批量发送与压缩,减少网络开销。

Q:Kafka 如何保证消息不丢失?

三端配合:Producer 设置 acks=-1 + 重试;Broker 设置副本数 ≥ 3,min.insync.replicas=2;Consumer 手动提交 Offset,处理成功后再提交。

Q:Kafka 和 RabbitMQ 怎么选?

高吞吐、需要消息回溯、日志/埋点场景选 Kafka;业务解耦、复杂路由、低延迟任务队列选 RabbitMQ。

Q:Partition 数量怎么设置?

根据目标吞吐量 / 单 Partition 吞吐量得出,并发消费数不超过 Partition 数。不建议太多:Partition 过多会增加 Leader 选举和元数据管理压力。

Q:Consumer 数量超过 Partition 数会怎样?

多余的 Consumer 不会被分配任何 Partition,处于空闲状态,造成资源浪费。

Q:Rebalance 期间会发生什么,如何避免频繁 Rebalance?

Rebalance 期间所有消费者停止消费(Stop The World),可能造成消息积压。避免方法:调大心跳和 session 超时参数、调大单次 poll 处理时间上限、使用静态成员 ID。

Q:如何实现消息顺序消费?

同一业务 key 的消息通过 hash 分区写入同一 Partition,Consumer Group 中该 Partition 只分配一个消费者单线程处理。

Q:Kafka 消息积压怎么处理?

短期:扩容消费者实例(上限是 Partition 数量);中期:优化消费逻辑(批量处理、异步落库);紧急:建临时 Topic + 多 Partition,把积压消息迁移过去后扩消费者消费。

Q:什么是 ISR,它的作用是什么?

ISR 是与 Leader 保持同步的副本集合,只有 ISR 中的副本才能参与 Leader 选举。通过控制 min.insync.replicas,保证消息写入足够多的副本后才确认,防止 Leader 宕机数据丢失。

Q:Exactly Once 是怎么实现的?

两层保障:幂等 Producer(PID + SequenceNumber 去重单 Partition 内重复写入)+ 事务(跨 Partition/Topic 的原子提交),配合 Consumer 的 read_committed 隔离级别,实现端到端精确一次。


十一、一张图记住核心

复制代码
                    ┌────────────────────────────────────┐
高性能来自:          │  顺序写 + 页缓存 + 零拷贝 + 批量    │
                    └────────────────────────────────────┘

                    ┌────────────────────────────────────┐
高可用来自:          │  多副本(ISR)+ Leader 自动选举      │
                    └────────────────────────────────────┘

                    ┌────────────────────────────────────┐
消息可靠来自:        │  acks=-1 + 手动提交 + 业务幂等       │
                    └────────────────────────────────────┘

                    ┌────────────────────────────────────┐
高并发消费来自:       │  Partition 并行 + Consumer Group   │
                    └────────────────────────────────────┘

总结:面试 Kafka 的核心考点是"为什么快"(存储机制)、"怎么保证可靠"(ACK + 副本 + 手动提交)、"怎么处理常见问题"(丢失/重复/顺序/积压)。把这四个方向讲清楚,Kafka 面试基本稳了。

相关推荐
iPadiPhone2 小时前
性能优化的“快车道”:Spring @Async 注解深度原理与大厂实战
java·后端·spring·面试·性能优化
future02102 小时前
Kafka分区策略:高频问题全解析
分布式·kafka
ErizJ2 小时前
面试 | Docker K8S
docker·面试·kubernetes
Volunteer Technology2 小时前
RabbitMQ面试场景题归纳
分布式·面试·rabbitmq
ErizJ2 小时前
面试 | Go八股
面试·golang
luckyzlb2 小时前
01-kafka
分布式·中间件·kafka
XPii3 小时前
FPGA工程师面试资料【1】
fpga开发·面试·职场和发展
研究点啥好呢3 小时前
百度 人工智能工程师面试题精选
人工智能·pytorch·神经网络·百度·ai·面试·文心一言
雨夜之寂11 小时前
Browser Use + DeepSeek,我踩了哪些坑
后端·面试