摘要
Kafka 是一款非常优秀的开源消息引擎,以消息吞吐量高、可动态扩容、可持久化存储、高可用的特性,以及完善的文档和社区支持成为目前最流行的消息队列中间件。
Kafka 的开发社区一直非常活跃,在消息引擎的领域取的不俗成绩之后,不断拓展自己的领域,在基于事件的流处理平台方向一直发力,不断自我更新迭代力图成为这个领域内的事实标准。
Kafka 的消息引擎功能十分强大,但是一直没有停下自我突破的脚步,随着 3.0 版本的中 KRaft 协议的推出,Zookeeper 的退出进程正式启动,Kafka 开始了又一次的自我蜕变。
ZK 的移除是一个非常大胆的举动,因为 ZK 在 Kafka 的集群管理中居于核心的地位,不会轻易取代,那为什么 Kafka 选择了自行实现选举机制的路线?
此外,虽然 Kafka 具备诸多优秀的特性,这些如今被视为最佳实践的特性也是不断演化而来的,从其不断升级改进的过程中也能间接反映出生产环境所面临的现实问题,那么 Kafka 在实际的生产环境中的表现究竟如何?
作为业务方,使用 Kafka 作为消息中间件进行业务开发,保证服务平稳运行需要避开哪些雷区?
这篇文档将从一个比较高的视角,从 Kafka 的设计理念、架构到实现层面进行深入解读,随着对 Kafka 相关机制的深入了解,这些问题的答案将浮出水面。
须知事项
-
这篇文档基于 Kafka 最近刚刚发布的 3.2 版本源码为基础进行介绍,主要讨论 Java 和 Scala 语言实现的原版客户端和服务端,其他语言版本的客户端与这篇文档介绍的机制在实现上会有较大出入,需要留意
-
此外,字节的业务很多使用的都是自研的 BMQ [3],在客户端协议上是完全兼容的,但是服务端进行了完全的重构,本文介绍的相关服务端机制并不适用
-
Kafka 整个项目包括 Core、Connect、Streams,只有 Core 这一部分是我们通常说的核心消息引擎组件,另外两个都是基于这个核心实现的上层应用,这篇文章主要介绍的就是 Kafka Core 相关的内容,下面的 「Kafka 的应用架构部分」会对这一点做简要介绍
名词对照
下面的表格给出了 Kafka 中出现的一些高频和重要概念的对照解释
英文名 |
中文名 |
解释 |
备注 |
KIP |
Kafka 改进提案 |
KIP(Kafka Improvement Proposal)是针对 Kafka 的一些重大功能变更的提案,通常包括改进动机、提议的改进内容、接口变更等内容 |
|
Partition |
分区 |
一个独立不可再分割的消息队列,分区中会有多个副 本保存消息,他们的状态应该是一致的 |
Kafka 分区副本的同步机制不是纯异步的,有高水位机制去跟踪从副本的同步进度,并且有对应的领导者副本选举机制保证分区整体对外可见的消息都是已提交的 |
Replica |
副本 |
分区中消息的物理存储单元,通常对应磁盘上的一个日志目录,目录中会对消息文件进一步进行分段保存 |
|
Leader Replica |
主副本、领导者副本 |
指一个 Partition 的多个副本中,对外提供读写服务的那个副本 |
Kafka 集群范围有对等地位的组件是 Controller |
Consumer |
消费者 |
Kafka 客户端消费侧的一个角色,负责将 Broker 中的消息拉取到客户端本地进行处理,还可以使用 Kafka 提供的消费者组管理机制进行消费进度的跟踪 |
|
Consumer Group Leader |
消费者组领导者 |
通常指 Consumer Group 中负责生成分区分配方案的 Consumer |
这个概念非常容易和上面的 Leader Replica 混淆 |
Log start offset |
消息起始偏移 |
Log start offset,Kafka 分区消息可见性的起点 |
此位置对应一条消息,对外可见可以消费 |
LSO |
上次稳定偏移 |
Last stable offset,和 Kafka 事务相关的一个偏移 |
当消费端的参数isolation.level 设置为“read_committed"的时候,那么消费者就会忽略事务未提交的消息,既只能消费到LSO(LastStableOffset)的位置 |
LEO |
消息终止偏移 |
Log end offset,Kafka 分区消息的终点 |
LEO 是下一条消息将要写入的位置,对外不可见不可供消费 |
HW |
高水位 |
High water mark,用于控制消息可见性,HW 以下的消息对外可见 |
HW 的位置可能对应一条消息,但是对外不可见不可以消费,HW 的最大值是 LEO |
LW |
低水位 |
Low water mark,用于控制消息可见性,LW 及以上的消息对外可见 |
一般情况下和 Log start offset 可以等价替换,代码里也是这个逻辑 |
ISR |
已同步副本 |
In sync replica 指满足副本同步要求的副本集合,包括领导者副本 |
副本同步是按照时间差进行判定的,而非消息偏移的延迟 |
Kafka 的应用生态
下面这张是我根据 Confluent 博客的一张资料图重绘的 Kafka 应用生态架构图,在正式开始介绍本文的主题之前,我们先了解一下 Kafka 的整个应用生态
这张图中居于核心地位的是 Kafka Core 的集群,也是我们常用的消息引擎的这部分功能,是我们这篇文档重点介绍的对象
在核心的周围,第一层是 Producer 和 Consumer 的基础 API,提供基础事件流消息的推送和消费
而基于这些基础 API Kafka 提供了更加高级的 Connect API,能够实现 Kafka 和其他数据系统的连接,比如消息数据自动推送到 MySQL 数据库或者将 RPC 请求转换为事件流消息进行推送
此外,Kafka 基于自己的消息引擎打造了一个流式计算平台 Streams,提供流式的计算和存储一体化的服务
Kafka Core 架构
Kafka Core 架构部分的解读从模型、角色和实体、典型架构三个方向层层递进进行介绍
消息模型
Kafka 的消息模型主要由生产消费模型、角色和实体,以及实体关系构成,前者表示了消息的生产消费模式,后者描述了为了实现前者,其内部角色和实体存在怎样的逻辑关系
基本消息生产消费模型如下图所示:
图中展示了一个非常基本的生产消费场景,生产端向队列尾部发送消息,消费端从队列头部开始消费
从左往右看分别是消费端、消息队列、生产端,这三块我们分开进行详细介绍
消费端
在消费端有众多消费者,它们之间用消费者组关联起来
注意图中 Consumer 0 是没有分配到分区进行消费的,因为消费者组主要起个负载均衡的作用,一个分区被两个消费者消费从业务视角来看就是在重复消费了
对已经分配到分区的消费者来说,消费从队列的头部开始,在 HW 前结束
消息队列
消息队列处于整个消息模型中心的地位,是连接生产端和消费端的枢纽,Kafka 在性能优化上做的工作最多的就是这一个部分
因为 Kafka 的消息存储是队列的数据结构,只允许消息追加写入,这样的设计能最大化利用现有持久化存储介质的写入性能(SSD 和 HDD 都存在顺序写入性能远大于随机写入的特性),实现消息队列的高吞吐量
此外,Kafka 的队列还设计了高水位机制,避免未被从副本完成同步的消息被消费者感知并消费
生产端
生产端的 Producer 持续发送消息到队列中,消息追加到队列尾部,通过指定分区算法可以决定消息发往 Topic 下的哪个分区
小结
Kafka 的整个消息模型还是基于经典的消息模型去设计和改进的,消息模型的设计还是非常简洁易懂的,它的创新和优势就是在于将这一套模型用分布式的多机模式实现出来,能支撑住大并发、高吞吐量场景下的低时延、高可用的业务需求
当然这套模型之下,还有一些比较小的话题值得去讨论,我这里选了两个话题展开叙述来结束这一节
Push vs Pull
在 Kafka 定义的消息模型中,消费端是通过主动拉取消息的方式来消费的,但是与之对应的还有消息推送模型,Broker 对生产者推送过来的消息进行主动分发和推送到消费端
直觉上我们会觉得这种方式很自然,或者认为这是消息引擎的唯一范式,但是实际上关于为什么选择 Pull 的方式来进行消费,Kafka 的官方文档中关于这部分设计有专门列出来,主要讨论的点是消息消费的流控策略应该放在 Broker 端还是 Consumer 端。
零拷贝(Zero-Copy)
零拷贝从广义的角度来看不是一种具体的技术实现(仅指操作系统实现的零拷贝机制),而是一种优化思想或者技巧,针对程序运行中不可变的数据或者不可变的部分尽量减少或者取消内存数据的拷贝,用内存地址去引用这些数据
Kafka 的消息队列的核心功能就是进行各种数据的 IO 和转发(IO 密集型应用),零拷贝带来的收益非常明显:
-
减少了 JVM 堆内存占用,降低了 GC 导致的服务暂停和 OOM 风险
-
减少了大批量频繁内存拷贝的时间,能大幅优化数据吞吐性能
所以很有必要进行这样的优化
Kafka 的实例是运行在 JVM 里的,零拷贝的技术落地也离不开 Java 运行时提供的环境,具体到实现上主要依赖 Java 提供的 FileChannel 去映射文件
针对消息拉取消费的场景,直接将日志段 FileChannel 中对应偏移和长度(Kafka 的日志段都有对应的索引文件,所以不需要读取原始消息日志段文件就能拿到这些信息)的数据发送到网络栈,规避应用层的数据拷贝中转
针对消息推送生产的场景,从网络栈读取出来处理好的消息直接从内存 Buffer 中向 FileChannel 写入追加,当然这个场景并没有实现严格意义上的零拷贝(JVM 堆内存存在于用户空间,写入文件中必须要拷贝到内核),只不过 Kafka 用了 MemoryRecords 这个类基于 Buffer 去管理内存中的消息,规避了使用对象结构的方式管理可能存在的内存拷贝和数据序列化行为(这个优化的思路和 String 以及 StringBuilder 一致)
这里只是以场景的例子提供一些分析零拷贝实现机制的视角(系统原生支持 + 处理逻辑层面优化),零拷贝单独展开也是一个很大的话题,总体来讲就是在各个环节尽可能减少内存拷贝的次数,提高数据读写性能
角色和实体
在 Kafka 对上述消息模型的实现中,定义了一系列负责执行的角色和表达数据结构的实体,每个角色和实体都有其对应的责任边界,这些角色和实体之间共同配合完成整个消息引擎的运作
Kafka 中有这么一些比较重要的角色和实体: