一、先说核心结论
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 写一条:
-
复制给 Follower
-
阻塞等 ACK
-
收到多数派 ACK
-
提交
-
返回 Client
每条都要等
Kafka 的"半同步"做法
Leader 写一批:
-
写到本地 log(Page Cache)
-
异步复制给 Follower
-
等多数派 Follower ACK(**不等全部**)
-
收到 ACK 后提交
-
返回 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 是用延迟和复杂度换吞吐的工业级典范。