kafka生产者生产的消息需要存储在服务端,那么服务端就需要保证消息的健壮性,需要保证其线性扩展,负载均衡,故障容错等。那么kafka是怎么来做的喃?
分区:指消息按照分区分布在kafka集群中所有的节点上;复制:指分区都会有多个副本存储在不同的节点上。消息存储:指新的消息总是以追加的方式进行文件存储。分区可以做到线性扩展和负载均衡。复制可以做到故障容错。消息追加的方式进行信息存储可以提供很高的写效率。
分区 副本
消息按照主题分类,为了提高消息的并行处理能力,每个主题会有多个分区,为了保证消息的可用性,每个分区都会有多个副本。主题以分区的形式存储在多个代理节点上,ZK记录了主题和分区的对应关系,集群中每个代理节点都会管理多个主题的多个分区。
主题采用多个分区,可控制消息写往不同的节点,从而分散每个节点的压力。客户端以分区作为最小的处理单位,生产者将消息同时写入不同的节点,多个消费者可以同时读取不同节点的不同分区数据,加快消费消息,降低消息的延迟。
为了保证分区的可用性,采用副本机制为一个分区备份多个副本,一个分区只有一个主副本(Leader),多个备份副本(Follower)。主副本负责客户端的读写,备份副本负责同步主副本的数据。当主副本挂掉之后,在多个备份副本中选择一个作为主副本,继续为客户端提供读写服务。分区有两个重要的集合:AR(分区的所有副本集合)和ISR(和主副本正在同步的副本集合)。副本是真正存储在消息代理节点上,持有日志文件对象。客户端访问分区,先获取分区的主副本,然后获取主副本所在的消息代理节点编号,最后从消息代理节点读写主副本对应的日志文件。
将分区数据存储到日志文件上时,每个分区对应一个目录,目录下有多个日志分段。同一个目录下的所有日志分段都属于同一个分区。每个日志分段在物理上由一个数据文件和一个索引文件组成。数据文件存储的是消息的真正内容,索引文件存储的是数据文件的索引信息。为数据文件建立索引文件目的是更快的访问数据文件。生产者采用追加的方式将消息写入日志文件,顺序写盘性能很高,同样消费者也是顺序读盘,但是消费者在异常恢复情况下可能需要重新处理消息,这个时候可以利用索引文件重新定位到数据文件中的消息。
消息写入日志文件
每个分区都有一个日志对象管理分区的所有日志分段。
生产者在发送消息时,会在客户端将属于同一个分区的一批消息作为一个生产请求发送给服务端。java版本的生产者产生的消息内容是字节缓冲区(ByteBuffer)。
消息集
消息集中的每条消息都会被分配一个相对偏移量,每一批消息集中消息的相对偏移量都从0开始:第一批消息:[0,1,2,3]。第二批消息:[0,1,2]。
消息集中的每条消息有三个部分组成:偏移量,大小,消息内容。消息内容包含:键值,键值的长度,校验值等数据(Record)。
kafka服务端在存储消息时,会为每条消息都指定一个唯一的偏移量。同一个分区的所有日志分段中消息的偏移量从0开始不断递增,不同分区的偏移量直接没关系。这也就是kafka只能保证同一个分区的消息有序性,不能保证跨分区消息的有序性。
客户端创建消息集中每条消息的偏移量,都是相对于本批次消息集的偏移量,每批消息的偏移量都是从0开始,但是这个偏移量不能直接存储在日志文件中。在服务端要对此偏移量进行转换,计算消息的偏移量时,采用下一个偏移量(nextOffsetMetadata)的值来替换消息中的相对偏移量,这时消息中偏移量就是保存在文件中的绝对偏移量。
疑问:既然要在服务端进行替换偏移量,为什么还要在客户端进行设置,而不在服务端直接设置喃?
说明:客户端生产的消息传到服务端时都是转化成了二进制内容保存在字节缓冲区中,假设我们在客户端没有设置偏移量写入字节缓冲区中,那么在服务端存储消息时,就需要在字节缓冲区每条消息前面添加偏移量,这样就需要修改字节缓冲区的大小。不能直接使用原来的缓冲区。假如我们在客户端设置了偏移量,消息格式是固定了。在服务端存储消息时,直接修改字节缓冲区中每条消息的偏移量的值就行了,其他数据内容不变,字节缓冲区的大小也不会发生变化。
服务端将每个分区的消息追加到日志中是以日志分段为单位的。日志分段中数据文件的大小的阀值1G,数据文件中存储的消息达到阀值后会创建一个新的日志分段文件来存储信息,分区的消息总是追加到最新的日志分段中,也就是说,一个分区的日志文件,在任何时刻都只有一个活动的日志分段。每个日志分段都有一个基准偏移量,在一个日志分段中是固定值,用它来计算出每条消息在当前这个日志分段中的绝对偏移量,最后把消息写到日志分段中。更新日志的下一个偏移量的值(用来设置下一个消息的绝对偏移量),满足条件时调用flush()把消息刷写到磁盘。
下一个偏移量(nextOffsetMetadata)声明类型是volatile,volatile类型的变量被修改时,其他所有使用到此变量的线程都能立即看到。服务端为每条消息指定绝对偏移量,会用nextOffsetMetadata的值作为起始偏移量,将消息写入日志分段中,获取到这批消息中最后一条消息的偏移量加上一后更新nextOffsetMetadata。消费者或者备份副本会根据nextOffsetMetadata最新的值拉取到新写入的消息。
为消息集分配绝对偏移量时(更新每条消息的偏移量数据:offset),以nextOffsetMetadata的偏移量作为起始偏移量。分配完后还要更新nextOffsetMetadata的偏移量值,因此获取nextOffsetMetadata的偏移量值并加一是一个原子操作。
消息的大小和消息内容都不变动,如何在字节缓冲区中定位到每条消息的偏移量所在的位置:先读取出消息大小的值,然后计算出下一条消息的起始偏移量,使用字节缓冲区提供的定位方法(position())直接定位到下一条消息的起始位置。并不需要按照顺序完整的读取每条消息的实际内容,这样代价太大。 每条消息的长度:8个字节+4个字节+消息大小。
来之《 Kafka技术内幕:图文详解Kafka源码设计与实现 》: 相对偏移量转换为绝对偏移量
日志分段
服务端处理每批追加到日志分段中的消息集,都是以nextOffsetMetadata作为起始的绝对偏移量。同一个分区的所有日志分段中,所有消息的偏移量都是递增的。
消息代理节点上的一个主题分区(TopicPartition)对应一个日志(Log)。每个日志有多个日志分段(LogSegment),一个日志管理该分区的所有日志分段。
多个日志分段中,只有一个活动日志分段(activeSegment)来存储当前接收的消息集。其中有几个重要的方法和变量:
1,activeSegment():获取segments(所有日志分段集合)中最后一个元素,作为日志最新的活动分段。如果有新的日志分段产生就会加入到segments的最后一个。
2,nextOffsetMetadata:下一个偏移量元数据,LogOffsetMetadata对象的实例,nextOffsetMetadata的构造方法中的值来源于活动日志分段:下一个偏移量的值(nextOffset),基准偏移量(baseOffset),活动日志分段的大小(size)。
3,logEndOffset:日志的最新偏移量表示下一条消息的偏移量,它的值是nextOffsetMetadata.nextOffset的值。实际上logEndOffset就是当前活动日志分段的下一个偏移量的值。
客户端对消息的读写操作会用到日志偏移量元数据(LogOffsetMetadata),写入消息集到日志,下一个偏移量元数据(nextOffsetMetadata)中的消息偏移量(nextOffset)会作为消息集的起始偏移量;从日志中读取消息时,不能超过日志结束偏移量元数据(logEndOffsetMetadata)中的结束偏移量(logEndOffset)或日志最高水位偏移量元数据(highWatermarkMetadata)的最高水位(highWatermark)。
日志偏移量元数据(LogOffsetMetadata)包含:消息偏移量(messageOffset),日志分段的基准偏移量(segmentBaseOffset),消息在日志分段中的物理位置(relativePositionInSement)。
作者:吉之无镜
链接:https://www.jianshu.com/p/d599d5f5f12e
共同学习,写下你的评论
评论加载中...
作者其他优质文章