部署
kafka支持两种启动方式
Kafka with KRaft
Kafka with ZooKeeper(4.0后将会被废弃)
核心API
- Producer API 允许应用程序将数据流发送到Kafka 集群中的主题
- Consumer API 允许应用程序从 Kafka 集群中的主题读取数据流
- Streams API 允许将数据流从输入主题转换到输出主题
- Connect API 允许实现连接器,该连接器不断地从某些源系统或应用程序拉入 Kafka,或者从 Kafka 推送到某些接收系统或应用程序
- Admin API 管理API 允许管理和检查topics,brokers和其他 Kafka 对象
目前只有Java客户端是由官方维护的,其他客户端由社区贡献维护。
常见配置
属性 | 描述 |
---|---|
advertised.listeners | Broker 向外部暴露的网络地址 |
auto.leader.rebalance.enable | 否启用自动的分区 Leader 重新平衡 |
delete.topic.enable | 控制 Kafka 是否允许删除主题 |
log.retention.ms | Kafka 日志保留的时间 |
message.max.bytes | Kafka Broker 能够接收的单条消息的最大字节数 |
log.cleanup.policy | 默认delete,可选范围[compact, delete],compact 压缩,delete删除 |
max.request.size | 限制了生产者发送的单个请求的最大字节数,可以是单个或多个消息 |
buffer.memory | 生产者(Producer)缓冲区的大小 |
batch.size | 用于设置生产者发送消息时,每个批次(Batch)可占用的内存大小(字节) |
linger.ms | 表示消息在生产者缓冲区中的最长滞留时间 |
acks | 0,1,all |
max.in.flight.requests.per.connection | 单个网络连接上,生产者可以同时发送的最大未确认请求数量,控制请求并发量 |
fetch.min.bytes | 消费者期望从Kafka服务器获取的最小数据量(以字节为单位) |
auto.offset.reset | earliest:消费者将从最早的记录开始读取,latest:消费者将从最新的记录开始读取,none:没有可用的偏移量时,消费者将抛出异常。 |
enable.auto.commit | 控制消费者是否自动提交偏移量 |
max.poll.records | 最大拉取数据量 |
设计
高性能设计
-
磁盘顺序读写:在某些情况下,顺序磁盘访问可能比随机内存访问更快!
-
充分利用操作系统的pagecache机制:kafka是运行在JVM上的,而Java内存使用情况存在如下问题:
- 对象的内存开销非常高,通常会使存储的数据大小加倍(或更糟)。
- 随着堆内数据的增加,Java 垃圾收集变得越来越繁琐和缓慢。
因此kafka提供了一种简单的设计:我们不是尽可能多地在内存中保存数据,然后在空间不足时将其全部刷新到文件系统中,而是将其反转。所有数据都会立即写入文件系统上的持久日志中,而不必刷新到磁盘。实际上,这仅意味着它被传输到内核的页面缓存中(pagecache)
-
批量处理(异步发送):批处理可以带来更大的网络数据包、更大的连续磁盘操作、连续的内存块等等,所有这些都允许 Kafka 将突发的随机消息写入流转换为流向消费者的线性写入
-
零拷贝(sendfile):kafka维护了通用的消息格式,使用sendfile系统调用完成从页面缓存(pagecache)直接发送到NIC缓冲区,避免了多次的复制和系统调用
1、传统的数据复制流程
2、sendfile
3、sendfile+DMA scatter/gather
这种 pagecache 和 sendfile 的组合意味着在 Kafka 集群中,当消费者大部分都忙于处理数据时,您将看不到磁盘上的任何读取活动,因为它们将完全从缓存中提供数据。
-
端到端批量压缩: kafka支持批处理数据进行压缩,并将保持压缩状态并保存在日志中,并且还将以压缩形式传输给消费者。消费者将解压缩收到的任何压缩数据。Kafka 支持 GZIP、Snappy、LZ4 和 ZStandard 压缩协议
-
分区:分区是Kafka存储消息的基本单位,也是消息并行处理的基础
负载均衡
生产负载均衡
轮询策略(Round-robin):默认情况下,如果没有指定分区,Kafka 生产者会使用轮询策略,将消息依次发送到所有可用的分区。
键控分区策略(Key-based Partitioning):如果消息带有一个键(key),Kafka 生产者会根据这个键的哈希值将消息映射到一个固定的分区。相同键的消息总是会被发送到同一个分区,保证了消息的顺序性。
自定义分区器(Custom Partitioners):用户可以实现自定义的分区器逻辑,根据业务需求对消息进行分区,以实现更细粒度的负载均衡控制。
消费者端负载均衡
分区再均衡(Rebalancing):当消费者组中的成员数量发生变化(如增加或减少消费者实例)时,Kafka 会触发分区再均衡操作。再均衡过程中,Kafka 会重新分配分区,以确保每个消费者实例获得相对均衡的分区。
再均衡协调器(Rebalance Coordinator):在 Kafka 中,每个消费者组都有一个协调器节点,负责管理组成员列表和分区分配。协调器通过触发再均衡来调整负载均衡状态。
静态成员(Static Membership):Kafka 2.3 及以上版本引入了静态成员概念,允许消费者在重启或故障恢复后继续使用先前分配的分区,而不需要立即触发再均衡。这可以减少不必要的分区移动,提高系统的稳定性。
消息传递语义
At most once / At least once
通过生产者重试,retries参数,消息确认 acks实现
Exactly once
kafka主要通过事务+幂等实现Exactly once语义
- 幂等:从 0.11.0.0 开始,Kafka 生产者支持幂等传递选项。代理为每个生产者分配一个 ID,并使用生产者随每条消息发送的序列号对消息进行重复数据删除。幂等性本身只针对单个分区起作用
- 事物:从 0.11.0.0 开始,生产者支持使用类似事务的语义将消息发送到多个主题分区:即,要么所有消息都成功写入,要么所有消息都未成功写入。
实现精确一次语义的完整过程
- 生产者端:
生产者初始化时,Kafka 分配一个唯一的 Producer ID(PID)。
在事务内,生产者将消息发送到多个分区,并在每个分区中使用幂等性机制(通过序列号)确保单个分区内的消息不会重复。
当所有消息发送完毕后,生产者提交事务,Kafka 会确保所有分区的数据要么全部可见(事务提交),要么全部不可见(事务中止)。
- 消费者端:
消费者配置为 isolation.level=read_committed,确保只消费已提交事务的数据。
这避免了消费者读到未提交或回滚的消息,从而实现精确一次的消费。
复制(Replication)
Kafka 中的每个分区都有一个leader和零个或多个followers。包括leader在内的副本总数构成复制因子。
ISR:kafka维护了一个同步副本集"in sync" replicas(ISR)。在ISR列表的brokers必须满足两个条件
1)保持活跃状态(控制器通过心跳检测),对于kraft集群通过broker.session.timeout.ms控制心跳超时时间,对于zookeeper集群通过zookeeper.session.timeout.ms控制;2)数据不能落后leader太多(通过replica.lag.time.max.ms控制,无法在该配置设置的最大时间内赶上领导者日志末尾的副本将从 ISR 中移除)。 如果不满足这两个条件之一则剔除ISR集合。
kafka leader选举优先会优先选择ISR列表中的副本,通过配置属性unclean.leader.election.enable允许从非ISR列表中选举leader,但这可能会破坏数据的一致性
可用性和持久性的平衡
默认情况下,当 acks=all 时,只要所有当前同步的副本都收到消息,就会立即进行确认。例如,如果某个主题仅配置了两个副本,并且其中一个发生故障(即,仅剩下一个同步副本),则指定 acks=all 的写入将会成功
另外,kafka支持指定最小的ISR大小,仅当 ISR 大小高于某个最小值时,分区才会接受写入,以防止丢失仅写入单个副本(随后不可用)的消息。此设置仅在生产者使用 acks=all 并保证至少这么多同步副本将确认该消息时才生效。 此设置提供了可用性与持久性的平衡,ISR SIZE设置越大一致性越好,但是可用性越低
日志压缩
日志压缩可确保 Kafka 始终在单个主题分区的数据日志中保留每个消息键的至少最后一个已知值。此保留策略可以根据每个主题进行设置,因此单个集群可以有一些按大小或时间强制保留的主题,还有一些按压缩强制保留的主题。
日志压缩由日志清理器处理(log cleaner),它是一组后台线程,用于重新复制日志段文件,删除日志头部出现的键记录。
配额
Kafka 集群能够对请求实施配额,以控制客户端使用的代理资源。Kafka 代理可以对共享配额的每组客户端实施两种类型的客户端配额:
网络带宽配额定义字节速率阈值(自 0.9 起)
请求率配额将 CPU 利用率阈值定义为网络和 I/O 线程的百分比(自 0.11 起)
为什么需要配额?
生产者和消费者可能会以极高的速率生产/消费大量数据或生成请求,从而垄断代理资源,导致网络饱和,并且通常会对其他客户端和代理本身造成 DOS 攻击。配额可以防止这些问题,在大型多租户集群中尤为重要,因为一小部分行为不当的客户端可能会降低行为良好的客户端的用户体验。事实上,当将 Kafka 作为服务运行时,这甚至可以根据商定的合同强制执行 API 限制。
Controller/Leader
控制器
Kafka控制器(Controller)是Kafka集群中的一个核心组件,它扮演着监控和管理Kafka集群的关键角色。控制器指责:
- 分区领导者的选举
- 副本分配和重新分配
- 集群元数据管理
- 分区副本的状态管理
所有的 Kafka Broker 都具备成为控制器的能力,但在任意时间内,整个集群只能有一个控制器。第一个成功在 Zookeeper 中创建 /controller 节点的 Broker 将成为控制器
Leader
- 当Kafka集群启动时,Controller会从ZooKeeper中读取每个分区的元数据,包括副本列表和当前的Leader信息。
- 通常情况下,副本列表中的第一个副本会被选为Leader(这可以通过配置来改变)。
- Controller通过心跳机制监控各个Broker的状态,确保Leader和Follower之间的同步。
协调器
协调器(Coordinator)是管理消费者组状态的关键组件。每个消费者组在 Kafka 中都有一个协调器,负责处理与消费者组相关的各种任务,如分区分配、消费者心跳监控、提交偏移量等。
协调器作用:
- 分区分配:协调器负责将 Kafka 主题的分区分配给消费者组中的各个消费者。确保每个分区有一个且只有一个消费者处理。
- 心跳监控:协调器定期接收消费者的心跳信号,以确认消费者仍然活跃。如果某个消费者未能在规定时间内发送心跳,协调器会触发 Rebalance(重新平衡)过程。
- 偏移量提交:协调器处理消费者提交的偏移量(offset),记录消费者已经处理到哪一条消息,以便在消费者重启或重新分配时恢复进度。
- Rebalance 过程:当消费者组的成员发生变化(如消费者加入或离开)时,协调器会触发 Rebalance 过程,以重新分配分区,确保负载均衡。
Rebalance机制
Kafka 的 Rebalance(重新平衡)机制是 Kafka 保证高可用性、负载均衡和数据一致性的关键部分。Kafka 重新平衡是 Kafka 在消费者之间重新分配分区的过程,以确保每个消费者处理的分区数量大致相同
- 触发条件
- 消费者组成员变更(增加、減少、崩溃)会触发
- 分区数或主题配置变更
- Broker 增减
- Leader 变更:在分区的 Leader 发生变化时,Rebalance 机制会确保新的 Leader 能够迅速接管,并继续处理读写请求
- rebalance过程
- 停止消费
- 协调器(Coordinator)通知
- 分配分区
- 消费者更新分区
- 恢复消费
- 静态组成员(Static group membership)
静态组成员身份是一种以固定且确定的方式将 Kafka 分区分配给消费者组中的消费者而不依赖于自动分区分配的方法。在这种方法中,开发人员明确定义分区分配,而不是让 Kafka 代理动态管理它。借助静态组成员身份,消费者组中的消费者会通过在其配置中指定分区 ID 来明确请求 Kafka 代理为其分配特定分区。每个消费者仅消费来自特定分区子集的消息,并且分区分配保持不变,直到消费者明确更改为止。
日志存储
日志文件的格式是一系列“日志条目”;每个日志条目都是一个 4 字节整数N,用于存储消息长度,后面跟着N 个消息字节。每条消息都由一个 64 位整数偏移量唯一标识,该偏移量给出了该消息在该分区上发送到该主题的所有消息流中的起始字节位置。
每个日志文件都以其包含的第一条消息的偏移量命名。因此,创建的第一个文件将是 0000000000000000000000.log
默认情况下,Kafka的日志文件大小是固定的,通常为1GB(可以通过log.segment.bytes参数进行配置)。当日志文件写满后,Kafka会创建一个新的日志文件继续写入。新文件的名称将基于该文件中第一条消息的偏移量。
除了日志文件外,Kafka还会为每个日志文件创建一个索引文件(通常以.index为后缀)。索引文件以偏移量为索引来记录对应的日志文件中的消息偏移量,这有助于快速定位到消息的物理位置。
offset 是 Kafka 消息的逻辑序号,用于消费者识别和定位消息;而 position 是消息在物理存储中的具体字节偏移量。Kafka 通过索引文件将 offset 映射到 position,从而实现快速的消息检索。
分区目录内容:
|── test-topic-0
├── 00000000000000000000.index
├── 00000000000000000000.log
├── 00000000000000000000.timeindex
├── 00000000000000001007.index
├── 00000000000000001007.log
├── 00000000000000001007.snapshot
├── 00000000000000001007.timeindex
├── leader-epoch-checkpoint
查看.index内容,执行:
bin/kafka-run-class.sh kafka.tools.DumpLogSegments --deep-iteration --print-data-log --files /tmp/kafka-logs/quickstart-events-0/00000000000000000000.index 可以看到如下index内容
offset: 0 position: 0
查看.log文件,执行:bin/kafka-run-class.sh kafka.tools.DumpLogSegments --deep-iteration --print-data-log --files /tmp/kafka-logs/quickstart-events-0/00000000000000000000.log
Log starting offset: 0
baseOffset: 0 lastOffset: 0 count: 1 baseSequence: 0 lastSequence: 0 producerId: 0 producerEpoch: 0 partitionLeaderEpoch: 0 isTransactional: false isControl: false deleteHorizonMs: OptionalLong.empty position: 0 CreateTime: 1724817016421 size: 90 magic: 2 compresscodec: none crc: 2848027936 isvalid: true
| offset: 0 CreateTime: 1724817016421 keySize: -1 valueSize: 22 sequence: 0 headerKeys: [] payload: This is my first event
baseOffset: 1 lastOffset: 1 count: 1 baseSequence: 1 lastSequence: 1 producerId: 0 producerEpoch: 0 partitionLeaderEpoch: 0 isTransactional: false isControl: false deleteHorizonMs: OptionalLong.empty position: 90 CreateTime: 1724817020796 size: 91 magic: 2 compresscodec: none crc: 3694315316 isvalid: true
| offset: 1 CreateTime: 1724817020796 keySize: -1 valueSize: 23 sequence: 1 headerKeys: [] payload: This is my second event
baseOffset: 2 lastOffset: 6 count: 5 baseSequence: 2 lastSequence: 6 producerId: 0 producerEpoch: 0 partitionLeaderEpoch: 0 isTransactional: false isControl: false deleteHorizonMs: OptionalLong.empty position: 181 CreateTime: 1724817208866 size: 115 magic: 2 compresscodec: none crc: 2750341639 isvalid: true