「这是我参与2022首次更文挑战的第23天,活动详情查看:2022首次更文挑战」
- 关于作者:励志不秃头的一个CURD的Java农民工
- 关于文章:上次说了写kafka的不基本概念的第一篇,在文章的最后说了未待续,主要是因为放在一起篇幅太长了,马上送上不基本概念第二篇
消息存储
存储机制——LogSegment
kafka是通过分段的方式将Log分为多个LogSegment,LogSegment是一个逻辑上的概念,一个LogSegment对应磁盘上的一个日志文件和一个索引文件
每个partition相当于一个巨型文件被平均分配到多个大小相等的segment数据文件中(每个segment文件中的消息不一定相等),这种特性方便已经被消费的消息的清理,提高磁盘的利用率。
- log.segment.bytes=107370 (设置分段大小),默认是1gb
segment文件命名规则:partion全局的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset值进行递增。数值最大为64位long大小,20位数字字符长度,没有数字用0填充
- 假如第一个log文件的最后一个offset为:5376,所以下一个segment的文件命名为:00000000000000005376.log。对应的index为00000000000000005376.index
segment index file采取稀疏索引存储方式,它减少索引文件大小
稀疏索引存储方式: index文件中并没有为每一条message建立索引。而是采用了稀疏存储的方式,每隔一定字节的数据建立一条索引,这样的话就是避免了索引文件占用过多的空间和资源,从而可以将索引文件保留到内存中。缺点是没有建立索引的数据在查询的过程中需要小范围内的顺序扫描操作。(考虑到index文件在加载到内存的时候,能不占用大量的内存和CPU资源)
在Partition中通过offset查找message
为了提高查找消息的性能,为每一个日志文件添加2个索引索引文件:OffsetIndex 和 TimeIndex,分别对应.index以及.timeindex, TimeIndex索引文件格式:它是映射时间戳和相对offset
查找索引内容:
sh kafka-run-class.sh kafka.tools.DumpLogSegments --files /tmp/kafka-logs/test-0/00000000000000000000.index --print-data-log
如图所示,index中存储了索引以及物理偏移量, log存储了消息的内容以及物理偏移量。索引文件的元数据执行对应数据文件中message的物理偏移地址。
查找算法
二分查找
查找举例
其中00000000000000000000.index文件中的3,4597对应到00000000000000000000.log文件中的第三条消息,并且该消息的绝对位置是4597。但是如果消费者想要 获取5,7912的话,此时index文件中并没有5,所以根据二分查找,先找到3的位置,在进行顺序扫描从而找到5,7912的message。
日志
清除策略
日志的分段存储,一方面能够减少单个文件内容的大小,另一方面,方便kafka进行日志清理。日志的清理策略有两个:
- 根据消息的保留时间,当消息在kafka中保存的时间超过了指定的时间,就会触发清理过程
- 根据topic存储的数据大小,当topic所占的日志文件大小大于一定的阀值,则可以开始删除最旧的消息。kafka会启动一个后台线程,定期检查是否存在可以删除的消息
通过log.retention.bytes和log.retention.hours这两个参数来设置,当其中任意一个达到要求,都会执行删除。
默认的保留时间是:7天
压缩策略
日志压缩,通过这个功能可以有效的减少日志文件的大小,缓解磁盘紧张的情况,在很多实际场景中,消息的key和value的值之间的对应关系是不断变化的,就像数据库中的数据会不断被修改一样,消费者只关心key对应的最新的value。
因此,可以开启kafka的日志压缩功能,服务端会在后台启动启动Cleaner线程池,定期将相同的key进行合并,只保留最新的value值
磁盘存储性能问题
顺序写
每条消息都被 append 到 patition 中,属于顺序写磁盘
零拷贝
通过“零拷贝”技术,可以去掉这些没必要的数据复制操作,同时也会减少上下文切换次数。
主要通过linux的sendfile来完成,直接将数据从页缓存传输到socket中,使用sendfile,只需要一次拷贝就行,允许操作系统将数据直接从页缓存发送到网络上。Java提供了访问这个系统调用的方法:FileChannel.transferTo API
正常拷贝
零拷贝
页缓存
页缓存是用来减少磁盘I/O操作的。
磁盘高速缓存有两个重要因素:
- 访问磁盘的速度要远低于访问内存的速度,若从处理器L1和L2高速缓存访问则速度更快。
- 数据一旦被访问,就很有可能短时间内再次访问。正是由于基于访问内存比磁盘快的多,所以磁盘的内存缓存将给系统存储性能带来质的飞越。
当一个进程准备读取磁盘上的文件内容时, 操作系统会先查看待读取的数据所在的页(page)是否在页缓存(pagecache)中,如果存在(命中)则直接返回数据, 从而避免了对物理磁盘的I/0操作;
- 如果没有命中, 则操作系统会向磁盘发起读取请求并将读取的数据页存入页缓存, 之后再将数据返回给进程。
同样,如果一个进程需要将数据写入磁盘, 那么操作系统也会检测数据对应的页是否在页缓存中
- 如果不存在, 则会先在页缓存中添加相应的页, 最后将数据写入对应的页。 被修改过后的页也就变成了脏页, 操作系统会在合适的时间把脏页中的数据写入磁盘, 以保持数据的 一 致性
Kafka中提供了同步刷盘及间断性强制刷盘(fsync),可以通过 log.flush.interval.messages 和 log.flush.interval.ms 参数来控制。
同步刷盘能够保证消息的可靠性,避免因为宕机导致页缓存数据还未完成同步时造成的数据丢失。但是实际使用上,我们没必要去考虑这样的因素以及这种问题带来的损失,消息可靠性可以由多副本来解决,同步刷盘会带来性能的影响。 刷盘的操作由操作系统去完成即可。
消息消费
消费指定分区
TopicPartition p = new TopicPartition("test6", 2);//声明只消费分区号为2的分区
consumer.assign(Arrays.asList(p));//传入消费分区
分区分配策略
主要有三种rebalance的策略:range、round-robin、sticky。
Kafka 提供了消费者客户端参数partition.assignment.strategy 来设置消费者与订阅主题之间的分区分配策略。默认情况为range分配策略。
RangeAssignor(范围分区)
假设n = 分区数/消费者数量,m= 分区数%消费者数量,那么前m个消费者每个分配n+l个分区,后面的(消费者数量-m)个消费者每个分配n个分区
假设我们有10个分区,3个消费者,排完序的分区将会是0, 1, 2, 3, 4, 5, 6, 7, 8, 9;消费者线程排完序将会是C1-0, C2-0, C3-0,此时n=3,m =1,结果是这样的:
C1-0 将消费 0, 1, 2, 3 分区
C2-0 将消费 4, 5, 6 分区
C3-0 将消费 7, 8, 9 分区
假如我们有11个分区,结果是这样的
C1-0 将消费 0, 1, 2, 3 分区
C2-0 将消费 4, 5, 6, 7 分区
C3-0 将消费 8, 9, 10 分区
假如我们有2个主题(T1和T2),分别有10个分区,那么最后分区分配的结果看起来是这样的:
C1-0 将消费 T1主题的 0, 1, 2, 3 分区以及 T2主题的 0, 1, 2, 3分区
C2-0 将消费 T1主题的 4, 5, 6 分区以及 T2主题的 4, 5, 6分区
C3-0 将消费 T1主题的 7, 8, 9 分区以及 T2主题的 7, 8, 9分区
弊端:C1-0 消费者线程比其他消费者线程多消费了2个分区
RoundRobinAssignor(轮询分区)
轮询分区策略是把所有partition和所有consumer线程都列出来,然后按照hashcode进行排序。最后通过轮询算法分配partition给消费线程。如果所有consumer实例的订阅是相同的,那么partition会均匀分布。
假设我们有10个分区,3个消费者,排完序的分区将会是0, 1, 2, 3, 4, 5, 6, 7, 8, 9;消费者线程排完序将会是C1-0, C2-0, C3-0
C1-0 将消费 0, 3, 6, 9 分区
C2-0 将消费 1, 4, 7, 10 分区
C3-0 将消费 2, 5, 8 分区
StrickyAssignor 分配策略
kafka在0.11.x版本支持了StrickyAssignor,主要目的是:
- 分区的分配尽可能的均匀
- 分区的分配尽可能和上次分配保持相同
当两者发生冲突时, 第 一 个目标优先于第二个目标。
假设消费组有3个消费者:C0,C1,C2,它们分别订阅了4个Topic(t0,t1,t2,t3),并且每个主题有两个分区(p0,p1),也就是说,整个消费组订阅了8个分区:tOpO 、 tOpl 、 tlpO 、 tlpl 、 t2p0 、
t2pl 、t3p0 、 t3pl
那么最终的分配场景结果为
CO: tOpO、tlpl 、 t3p0
Cl: tOpl、t2p0 、 t3pl
C2: tlpO、t2pl
这种分配方式有点类似于轮询策略,但实际上并不是,因为假设这个时候,C1这个消费者挂了,就势必会造成重新分区(reblance),如果是轮询,那么结果应该是
CO: tOpO、tlpO、t2p0、t3p0
C2: tOpl、tlpl、t2pl、t3pl
然后,strickyAssignor它是一种粘滞策略,所以它会满足`分区的分配尽可能和上次分配保持相同`,所以分配结果应该是
消费者CO: tOpO、tlpl 、 t3p0、t2p0
消费者C2: tlpO、t2pl、tOpl、t3pl
也就是说,C0和C2保留了上一次是的分配结果,并且把原来C1的分区分配给了C0和C2。
这种策略的好处是使得分区发生变化时,由于分区的“粘性,减少了不必要的分区移动
触发rebalance时机
- 同一个consumer group内新增了消费者
- 消费者离开当前所属的consumer group,比如主动停机或者宕机
- topic新增了分区(也就是分区数量发生了变化)
consumer和partition数量建议
- 如果consumer比partition多,是浪费,因为kafka的设计是在一个partition上是不允许并发的,所以
consumer数不要大于partition数 - 如果consumer比partition少,一个consumer会对应于多个partitions,这里主要合理分配consumer数和partition数,否则会导致partition里面的数据被取的不均匀。最好
partiton数目是consumer数目的整数倍,所以partition数目很重要,比如取24,就很容易设定consumer数目 - 如果consumer从多个partition读到数据,
不保证数据间的顺序性,kafka只保证在一个partition上数据是有序的,但多个partition,根据你读的顺序会有不同
coordinator
执行Rebalance以及管理consemer的group
如何确立
consumer group的位移信息写入哪个consumer_offsets_*,那么其分区leader所在的borker就是coordinator。
rebalance过程
第一阶段,选择组协调器
组协调器GroupCoordinator: 每个consumer group都会选择一个broker作为自己的组协调器coordinator,负责监控这个消费组里的所有消费者的心跳,以及判断是否宕机,然后开启消费者rebalance。
consumer group中的每个consumer启动时会向kafka集群中的某个节点发送 FindCoordinatorRequest 请求来查找对应的组协调器GroupCoordinator,并跟其建立网络连接。
组协调器选择方式:
通过如下公式可以选出consumer消费的offset要提交到__consumer_offsets的哪个分区,这个分区leader对应的broker就是这个consumer group的coordinator
公式:hash(consumer group id) % __consumer_offsets主题的分区数
第二阶段, 加入消费组JOIN GROUP
- 在成功找到消费组所对应的 GroupCoordinator 之后就进入加入消费组的阶段,在此阶段的消费者会向GroupCoordinator 发送 JoinGroupRequest 请求,并处理响应。
- 然后GroupCoordinator 从一个consumer group中选择第一个加入group的consumer作为leader(消费组协调器),把consumer group情况发送给这个leader,接着这个leader会负责制定分区方案。
第三阶段 ( SYNC GROUP)
consumer leader通过给GroupCoordinator发送SyncGroupRequest,接着GroupCoordinator就把分区方案下发给各个consumer,他们会根据指定分区的leader broker进行网络连接以及消息消费。
保存消费端的消费位置
某个消费组消费 partition 需要保存 offset 记录当前消费位置,0.10 之前的版本是把 offset 保存到 zk 中,但是 zk 的写性能不是很好,Kafka 采用的方案是 consumer 每分钟上报一次,这样就造成了重复消费的可能。
0.10 版本之后 Kafka 就 offset 的保存从 zk 剥离,保存到一个名为 consumer_offsets 的 Topic 中。消息的 key 由 [groupid、topic、partition] 组成,value 是偏移量 offset。Topic 配置的清理策略是compact。总是保留最新的 key,其余删掉。一般情况下,每个 key 的 offset 都是缓存在内存中,查询的时候不用遍历 Partition,如果没有缓存第一次就会遍历 Partition 建立缓存然后查询返回。
consumer_offsets一一按保存了每个consumer group某一时刻提交的offset信息。
__consumer_offsets 默认有50个分区。
好了,以上就是Kafka的不基础概念第二篇了,但并不是kafka的结束,因为还有面试和实战没有讲呢,下一篇,总价下kafka面试常问的问题,敬请期待。我是新生代农民工L_Denny,我们下篇文章见。