Kafka acks=all 为什么比 Raft 快

一、先说核心结论

Kafka acks=all 不等于 Raft ------Kafka 用批处理 + 顺序 IO + 异步刷盘 + Page Cache 四个"作弊手段"把"多数派同步"的成本压到了极限。Raft 还在"一条一条来" ,Kafka 已经"攒一批再发"------这就是 10 倍性能差异的来源

二、先把对比拉直

维度 Raft(etcd) Kafka acks=all
核心写入单位 单条日志 批次(Batch)
网络传输 一次一条 一次一批
磁盘写入 实时刷盘(fsync) 顺序写 + 异步刷盘
节点通信 同步等待 ACK 异步 ACK + 流水线
典型延迟 5-10ms 5-20ms(一次写入)
吞吐量 1-3 万/s 10-30 万/s

⚠️ 注意 :单次延迟 Kafka 并不一定比 Raft 短(甚至更长),但吞吐量是 10-30 倍 ------Kafka 是用"延迟换吞吐"

三、5 个核心优化(重点)

优化 1:批处理(Batching)------最大的作弊
Raft 的"笨"做法

写 100 条数据:

提议 #1 → 复制 → ACK → 提交

提议 #2 → 复制 → ACK → 提交

...

提议 #100 → 复制 → ACK → 提交

网络往返 = 200 次(每条 2 次)

Kafka 的"聪明"做法

写 100 条数据:

攒成 1 个 batch(按时间或大小)

一次发送 → 一次复制 → 一次 ACK

网络往返 = 2 次

生产端配置

默认配置

linger.ms=5 # 攒 5ms 再发

batch.size=16384 # 攒 16KB 再发

compression.type=lz4 # 压缩传输

实际效果:100 条消息压成 1 个 16KB batch

Broker 端同步

// Kafka ProducerRecord 攒批发送

RecordAccumulator append(ProducerRecord<K, V> record) {

// 1. 按 topic-partition 分组

// 2. 每个 partition 一个 buffer

// 3. 攒到 batch.size 或 linger.ms 触发发送

if (buffer.remaining() < record.size()) {

// buffer 满了,触发发送

sender.send(buffer);

}

}

效果

  • 单条延迟 5ms(linger.ms 等待)
  • 但吞吐能从 1 万/s 提升到 30 万/s
  • 延迟换吞吐
优化 2:顺序磁盘 IO + Page Cache
Raft 的"机械做法"

每条日志立即 fsync(强制刷盘)

→ 随机写(不同 LogEntry 写不同位置)

→ 每次都是磁盘 IO

→ 1 万 IOPS 就到极限

Kafka 的"作弊"做法

所有消息追加到日志文件尾部(顺序写)

先写 Page Cache(内存,纳秒级)

后台异步 fsync(默认 5s 一次)

真正的磁盘 IO 每 5s 才发生一次

关键代码

// Kafka Log.append (简化)

public void append(RecordBatch batch) {

// 1. 顺序追加到 file channel

fileChannel.position(fileChannel.size()); // 跳到末尾

fileChannel.write(batch.buffer()); // 顺序写

// 2. 更新 LEO(Log End Offset)

updateLEO();

// 3. 不等 fsync 直接返回

// fsync 由 LogFlusher 线程异步处理

}

关键配置

Kafka 的"作弊"配置

log.flush.interval.messages=10000 # 攒 1w 条才刷盘

log.flush.interval.ms=5000 # 攒 5 秒才刷盘

log.segment.bytes=1073741824 # 1GB 一个 segment

效果

  • Page Cache 命中 = 内存访问(纳秒级)
  • 真磁盘 IO 每 5s 一次
  • 磁盘不是瓶颈
优化 3:零拷贝(Zero-Copy)------ Kafka 独门绝技
Raft 的"笨"做法

客户端读数据:

磁盘 → 内核缓冲区 → 用户缓冲区 → Socket 缓冲区 → 网卡

4 次拷贝,2 次系统调用

Kafka 的"独门优化"

客户端读数据:

磁盘 → 内核缓冲区 → 网卡

2 次拷贝,0 次系统调用(sendfile)

关键代码

// Kafka FileRecords.readInto

public long readInto(ByteBuffer buf, int position, int length) {

// 不用 sendfile 的传统实现:

// 1. 磁盘读到内核 buffer

// 2. 拷贝到用户 buffer

// 3. 拷贝到 socket buffer

// 4. 发送到网卡

// Kafka 的优化(FileChannel.transferTo):

return fileChannel.transferTo(position, length, socketChannel);

// 1. 磁盘读到内核 buffer

// 2. 直接从内核 buffer 发送到网卡

}

效果

  • 网络传输 4 倍提升
  • CPU 占用降低 60%
  • Kafka 独门,Raft 没做这个
优化 4:异步复制(ack=all 不等于强同步)
Raft 的"同步做法"

Leader 写一条:

  1. 复制给 Follower

  2. 阻塞等 ACK

  3. 收到多数派 ACK

  4. 提交

  5. 返回 Client

每条都要等

Kafka 的"半同步"做法

Leader 写一批:

  1. 写到本地 log(Page Cache)

  2. 异步复制给 Follower

  3. 等多数派 Follower ACK(**不等全部**)

  4. 收到 ACK 后提交

  5. 返回 Client

关键:不等所有 Follower,只等 ISR 多数派

异步的"作弊"

