vault backup: 2026-05-24 20:51:06
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
---
|
||||
tags: [MQ, Kafka, 消息队列, 分布式系统]
|
||||
create time: 2026-05-24 19:52
|
||||
---
|
||||
|
||||
# Kafka
|
||||
|
||||
## 概述
|
||||
|
||||
Kafka 是 LinkedIn 开源的分布式流处理平台,以极高的吞吐量和水平扩展能力著称。它采用分区日志(Partition Log)作为核心存储模型,配合副本机制和消费者组协议,成为大数据和事件驱动架构的事实标准。
|
||||
|
||||
## 正文
|
||||
|
||||
### 1. 架构总览
|
||||
|
||||
Kafka 集群由多个 Broker 组成,消息按 Topic 逻辑分类,每个 Topic 被划分为若干 Partition 分散在不同 Broker 上。每个 Partition 有多个 Replica(副本),其中一个为 Leader 负责读写,其余 Follower 通过拉取同步数据。
|
||||
|
||||
早期版本依赖 ZooKeeper 管理元数据(Broker 注册、Topic 配置、Leader 选举等),从 Kafka 3.3 开始正式推出 KRaft 模式,用内置的 Raft 共识协议取代 ZooKeeper,简化了运维和启动流程。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Cluster["Kafka 集群"]
|
||||
B1["Broker 1<br/>Leader P0, Follower P1"]
|
||||
B2["Broker 2<br/>Leader P1, Follower P2"]
|
||||
B3["Broker 3<br/>Leader P2, Follower P0"]
|
||||
end
|
||||
|
||||
Producer["Producer"] --> B1
|
||||
Producer --> B2
|
||||
Producer --> B3
|
||||
B1 --> ConsumerGroup["Consumer Group"]
|
||||
B2 --> ConsumerGroup
|
||||
B3 --> ConsumerGroup
|
||||
|
||||
B1 <-.-> B2
|
||||
B2 <-.-> B3
|
||||
B3 <-.-> B1
|
||||
|
||||
Controller["KRaft Controller<br/>元数据管理"] --> B1
|
||||
Controller --> B2
|
||||
Controller --> B3
|
||||
|
||||
style Cluster fill:#f9f9f9,color:#333
|
||||
style Controller fill:#4A90D9,color:#fff
|
||||
```
|
||||
|
||||
### 2. 高吞吐原理
|
||||
|
||||
Kafka 单集群可轻松达到百万级 TPS,核心秘诀在于:
|
||||
|
||||
- **顺序写磁盘**:Partition 是 append-only 日志,写入永远追加到文件末尾,避免了随机 I/O。顺序写磁盘的吞吐甚至可以媲美内存随机写(600MB/s vs 200MB/s 这种量级)。
|
||||
- **零拷贝(sendfile)**:Consumer 拉取消息时,Kafka 直接通过 Linux `sendfile` 系统调用将 Page Cache 中的数据发送到网卡,绕过用户态缓冲区,减少两次上下文切换和数据拷贝。
|
||||
- **批量发送**:Producer 端将多条消息攒成一个批次(batch)再发送,减少网络 RTT。`linger.ms` 和 `batch.size` 控制攒批策略。
|
||||
- **消息压缩**:支持 Snappy、LZ4、Zstd 等压缩算法,在 Producer 端压缩、Consumer 端解压,显著减少网络带宽和磁盘占用。
|
||||
- **分区并行**:多个 Partition 可以被不同的 Consumer 并行消费,也可以被不同的 Producer 并行写入,天然支持水平扩展。
|
||||
|
||||
> [!question] 思考:零拷贝要求数据不能被修改,那 Kafka 对消息做压缩怎么办?
|
||||
> 压缩发生在 Producer 端,写入 Partition 的数据已经是压缩后的字节流。Broker 存储和传输时保持原样,解压由 Consumer 端完成。所以零拷贝和压缩并不冲突——Broker 不需要解压就能直接 sendfile。
|
||||
|
||||
### 3. 副本机制
|
||||
|
||||
Kafka 的数据可靠性靠副本机制保证。每个 Partition 有多个 Replica,分布在不同 Broker 上:
|
||||
|
||||
- **Leader**:处理所有读写请求。
|
||||
- **Follower**:被动拉取 Leader 的数据,保持同步。
|
||||
- **ISR(In-Sync Replicas)**:与 Leader 保持同步的副本集合。如果 Follower 落后太多(由 `replica.lag.time.max.ms` 控制),会被踢出 ISR。
|
||||
- **LEO(Log End Offset)**:每个副本最后一条消息的下一个偏移量。
|
||||
- **HW(High Watermark)**:ISR 中最小的 LEO。Consumer 只能读到 HW 之前的消息,保证读到的数据在所有 ISR 副本上都存在。
|
||||
|
||||
Leader 崩溃时,Controller 从 ISR 中选举新 Leader。如果 `unclean.leader.election.enable=true`,允许从非 ISR 副本中选举(可能丢数据),生产环境应设为 `false`。
|
||||
|
||||
> [!question] 为什么 HW 要取 ISR 中最小的 LEO,而不是最大或平均值?
|
||||
> 因为 HW 的意义是"所有同步副本都确认收到的位置"。取最小值才能保证:即使 Leader 挂了,任何一个 ISR 副本接替后,Consumer 已读到的数据都不会丢失。
|
||||
|
||||
### 4. Producer 分区策略
|
||||
|
||||
Producer 发送消息时需要决定发往哪个 Partition:
|
||||
|
||||
| 策略 | 说明 |
|
||||
|------|------|
|
||||
| **轮询(Round-Robin)** | 默认策略,消息均匀分配到各 Partition,保证最大吞吐 |
|
||||
| **Key 哈希** | 指定 Key 时,对 Key 做 hash 取模确定 Partition。相同 Key 的消息总是发到同一 Partition,保证局部有序 |
|
||||
| **自定义** | 实现 `Partitioner` 接口,按业务逻辑路由(如按用户 ID 分区) |
|
||||
|
||||
**acks 参数**控制 Producer 的可靠性级别:
|
||||
|
||||
| acks | 含义 | 适用场景 |
|
||||
|------|------|----------|
|
||||
| `0` | 不等确认,发完即忘 | 日志采集等允许丢失的场景 |
|
||||
| `1` | Leader 写入成功即返回 | 大多数业务场景,平衡性能与可靠性 |
|
||||
| `all` | 所有 ISR 副本写入成功才返回 | 金融级可靠性要求 |
|
||||
|
||||
> [!question] 思考题:Kafka 的 acks=all + min.insync.replicas=2 配置为什么能兼顾性能和可靠性?
|
||||
>
|
||||
> **提示**:想想如果只有 1 个副本(min.insync.replicas=1),acks=all 等于 acks=1,可靠性没有实质提升。设为 2 意味着至少有两个副本确认,即使 Leader 宕机,数据也不会丢。而 ISR 副本通常是同步延迟极低的(毫秒级),所以对性能影响有限。这个组合的精髓在于:acks=all 保证写入被多个副本确认,min.insync.replicas 保证 ISR 中至少有足够副本,两者配合才真正实现了"不丢数据"。
|
||||
|
||||
### 5. Consumer 与 Consumer Group
|
||||
|
||||
Consumer Group 是 Kafka 实现"发布-订阅"和"队列"两种语义的核心机制:
|
||||
|
||||
- 同一 Group 内的 Consumer 分担消费(类似队列模式)。
|
||||
- 不同 Group 各自独立消费全部消息(类似发布-订阅模式)。
|
||||
|
||||
**Rebalance**:当 Consumer 加入/离开 Group,或 Topic 分区数变化时,触发分区重新分配。Kafka 引入了 **Cooperative Rebalance**(渐进式再平衡),避免 Stop-the-World 式的全局暂停。
|
||||
|
||||
**Offset 管理**:
|
||||
- **自动提交**:`enable.auto.commit=true`,后台定期提交 Offset。简单但可能丢消息(提交后还没处理完就崩溃)或重复消费。
|
||||
- **手动提交**:消费完成后显式调用 `commitSync()` 或 `commitAsync()`。更可靠,是生产环境推荐做法。Offset 存储在 `__consumer_offsets` 内部 Topic 中。
|
||||
|
||||
```go
|
||||
// 使用 confluent-kafka-go 创建 Producer
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/confluentinc/confluent-kafka-go/v2/kafka"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 创建 Producer 实例,指定 Broker 地址
|
||||
p, err := kafka.NewProducer(&kafka.ConfigMap{
|
||||
"bootstrap.servers": "localhost:9092",
|
||||
"acks": "all", // 等待所有 ISR 副本确认
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer p.Close()
|
||||
|
||||
topic := "my-topic"
|
||||
// 发送消息
|
||||
err = p.Produce(&kafka.Message{
|
||||
TopicPartition: kafka.TopicPartition{
|
||||
Topic: &topic,
|
||||
Partition: kafka.PartitionAny, // 自动选择分区
|
||||
},
|
||||
Value: []byte("hello kafka"),
|
||||
}, nil)
|
||||
if err != nil {
|
||||
fmt.Printf("produce failed: %v\n", err)
|
||||
}
|
||||
|
||||
// 确保所有消息发送完成
|
||||
p.Flush(5000)
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// 使用 confluent-kafka-go 创建 Consumer
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/confluentinc/confluent-kafka-go/v2/kafka"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 创建 Consumer 实例,加入 Consumer Group
|
||||
c, err := kafka.NewConsumer(&kafka.ConfigMap{
|
||||
"bootstrap.servers": "localhost:9092",
|
||||
"group.id": "my-group",
|
||||
"auto.offset.reset": "earliest", // 从最早消息开始消费
|
||||
"enable.auto.commit": false, // 关闭自动提交,手动控制
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
// 订阅 Topic
|
||||
c.SubscribeTopics([]string{"my-topic"}, nil)
|
||||
|
||||
for {
|
||||
// 轮询拉取消息,超时 100ms
|
||||
msg, err := c.ReadMessage(100e6)
|
||||
if err != nil {
|
||||
continue // 超时或错误,继续轮询
|
||||
}
|
||||
fmt.Printf("received: %s (partition=%d, offset=%d)\n",
|
||||
string(msg.Value), msg.TopicPartition.Partition, msg.TopicPartition.Offset)
|
||||
|
||||
// 处理完成后手动提交 Offset
|
||||
if _, err := c.CommitMessage(msg); err != nil {
|
||||
fmt.Printf("commit failed: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Kafka 生态
|
||||
|
||||
Kafka 不只是一个消息队列,而是一个完整的流处理生态:
|
||||
|
||||
| 组件 | 作用 |
|
||||
|------|------|
|
||||
| **Kafka Connect** | 数据集成框架,通过 Source/Sink Connector 在 Kafka 与外部系统(MySQL、Elasticsearch、S3 等)之间搬运数据,无需写代码 |
|
||||
| **Kafka Streams** | 客户端流处理库,直接嵌入应用中,支持窗口、Join、聚合等操作,无需独立的流处理集群 |
|
||||
| **Schema Registry** | 管理消息 Schema(Avro/Protobuf/JSON Schema),保证生产者和消费者的 Schema 兼容性,防止数据格式不匹配 |
|
||||
| **ksqlDB** | 用 SQL 语法实时查询和处理 Kafka 流数据,降低流处理门槛 |
|
||||
|
||||
### 7. KRaft 模式
|
||||
|
||||
Kafka 早期依赖 ZooKeeper 做元数据管理和 Leader 选举,但 ZooKeeper 带来了额外的运维负担(独立集群、脑裂风险、元数据瓶颈)。KRaft(Kafka Raft)模式将元数据管理内置到 Kafka 自身:
|
||||
|
||||
- Controller 节点通过 Raft 协议选举,维护元数据日志。
|
||||
- Broker 从 Controller 拉取元数据,不再依赖 ZooKeeper。
|
||||
- 启动更快、集群规模上限更高(支持百万级 Partition)、运维更简单。
|
||||
- Kafka 3.3 起 KRaft 进入生产就绪状态,4.0 版本已完全移除 ZooKeeper 支持。
|
||||
|
||||
> [!question] KRaft 用 Raft 管理元数据,那 Kafka 本身的数据副本同步为什么不用 Raft?
|
||||
> Kafka 的数据副本同步是 Leader-Follower 模型 + ISR 机制,本质是"主从异步复制 + 可配置同步确认"。Raft 更适合小数据量的元数据共识,而 Kafka 的 Partition 日志数据量巨大,ISR 模型通过 HW 机制实现了类似 Raft 的"多数派确认"语义,同时保持了高吞吐的顺序写优势。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[01-基础概念/1-MQ-基础概念|MQ 基础概念]]
|
||||
- [[02-消息模型/3-MQ-消息模型|MQ 消息模型]]
|
||||
- [[04-存储引擎/8-MQ-存储引擎设计|MQ 存储引擎设计]]
|
||||
- [[05-可靠性保障/12-MQ-消息确认与持久化|MQ 消息确认与持久化]]
|
||||
- [[05-可靠性保障/15-MQ-顺序性保障|MQ 顺序性保障]]
|
||||
- [[06-高级特性/21-MQ-消息压缩与批处理|MQ 消息压缩与批处理]]
|
||||
- [[09-流处理与事件驱动/33-MQ-与流处理|MQ 与流处理]]
|
||||
- [[12-架构与实战/45-MQ-跨集群复制与容灾|MQ 跨集群复制与容灾]]
|
||||
- [[07-主流MQ对比/27-MQ-选型对比|MQ 选型对比]]
|
||||
Reference in New Issue
Block a user