Kafka 架构及基本原理简析
Kafka 简介
Kafka 是一个由 Scala 和 Java 编写的企业级的消息发布和订阅系统,最早是由 Linkedin 公司开发,最终开源到 Apache 软件基金会的项目。Kafka 是一个分布式的,支持分区的,多副本的和多订阅者的高吞吐量的消息系统,被广泛应用在应用解耦、异步处理、限流削峰和消息驱动等场景。本文将针对 Kafka 的架构和相关组件进行简单的介绍。在介绍 Kafka 的架构之前,我们先了解一下 Kafk 的核心概念。
Kafka 核心概念
在详细介绍 Kafka 的架构和基本组件之前,需要先了解一下 Kafka 的一些核心概念。Producer:消息的生产者,负责往 Kafka 集群中发送消息;Consumer:消息的消费者,主动从 Kafka 集群中拉取消息。Consumer Group:每个 Consumer 属于一个特定的 Consumer Group,新建 Consumer 的时候需要指定对应的 Consumer Group ID。Broker:Kafka 集群中的服务实例,也称之为节点,每个 Kafka 集群包含一个或者多个 Broker(一个 Broker 就是一个服务器或节点)。Message:通过 Kafka 集群进行传递的对象实体,存储需要传送的信息。Topic:消息的类别,主要用于对消息进行逻辑上的区分,每条发送到 Kafka 集群的消息都需要有一个指定的 Topic,消费者根据 Topic 对指定的消息进行消费。Partition:消息的分区,Partition 是一个物理上的概念,相当于一个文件夹,Kafka 会为每个 topic 的每个分区创建一个文件夹,一个 Topic 的消息会存储在一个或者多个 Partition 中。Segment:一个 partition 当中存在多个 segment 文件段(分段存储),每个 Segment 分为两部分,.log 文件和 .index 文件,其中 .index 文件是索引文件,主要用于快速查询.log 文件当中数据的偏移量位置;.log 文件:存放 Message 的数据文件,在 Kafka 中把数据文件就叫做日志文件。一个分区下面默认有 n 多个.log 文件(分段存储)。一个.log 文件大默认 1G,消息会不断追加在.log 文件中,当.log 文件的大小超过 1G 的时候,会自动新建一个新的.log 文件。.index 文件:存放.log 文件的索引数据,每个.index 文件有一个对应同名的.log 文件。
后面我们会对上面的一些核心概念进行更深入的介绍。在介绍完 Kafka 的核心概念之后,我们来看一下 Kafka 的对外提供的基本功能,组件及架构设计。
Kafka API
如上图所示,Kafka 主要包含四个主要的 API 组件:1. Producer API 应用程序通过 Producer API 向 Kafka 集群发送一个或多个 Topic 的消息。
2. Consumer API 应用程序通过 Consumer API,向 Kafka 集群订阅一个或多个 Topic 的消息,并处理这些 Topic 下接收到的消息。
3. Streams API 应用程序通过使用 Streams API 充当流处理器(Stream Processor),从一个或者多个 Topic 获取输入流,并生产一个输出流到一个或者多个 Topic,能够有效地将输入流进行转变后变成输出流输出到 Kafka 集群。
4. Connect API 允许应用程序通过 Connect API 构建和运行可重用的生产者或者消费者,大数据培训能够把 kafka 主题连接到现有的应用程序或数据系统。Connect 实际上就做了两件事情:使用 Source Connector 从数据源(如:DB)中读取数据写入到 Topic 中,然后再通过 Sink Connector 读取 Topic 中的数据输出到另一端(如:DB),以实现消息数据在外部存储和 Kafka 集群之间的传输。
Kafka 架构
接下来我们将从 Kafka 的架构出发,重点介绍 Kafka 的主要组件及实现原理。Kafka 支持消息持久化,消费端是通过主动拉取消息进行消息消费的,订阅状态和订阅关系由客户端负责维护,消息消费完后不会立刻删除,会保留历史消息,一般默认保留 7 天,因此可以通过在支持多订阅者时,消息无需复制多分,只需要存储一份就可以。下面将详细介绍每个组件的实现原理。1. ProducerProducer 是 Kafka 中的消息生产者,主要用于生产带有特定 Topic 的消息,生产者生产的消息通过 Topic 进行归类,保存在 Kafka 集群的 Broker 上,具体的是保存在指定的 partition 的目录下,以 Segment 的方式(.log 文件和.index 文件)进行存储。
2. ConsumerConsumer 是 Kafka 中的消费者,主要用于消费指定 Topic 的消息,Consumer 是通过主动拉取的方式从 Kafka 集群中消费消息,消费者一定属于某一个特定的消费组。
3. TopicKafka 中的消息是根据 Topic 进行分类的,Topic 是支持多订阅的,一个 Topic 可以有多个不同的订阅消息的消费者。Kafka 集群 Topic 的数量没有限制,同一个 Topic 的数据会被划分在同一个目录下,一个 Topic 可以包含 1 至多个分区,所有分区的消息加在一起就是一个 Topic 的所有消息。
4. Partition 在 Kafka 中,为了提升消息的消费速度,可以为每个 Topic 分配多个 Partition,这也是就之前我们说到的,Kafka 是支持多分区的。默认情况下,一个 Topic 的消息只存放在一个分区中。Topic 的所有分区的消息合并起来,就是一个 Topic 下的所有消息。每个分区都有一个从 0 开始的编号,每个分区内的数据都是有序的,但是不同分区直接的数据是不能保证有序的,大数据培训因为不同的分区需要不同的 Consumer 去消费,每个 Partition 只能分配一个 Consumer,但是一个 Consumer 可以同时一个 Topic 的多个 Partition。
5. Consumer GroupKafka 中的每一个 Consumer 都归属于一个特定的 Consumer Group,如果不指定,那么所有的 Consumer 都属于同一个默认的 Consumer Group。Consumer Group 由一个或多个 Consumer 组成,同一个 Consumer Group 中的 Consumer 对同一条消息只消费一次。每个 Consumer Group 都有一个唯一的 ID,即 Group ID,也称之为 Group Name。Consumer Group 内的所有 Consumer 协调在一起订阅一个 Topic 的所有 Partition,且每个 Partition 只能由一个 Consuemr Group 中的一个 Consumer 进行消费,但是可以由不同的 Consumer Group 中的一个 Consumer 进行消费。如下图所示:
在层级关系上来说 Consumer 好比是跟 Topic 对应的,而 Consumer 就对应于 Topic 下的 Partition。Consumer Group 中的 Consumer 数量和 Topic 下的 Partition 数量共同决定了消息消费的并发量,且 Partition 数量决定了最终并发量,因为一个 Partition 只能由一个 Consumer 进行消费。当一个 Consumer Group 中 Consumer 数量超过订阅的 Topic 下的 Partition 数量时,Kafka 会为每个 Partition 分配一个 Consumer,多出来的 Consumer 会处于空闲状态。当 Consumer Group 中 Consumer 数量少于当前定于的 Topic 中的 Partition 数量是,单个 Consumer 将承担多个 Partition 的消费工作。如上图所示,Consumer Group B 中的每个 Consumer 需要消费两个 Partition 中的数据,而 Consumer Group C 中会多出来一个空闲的 Consumer4。总结下来就是:同一个 Topic 下的 Partition 数量越多,同一时间可以有越多的 Consumer 进行消费,消费的速度就会越快,吞吐量就越高。同时,Consumer Group 中的 Consumer 数量需要控制为小于等于 Partition 数量,且最好是整数倍:如 1,2,4 等。
6. Segment 考虑到消息消费的性能,Kafka 中的消息在每个 Partition 中是以分段的形式进行存储的,即每 1G 消息新建一个 Segment,每个 Segment 包含两个文件:.log 文件和.index 文件。之前我们已经说过,.log 文件就是 Kafka 实际存储 Producer 生产的消息,而.index 文件采用稀疏索引的方式存储.log 文件中对应消息的逻辑编号和物理偏移地址(offset),以便于加快数据的查询速度。.log 文件和.index 文件是一一对应,成对出现的。下图展示了.log 文件和.index 文件在 Partition 中的存在方式。
Kafka 里面每一条消息都有自己的逻辑 offset(相对偏移量)以及存在物理磁盘上面实际的物理地址便宜量 Position,也就是说在 Kafka 中一条消息有两个位置:offset(相对偏移量)和 position(磁盘物理偏移地址)。在 kafka 的设计中,将消息的 offset 作为了 Segment 文件名的一部分。Segment 文件命名规则为:Partition 全局的第一个 Segment 从 0 开始,后续每个 segment 文件名为上一个 Partition 的最大 offset(Message 的 offset,非实际物理地偏移地址,实际物理地址需映射到.log 中,后面会详细介绍在.log 文件中查询消息的原理)。数值最大为 64 位 long 大小,由 20 位数字表示,前置用 0 填充。
上图展示了.index 文件和.log 文件直接的映射关系,通过上图,我们可以简单介绍一下 Kafka 在 Segment 中查找 Message 的过程:
1.根据需要消费的下一个消息的 offset,这里假设是 7,使用二分查找在 Partition 中查找到文件名小于(一定要小于,因为文件名编号等于当前 offset 的文件里存的都是大于当前 offset 的消息)当前 offset 的最大编号的.index 文件,这里自然是查找到了 00000000000000000000.index。
2.在.index 文件中,使用二分查找,找到 offset 小于或者等于指定 offset(这里假设是 7)的最大的 offset,这里查到的是 6,然后获取到 index 文件中 offset 为 6 指向的 Position(物理偏移地址)为 258。
3.在.log 文件中,从磁盘位置 258 开始顺序扫描,直到找到 offset 为 7 的 Message。至此,我们就简单介绍完了 Segment 的基本组件.index 文件和.log 文件的存储和查询原理。但是我们会发现一个问题:.index 文件中的 offset 并不是按顺序连续存储的,为什么 Kafka 要将索引文件设计成这种不连续的样子?这种不连续的索引设计方式称之为稀疏索引,Kafka 中采用了稀疏索引的方式读取索引,kafka 每当.log 中写入了 4k 大小的数据,就往.index 里以追加的写入一条索引记录。使用稀疏索引主要有以下原因:
(1)索引稀疏存储,可以大幅降低.index 文件占用存储空间大小。
(2)稀疏索引文件较小,可以全部读取到内存中,可以避免读取索引的时候进行频繁的 IO 磁盘操作,以便通过索引快速地定位到.log 文件中的 Message。
7. MessageMessage 是实际发送和订阅的信息是实际载体,Producer 发送到 Kafka 集群中的每条消息,都被 Kafka 包装成了一个 Message 对象,之后再存储在磁盘中,而不是直接存储的。Message 在磁盘中的物理结构如下所示。
其中key
和value
存储的是实际的 Message 内容,长度不固定,而其他都是对 Message 内容的统计和描述,长度固定。因此在查找实际 Message 过程中,磁盘指针会根据 Message 的offset
和message length
计算移动位数,以加速 Message 的查找过程。之所以可以这样加速,因为 Kafka 的.log 文件都是顺序写的,往磁盘上写数据时,就是追加数据,没有随机写的操作。
8.Partition Replicas 最后我们简单聊一下 Kafka 中的 Partition Replicas(分区副本)机制,0.8 版本以前的 Kafka 是没有副本机制的。创建 Topic 时,可以为 Topic 指定分区,也可以指定副本个数。kafka 中的分区副本如下图所示:
Kafka 通过副本因子(replication-factor)控制消息副本保存在几个 Broker(服务器)上,一般情况下副本数等于 Broker 的个数,且同一个副本因子不能放在同一个 Broker 中。副本因子是以分区为单位且区分角色;主副本称之为 Leader(任何时刻只有一个),从副本称之为 Follower(可以有多个),处于同步状态的副本叫做 in-sync-replicas(ISR)。Leader 负责读写数据,Follower 不负责对外提供数据读写,只从 Leader 同步数据,消费者和生产者都是从 leader 读写数据,不与 follower 交互,因此 Kafka 并不是读写分离的。同时使用 Leader 进行读写的好处是,降低了数据同步带来的数据读取延迟,因为 Follower 只能从 Leader 同步完数据之后才能对外提供读取服务。
如果一个分区有三个副本因子,就算其中一个挂掉,那么只会剩下的两个中,选择一个 leader,如下图所示。但不会在其他的 broker 中,另启动一个副本(因为在另一台启动的话,必然存在数据拷贝和传输,会长时间占用网络 IO,Kafka 是一个高吞吐量的消息系统,这个情况不允许发生)。如果指定分区的所有副本都挂了,Consumer 如果发送数据到指定分区的话,将写入不成功。Consumer 发送到指定 Partition 的消息,会首先写入到 Leader Partition 中,写完后还需要把消息写入到 ISR 列表里面的其它分区副本中,写完之后这个消息才能提交 offset。
到这里,差不多把 Kafka 的架构和基本原理简单介绍完了。Kafka 为了实现高吞吐量和容错,还引入了很多优秀的设计思路,如零拷贝,高并发网络设计,顺序存储,以后有时间再说。
评论