Kafka 的设计与实现
一、数据持久化
kafka的消息存储与文件系统之上。
操作系统会通过各种手段加快磁盘的访问速度,包括预读、后写、磁盘缓存等操作。
kafka消息存储在磁盘也能实现较高的读写速度的因素:
- 顺序写磁盘。kafka将消息以追加方式写入磁盘,采用顺序写磁盘的方式,对于磁盘IO性能是有利的。相比于随机写入,顺序写入拥有更高的吞吐量。
- 分区和并行处理。topic被分为多个分区,每个分区可以并行地处理读写操作。这允许kafka在多个磁盘上并行读写数据,提高整体吞吐量。
- 消息索引:Kafka在消息存储文件中使用索引,这有助于快速定位和检索消息。索引信息存储在内存中,减少了磁盘 I/O 的需求,提高了读取速度。
- 写入缓存:Kafka使用写入缓存,将消息先存储在内存中,然后进行批量写入磁盘,以降低磁盘 I/O 的频率。这可以提高写入性能。
- 零拷贝技术:Kafka使用零拷贝技术,通过直接内存映射文件的方式,将消息从生产者传递到磁盘,或者从磁盘读取到消费者。这减少了数据在用户空间和内核空间之间的拷贝,提高了效率。
topic是逻辑上的概念,物理上存储的是分区partition,每一个分区最终对应一个目录,里面存储所有的消息和索引文件。默认情况下每一个topic在创建时如果不指定分区数量则只会创建一个分区。
任何推送到分区的数据都会被追加到分区数据文件的尾部,顺序写入磁盘的效率使得kafka效率非常高
二、底层存储设计
在kafka的文件存储中,同一个topic下有多个不同的分区,每一个分区都为一个目录,每个目录被平均分配成多个大小相等的Segment File
中,Segment File
由Index File
和Data File
组成,两者总是成对出现,两者文件后缀分别为.index
和.log
。
Segment File
是Kafka文件存储的最小单位。
可以设置每个Segment文件的大小。
生产者向topic写入数据时,在分区文件夹下会成对产生index和data文件。
Segment文件命名规则:分区下第一个Segment文件名从0开始,达到设置的Segment文件大小后会产生第二个Segment文件。后续的Segment文件名为上一个Segment文件最后一条消息的offset值。文件名数值最大为64位long大小,19位数字字符长度,没有的数字用0填充。
以下以一对Segment File
说明索引文件和数据文件的对应关系:
以索引文件中元数据3,497
为例,表示在数据文件中第三个message,以及该消息的物理偏移地址497。
index文件不是每次递增1的,因为kafka采用稀疏索引存储方式,每隔一定字节的数据建立一条索引,以此来减小索引文件大小,同时不会给查询带来太多的时间消耗。
Segment的文件名称为上一个Segment最后一条消息的offset,所以当查找一个指定offset的message时,通过在所有的Segment文件名中进行二分查找,就不能找到它归属的Segment,再在Segment的index文件中找到对应到文件上的物理位置,就能获取到该message。
message的属性:
- offset。表示message在当前partition中的偏移量,是一个逻辑上的值,唯一确定了partition中的一条message。
- messageSize。表示message中data的大小
- data。message存储的具体内容
三、生产者设计
生产者产生并推送消息的基本流程:
- 创建一个
ProducerRecord
对象,这个对象需要办好消息主题和消息数据,可以选择性地指定一个键(key)或者分区 - 发送消息时,生产者会将键(key)和消息数据序列化为字节数组,然后发送到分配器(partitioner)
- 如果指定了分区,那么分配器什么都不做只是返回指定的分区。如果没有指定分区,分配器将根据键(key)值hash出一个partition。如果即没有指定分区,又没有指定键(key),那么将轮询选出一个partition
- 选择完分区后,生产者知道了消息所属的主题和分区,它将这条记录添加到相同主题和分区的pilling消息中,另一个线程负责发送这些pilling消息到对应的Broker中
- 当Broker接收到消息后,如果成功写入则返回一个包含消息的主题、分区及分区内记录的偏移量的RecordMetadata对象,否则返回异常
- 生产者接收到结果后,对于异常可能会选择进行重试
四、消费者设计
假设有一个主题,该主题有四个分区。有一个消费者组,这个组内只有一个消费者,那么这个消费者将会接收到这四个分区的消息。如下图所示:
在消费者组内增加一个消费者,那么每个消费者将会分别收到来自两个分区的消息。如下图所示:
如果增加到4个消费者,那么每个消费者都将会接收到一个分区的消息。如下图所示:
如果继续在这个消费者组内增加消费者,剩余的消费者会空闲,不会收到任何消息:
总之,我们可以通过增加消费者组内的消费者来进行水平扩展提升消费能力。向主题中添加超过分区数量的消费者是没有意义的,因为其中一些消费者将处于空闲状态。
除了为了扩展单个应用程序而添加消费者外,通常会有多个应用程序从相同的主题获取数据的情况。实际上,Kafka的主要设计目标之一就是Kafka主题中产生的数据可以支持在任意多的应用中被读取。
在这种情况下,我们希望每个应用程序都能获取主题中的所有消息,而不仅仅是其中的一部分。
为了确保应用程序从主题中获取到所有消息,需要确保该应用程序有自己的消费者组。与传统的消息系统不同,Kafka可以扩展大量的消费者和消费者组而不降低性能。
总结,为每个需要从一个或多个主题获取所有消息的应用程序创建一个新的消费者组。向现有消费者组添加消费者以扩展读取和处理消息的能力,组内的每个消费者将只会获取到消息的子集。
五、消费者组和分区重平衡
当新的消费者加入消费者组中,它会消费一个或多个分区,而这些分区之前是由其他消费者负责的。当消费者离开消费者组时(重启、宕机),它所消费的分区会分配给其他剩下的消费者。重新给消费者分配分区也会发生在消费者分组正在消费的主体被修改时(比如增加新的分区)。
变更分区所有权从一个消费者到另一个消费者的行为称为重平衡。充平衡是很重要的特征,因为它保证了消费者组的高可用和水平扩展能力(允许更简单更安全地增加减少消费者)。在重平衡期间,消费者不能消费消息,因此会造成整个消费者组一个短暂的不可用期。并且,当重平衡发生时,消费者会丢弃状态信息,如果它正在缓存数据,需要重新刷新缓存,这将降低程序性能直到消费者重新更新好状态。
消费者通过定期发送心跳到一个组协调者的broker来保证在消费者组中存活。对于不同的消费者组,这个broker可以是不同的。心跳在消费者拉取消息(检索记录)或提交时发送。
如果一个消费者在一段时间内停止发送心跳,它的会话将会超时,组协调器将会认定其已经死亡并触发重平衡。
- 如果一个消费者崩溃并停止处理消息,组协调者在几秒中的时间内没有收到心跳以此来决定其已死亡并触发重平衡。在这几秒的时间里,已死亡的消费者拥有的分区中的消息将不会被处理。
- 如果正常关闭消费者时,消费者将通知组协调器其正在离开,组协调器将立即出发重平衡,节省等待时间
在Kafka的0.10.1版本中,将发送心跳从拉取消息中分离,这样使得发送心跳的频率不受拉取频率影响。在更新版本的Kafka中,可以配置一个时间来决定应用程序可以在没有进行拉取时存活多久。这个配置可以用来预防活锁,活锁代表应用程序没有崩溃但是因为一些原因不能处理数据。这个配置与session.timeout.ms
是分开的,后者是控制消费者崩溃并停止发送心跳所需的时间。
六、分区与消费模型
相关链接
OB links
OB tags
#Kafka