// Kafka ReplicaFetcherThread (简化)

public void fetchFromLeader() {

// 异步线程一直拉,不需要 Leader 等

while (running) {

FetchRequest req = new FetchRequest(offset);

FetchResponse resp = leader.fetch(req);

// 写到本地 log

localLog.append(resp.records);

// 通知 Leader:我的 offset 追到这了

leader.updateFollowerOffset(partition, myOffset);

}

}

效果

  • Leader 不用等最慢的 Follower
  • Follower 慢 = 不影响 Leader 写入延迟
  • 磁盘 I/O 和网络 I/O 流水线化
优化 5:分区并行(Partition Parallelism)
Raft 的"串行"问题

一个集群的所有写入都走同一个 Leader

→ Leader 是瓶颈

→ 1 个 Leader 处理 1 万/s 就到顶

Kafka 的"天然并行"

Topic 有 32 个 Partition:

  • 32 个 Leader(不同 broker)

  • 32 倍并行

  • 32 万/s 不是梦

关键理解

  • 每个 Partition 是独立的 Raft-like 协议
  • 多个 Partition 并行 = 多个 Raft 实例并行
  • 这是 Kafka 能做到百万级吞吐的根本原因

配置

决定并行度

num.partitions=32 # topic 分区数

单 partition 10万/s,32 partition = 320万/s

四、量化对比(性能数字)

场景 Raft(etcd) Kafka acks=all 提升
单条写入延迟 5-10ms 5-20ms 接近
吞吐量(单实例) 1-3 万/s 10-30 万/s 5-10 倍
吞吐量(多 Partition) 1-3 万/s 100-300 万/s 100 倍
磁盘 IO fsync 立即 5s 一次 降低 99%
网络拷贝次数 4 次 2 次 降低 50%
CPU 占用 降低 60%

五、Kafka 的"妥协"------丢了什么

性能提升不是免费的,Kafka 牺牲了三件事:

1. 强一致性的"严格性"降低

Raft 的强一致:

写入返回 = 多数派确认 = 数据绝对安全

Kafka 的强一致:

acks=all = ISR 多数派确认

但 ISR 是"动态"的(可能缩小)

→ 极端情况可能丢数据

金融场景应对

acks=all

enable.idempotence=true # 幂等

min.insync.replicas=2 # 至少 2 个 ISR

replication.factor=3

unclean.leader.election.enable=false # 防脑裂

2. 延迟的"最坏情况"变差

Raft 的延迟:

稳定 5-10ms(一次写入)

Kafka 的延迟:

最佳 5ms(batch 满了立刻发)

最差 5ms + linger.ms = 10ms+

→ 等待攒批会有"延迟毛刺"

3. 实现复杂度极高

Raft 协议:

  • 几行代码

  • 论文清晰

  • 易于理解

Kafka 实现:

  • 几十万行代码

  • Producer / Broker / Consumer / Coordinator

  • 需要理解 Page Cache、Sendfile、Batch、ISR、HW/LEO

→ 学习曲线极陡

六、面试话术

"Kafka acks=all 比 Raft 快靠四个工程作弊

1. 批处理 :Raft 一次写一条(200 次网络往返),Kafka 攒批发 (2 次往返)------网络成本降低 100 倍

2. Page Cache :Raft 每条立即 fsync,Kafka 先写内存、5 秒一次刷盘 ------磁盘 IO 降低 99%

3. 零拷贝 :Raft 4 次数据拷贝,Kafka 用 sendfile 2 次拷贝 ------CPU 降低 60%

4. 异步复制 + 多 Partition :Raft 单 Leader 串行,Kafka 多 Partition 并行 + Follower 异步 ------吞吐量提升 100 倍

代价 :延迟更差(攒批等待)、实现复杂、强一致性略弱。Kafka 是用延迟和复杂度换吞吐 ------适合日志/流计算,不适合配置中心。"

七、和 etcd 的真实场景对比

场景 etcd Raft Kafka acks=all
写入频率 低(配置变更) 极高(日志/事件)
延迟敏感 极高(K8s 调度) 中(批处理可接受)
吞吐要求 低(千级) 极高(百万级)
数据大小 小(几 KB) 大(批量几 MB)
典型应用 K8s 元数据 日志、事件、消息

所以 etcd 和 Kafka 是互补的,不是竞争的 ------用 etcd 存配置,用 Kafka 传日志

八、Kafka 还有哪些"作弊"手段(你可能不知道)

作弊 6:消息压缩

compression.type=lz4

100 条消息压成 1 个 16KB batch

压缩后可能只有 5KB

网络传输又降低 3 倍

作弊 7:顺序消费 + Offset 复用

Raft 的日志:每条都要广播

Kafka 的 Offset:消费者本地维护,不用每次同步

→ 大幅减少网络流量

作弊 8:日志压缩(Log Compaction)
复制代码
普通日志:所有消息都保留压缩日志:只保留每个 key 的最新 value→ 磁盘占用降低 10 倍

九、一句话总结

Kafka acks=all 比 Raft 快的核心是"批处理 + Page Cache + 零拷贝 + 多 Partition 并行"四个工程作弊 ------网络往返降低 100 倍、磁盘 IO 降低 99%、CPU 降低 60%、并行度提升 100 倍代价是延迟更差、实现更复杂、强一致性略弱Kafka 是用延迟和复杂度换吞吐的工业级典范