Kafka学习笔记(五)·分区配置以及存储

五、分区

kafka可以将主题划分为多个分区(Partition),会根据分区规则选择把消息存储到哪个分区中,只要如果分区规则设置的合理,那么所有的消息将会被均匀的分布到不同的分区中,这样就实现了负载均衡和水平扩展,另外,多个订阅者可以从一个或者多个分区中同时消费数据,以支撑海量数据处理能力。

顺便说一句,由于消息是以追加到分区中的,多个分区顺序写磁盘的总效率要比随机写内存还要高(引入Apache Kafka - A High Throughput Distributed Messaging System的观点),是Kfaka高吞吐率的重要保证之一。

1、副本机制

由于Producer和Consumer都只会与Leader角色的分区副本相连,所以Kafka需要以集群的组织形式提供主题下的消息高可用。Kafka支持主备复制,所以消息具备高可用和持久性。

一个分区可以有多个副本,这些副本保存在不同的broker上。每个分区的副本中都会由一个作为Leader。当一个broker失败时,Leader在这台broker上的分区都会变得不可用,kafka会自动移除Leader,在其他副本中选一个作为新的Leader。

在通常情况下,增加分区可以提供kafka集群的吞吐量。然而,也应该意识到集群的总分区数或是单台服务器上的分区数过多,会增加不可用及延迟的风险。

image-20201218111646469

2、分区Leader选举

可以预见的是,如果某个分区的Leader挂了,那么其他跟随着将会进行选举产生一个新的Leader,之后所有的读写就会转移到这个新的Leader上,在kafka中,其不是采用常见的多数选举的方式进行副本的Leader选举,而是会在zookeeper上针对每个Topic维护一个称为ISR(in-sync-replice,已同步的副本)的集合,显然还有一些副本没有来得及同步。只有这个ISR列表里面的才有资格称为leader(先使用ISR里面的第一个,如果不行以此类推,因为ISR里面的是同步副本,消息是最完整且各个节点都是一样的)。

3、分区重新分配

我们往已经部署好的kafka集群里面添加机器是最正常不过的需求,而且添加起来非常的方便,我们需要做的事是从已经部署好的kafka节点中复制相应的配置文件,然后把里面的broker id修改成全局唯一的,最后启动这个节点即可将它加入到现用kafka集群中。

但是问题来了,新添加的kafka节点并不会自动的分配数据,所以无法分担集群的负载,除非我们新建一个topic。但是现在我们想手动将部分分区移到新添加的kafka节点,kafka内部提供了相关的工具来重新分布某个topic的分区。

具体步骤

  • 第一步:我们创建一个由三个节点的集群

    [root@k8s ~]# bin/kafka-topics.sh --create --zookeeper localhost:2181 --topic hello --partitions 3 --replication-factor 3
    Created topic hello.
    

    详情查看

    [root@k8s ~]# bin/kafka-topics.sh --zookeeper localhost:2181 --describe --topic hello
    Topic:hello		PartitionCount:3	ReplicationFactor:3		configs:
    Topic:hello		Partition: 0		Leader: 2	Replicas: 2,1,0		Isr: 2,1,0
    Topic:hello		Partition: 1		Leader: 0	Replicas: 0,2,1		Isr: 0
    Topic:hello		Partition: 2		Leader: 1	Replicas: 1,0,2		Isr: 1,0,2
    

    从上面的输出可以看出hello这个主题一共由三个分区,有三个副本

  • 第二步:主题hello在添加一个分区

    [root@k8s ~]# bin/kafka-topics.sh --alter --zookeeper localhost:2181 --topic hello --partitions 4
    WARNING: If partitions are increased for a topic that has a key, the partition logic or ordering of the messages will be affected
    Adding partitions succeeded!
    

    查看详情已经变成4个分区

    [root@k8s ~]# bin/kafka-topics.sh --zookeeper localhost:2181 --describe --topic hello
    Topic:hello		PartitionCount:4	ReplicationFactor:3		configs:
    Topic:hello		Partition: 0		Leader: 2	Replicas: 2,1,0		Isr: 2,1,0
    Topic:hello		Partition: 1		Leader: 0	Replicas: 0,2,1		Isr: 0
    Topic:hello		Partition: 2		Leader: 1	Replicas: 1,0,2		Isr: 1,0,2
    Topic:hello		Partition: 3		Leader: 2	Replicas: 2,1,0		Isr: 2,1,0
    

    这样会导致broker2维护更多的分区

  • 第三步:在添加一个broker节点

    查看主题信息

    [root@k8s ~]# bin/kafka-topics.sh --zookeeper localhost:2181 --describe --topic hello
    Topic:hello		PartitionCount:4	ReplicationFactor:3		configs:
    Topic:hello		Partition: 0		Leader: 2	Replicas: 2,1,0		Isr: 2,1,0
    Topic:hello		Partition: 1		Leader: 0	Replicas: 0,2,1		Isr: 0
    Topic:hello		Partition: 2		Leader: 1	Replicas: 1,0,2		Isr: 1,0,2
    Topic:hello		Partition: 3		Leader: 2	Replicas: 2,1,0		Isr: 2,1,0
    

    从上面输出信息可以看出新添加的节点并没有分配之前主题的分区

  • 第四步:重新分配

    现在我们需要将原先分布在broker1-3节点上的分区重新分布到broker1-4节点上,借助kafka-reassign-partitions.sh工具生成reassign plan,不过我们先得按照要求定义一个文件,里面说明哪些topc需要重新分区,文件内容如下:

    [root@k8s ~]# cat reassign.json
    {"topics":[{"topic":"hello"}],
    "version":1
    }
    

    然后使用kafka-reassign-partitions.sh工具生成reassign plan

    [root@k8s ~]# bin/kafka-reassign-partitions.sh --zookeeper localhost:2181 --topics-to-move-json-file reassign.json --broker-list "0,1,2,3" --generate
    Current partition replica assignment
    {"version":1,"partitions":[{"topic":"hello","partition":2,"replicas":[1,0,2],"log_dirs":["any","any","any"]},{"topic":"hello","partition":1,"replicas":[0,2,1],"log_dirs":["any","any","any"]},{"topic":"hello","partition":0,"replicas":[2,1,0],"log_dirs":["any","any","any"]},{"topic":"hello","partition":3,"replicas":[2,1,0],"log_dirs":["any","any","any"]}]}
    
    Proposed partition reassignment configuration
    {"version":1,"partitions":[{"topic":"hello","partition":0,"replicas":[1,2,3],"log_dirs":["any","any","any"]},{"topic":"hello","partition":2,"replicas":[3,0,1],"log_dirs":["any","any","any"]},{"topic":"hello","partition":1,"replicas":[2,3,0],"log_dirs":["any","any","any"]},{"topic":"hello","partition":3,"replicas":[0,1,2],"log_dirs":["any","any","any"]}]}
    

    上面命令中

    --generate 表示指定类型参数

    --topics-to-move-json-file 指定分区重分配对应的主题清单路径

    注意:

    命令输入两个json字符串,第一个json内容为当前的分区副本分配情况,第二个为重新分配的候选方案,注意这里只是生成一份可行性的方案,并没有真正执行重分配的动作。

    我们将第二个JSOn内容保存到名为result.json文件里面(文件名不重要,文件格式也不重要),然后执行这些reassign plan

    执行分配策略

    [root@k8s ~]# bin/kafka-reassign-partitions.sh --zookeeper localhost:2181 --reassignment-json-file result.json --execute
    CurrentPartition replica assignment
    {"version":1,"partitions":[{"topic":"hello","partition":2,"replicas":[1,0,2],"log_dirs":["any","any","any"]},{"topic":"hello","partition":1,"replicas":[0,2,1],"log_dirs":["any","any","any"]},{"topic":"hello","partition":0,"replicas":[2,1,0],"log_dirs":["any","any","any"]},{"topic":"hello","partition":3,"replicas":[2,1,0],"log_dirs":["any","any","any"]}]}
    
    Save this to use as the --reassignment-json-file option during rollback
    Successfully started reassignment of partitions.
    

    查看分区重新分配的进度

    [root@k8s ~]# bin/kafka-reassign-partitions.sh --zookeeper localhost:2181 --reassignment-json-file result.json --verify
    Status of partition reassignment:
    Reassignment of partition hello-3 completed successfully
    Reassignment of partition hello-0 completed progress
    Reassignment of partition hello-1 completed progress
    Reassignment of partition hello-2 completed progress
    

    从上面信息可以看出hello已经完成,其他三个正在进行中

4、修改副本因子

**场景:**实际项目中我们可能在创建topic时没有设置好争取的replication-factor,导致kafka集群虽然是高可用的,但是该topic在又broker宕机时,可能发生无法使用的情况。topic一定那使用又不能轻易删除重建,因此动态增加副本因子就成为最终的选额。

5、分区分配策略

按照kafka默认的消费逻辑设定,一个分区只能被同一个消费组(ConsumerGroup)内的一个消费组消费。假设目前某消费组内只有一个消费者CO,订阅了一个topic,这个topic包含7个分区,也就是说这个消费组CO订阅了7个分区,参考下图

image-20201218141622371

此时消费组内又加入了一个新的消费组C1,按照既定的逻辑需要将原来消费组C0的部分分区分配给消费组C1消费,消费者C0和C1各自负责消费所分配到的分区,相互之间并无实质性的干扰。

接着消费组内又加入了一个新的消费者C2,如此消费者C0、C1和C2各自负责消费所分配到的分区。

如果消费者过多,出现了消费者的数量大于分区的数量的情况,就会由消费者分配不到任何分区。参考下图,一共有8个消费者,7个分区,那么最后的消费者C7由于分配不到任何分区进而就无法消费任何消息

image-20201218141930697

上面各个示例中的整套逻辑时按照kafka中默认的分区分配策略来实施的。kafka提供了消费者客户端参数partition.assignment.strategy用来设置消费者与订阅主题之间的分区分配策略。默认情况下,此参数的值为:org.apache.kafka.clients.consumer.RangeAssignor,即参与RangeAssignor分配策略。除此之外,kafka中还提供了另外两种分配策略:RoundRobinAssignor和StickyAssignor。消费者客户端参数partition.assignment.strategy可以配置多个分配策略。彼此之间以逗号分隔。

  • RangeAssignor分配策略

    参考源码:org.apache.kafka.clients.consumer.RangeAssignor

    RangeAssignor策略的原理时按照消费者总数和分区总数进行整除运算来获得一个跨度,然后将分区按照跨度进行平均分配,以保证分区尽可能均匀的分配给所有的消费者。对于每一个topic,RangeAssignor策略会将消费组内所有订阅这个topic的消费者按照名称的字典序排序,然后为每个消费者划分固定的分区范围,如果不够平均分配,那么字典序靠前的消费者会被多分配一个分区。

    假设n=分区数/消费者数量m=分区数%消费者数量,那么前m个消费者每个分配n+1个分区,后面的(消费者数量-m)个消费者每个分配n个分区。

    假设消费组内又2个消费者C0和C1,都订阅了主题t0和t1,并且每个主题都有4个分区,那么所订阅的所有分区可以标识为:t0p0、t0p1、t0p2、t0p3、t1p0、t1p1、t1p2、t1p3。最终的分配结果为:

    消费者C0:t0p0、t0p1、t1p0、t1p1
    消费者C1:t0p2、t0p3、t1p2、t1p3
    

    可以明显的看到这样的分配并不均匀,如果将类似的情形扩大,有可能会出现部分消费者过载的情况。

  • RoundRobinAssignor分配策略

    参考源码:org.apache.kafka.clients.consumer.RoundRobinAssignor

    RoundRobinAssignor策略的原理时将消费组内所有消费者以及消费者所订阅的所有topic的partition按照字典序排序,然后通过轮询方式逐个将分区依次分配给每个消费者。RoundRobinAssignor策略对应的partition.assignment.strategy参数值为:org.papche.kafka.clients.consumer.RoundRobinAssignor。

    假设消费组中又2个消费者C0和C1,都订阅了主题t0和t1,并且每个主题都有3个分区,那么所订阅的所有分区可以标识为:t0p0、t0p1、t0p2、t1p0、t1p1、t1p2。最终的分配结果为:

    消费者C0:t0p0、t0p2、t1p1
    消费者C1:t0p1、t1p0、t1p2
    

    如果同一个消费组内的消费者所订阅的信息是不同的,那么在执行分区分配的时候就不是完全的轮询分配,有可能会导致分区分配的不均匀。如果某个消费者没有订阅消费组内的某个topic,那么在分配分区的时候此消费者将分配不到这个topic的任何分区。

    假设消费组内又3个消费者C0、C1和C2,他们共订阅了3个主题:t0、t1、t2,这3个主题分别由1、2、3个分区,即整个消费组订阅了t0p0、t1p0、t1p1、t2p0、t2p1、t2p2这6个分区。具体而言,消费者C0订阅的主题是t0,消费者C1订阅的是主题t0和t1,消费者C2订阅的是主题t0、t1和t2,那么最终的分配结果为:

    消费者C0:t0p0
    消费者C1:t1p0
    消费者C2:t1p1、t2p0、t2p1、t2p2
    

    可以看到RoundRobinAssignor策略也不是十分完美,这样分配其实并部署最优解,因为完全可以将分区t1p1分配给消费者C1。

  • StickyAssignor分配策略

    参考源码:org.apache.kafka.clients.consumer.StickyAssignor

    Kafka从0.11.x版本开始引入这种分配策略,它主要有两个目的:

    • ​ 分区的分配要尽可能的均匀;
    • 分区的分配尽可能的与上次分配的保持相同。

    当两者发生冲突时,第一个目标优先于第二个目标。鉴于这两个目标,StickyAssignor策略的具体实现要比RangeAssignor话让RoundRobinAssignor这两种分配策略要复杂很多。

    假设消费组内有3个消费组:C0、C1和C2,他们都订阅了4个主题:t0、t1、t2、t3,并且每个主题有2个分区,也就是说整个消费组订阅 了t0p0、t0p1、t1p0、t1p1、t2p0、t2p1、t3p0、t3p1这8个分区。最终的分配结果如下:

    消费者C0:t0p0、t1p1、t3p0
    消费者C1:t0p1、t2p0、t3p1
    消费者C2:t1p0、t2p1
    

    假设此时消费者C1脱离了消费组,那么消费组就会执行在平衡操作,进而消费分区会重新分配。如果采用RoundRobinAssignor策略,那么此时的分配结果如下:

    消费者C0:t0p0、t1p0、t2p0、t3p0
    消费者C2:t0p1、t1p1、t2p1、t3p1
    

    如分配结果所示,RoundRobinAssignor策略会按照消费者C0和C2进行重新轮询分配。而如果此时使用的时StickyAssignor策略,那么分配结果为:

    消费者C0:t0p0、t1p1、t3p0、t2p0
    消费者C2:t1p0、t2p1、t0p1、t3p1
    

    可以看到分配结果中保留了上一次分配中对于消费者C0和C2的所有分配结果,并将原来消费者C1的“负担”分配给了剩余的两个消费者C0和C2,最终C0和C2的分配还保持了均衡。

  • 自定义分配策略

    需实现:org.apache.kafka.clients.consumer.internals.PartitionAssignor

    继承自:org.apache.kafka.clients.consumer.internals.AbstractPartitionAssignor

    六、kafka存储

1、存储结构概述

image-20201218155214035

  • 每个partion(文件夹)相当于一个巨型文件被平均分配到多个大小相等segment(段)数据文件里。

但每一个段segment file消息数量不一定相等,这样的特性方便old segment file高速被删除。(默认情况下每 一个文件大小为1G)

  • 每一个partition仅仅需要支持顺序读写即可了。Segment文件生命周期由服务端配置参数决定。

partition中segment文件存储结构

segment file组成:由2大部分组成。分别为index file 和data file,此2个文件一一相应,成对出现,后缀".index"和".log"分别标识为segment索引文件、数据文件。

segment文件 命名规则:partion全局的第一个segment从0开始,兴许每一个segment文件名称为上一个segment文件最后一条消息的offset值。

数值最大为64位long大小。19位数字字符长度,没有数字用0填充。

[root@k8s ~]# /tmp/kafka/log/hello-0/

2、日志索引

1、数据文件的分段

kafka解决查询效率的手段之一时将数据文件分段,比如由100调message,它们的offset是从0带99.假设将数据文件分成5段,第一段位0-19,第二段位20-39,以此类推,每段放在一个单独的数据文件里面,数据文件以该段中最小的offset命名。这样在查找指定offset的message的时候,用二分查找就可以定位到该message在哪个段中。

2、偏移量索引

数据文件分段使得可以在一个较小的数据文件中查找对应offset的message了,但是这依然需要顺序扫描才能找到对应offset的message。为了进一步提高查找的效率。kafka为每个分段后的数据文件建立了索引文件,文件名与数据文件的名字是一样的,只是文件扩展名为.index。

image-20201218160710376

比如,要查找绝对offset为7的message:

首先是用二分查找确定它是在哪个LogSegment中,自然是在第一个Segment中。

打开这个segment的index文件,也是用二分查找找到offset小于或者等于指定offset的索引条目中最大的那个offset。自然offset为6的那个索引是我们要找的,通过索引文件我们知道offset为6的message在数据文件中的位置为9807.

打开数据文件,从位置9807的那个地方开始顺序扫描直到找到offset为7的那条message。

这套机制是建立在offset是有序的。索引文件被映射到内存中。索引查找的速度还是很快的。

一句话,kafka的message存储采用了分区(partition),分段(LogSegment)和稀疏索引这几个手段来达到了高效性。

3、日志清理

1、日志删除

kafka日志管理器允许定制删除策略。目前的策略是删除修改时间在N填之前的日志(按时间删除),也可以使用另外一个策略:保留最后的N GB数据的策略(按大小删除)。为了避免在删除时阻塞读操作,采用了copy-on-write形式的实现,删除操作进行时,读取操作的二分查找功能实际时在一个静态的快照副本上进行的,这类似于java的CopyOnWriteArrayList。

kafka消费日志删除思想:kafka把topic中一个partition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用

#启动删除策略 直接删除,删除后的消息不可恢复。
log.cleanup.policy=delete	
#可配置以下两个策略:
#清理超过指定时间清理
log.retention.hours=16
#超过指定大小后,删除旧的消息
log.retention.bytes=1073741824

2、日志压缩

将数据压缩,只保留每个key最后一个版本的数据。首先在broker的配置中设置log.cleaner.enable=true启用cleaner,这个默认时关闭的。在topic的配置中设置log.cleanup.policy=compact启用压缩策略。

image-20201218161557534

压缩后的offset可能时不连续的。比如上图中没有5和7,因为这些offset的消息被merge了,当从这些offset消费消息时,将会拿到比这个offset大的offset对应的消息,比如,当试图获取offset为5的消息时,实际上会拿到offset为6的消息,并从这个位置开始消费。

这种策略只适合特俗场景,比如消息的key时用户ID,消息体时用户的资料,通过这种压缩策略,整个消息集里就保存了所有用户最新的资料。

压缩策略支持删除,当某个key的最新版本的消息没有内容时,这个key将被删除,这也符合以上逻辑。

4、磁盘存储优势

kafka在设计的时候,采用了文件追加的方式来写入消息,即只能在日志文件的尾部追加新的消息,并且不允许修改已经写入的消息,这种方式属于典型的顺序写入此判断的操作,所以就算是kafka使用磁盘作为存储介质,所能实现的吞吐量也非常可观。

kafka中大量使用页缓存,这页时kafka实现高吞吐的重要因素之一。

image-20201218162300591

除了消息顺序追加,页缓存等技术,kafka还是用了零拷贝技术来进一步提升性能。"零拷贝技术"只用将磁盘文件的数据复制到页面缓存中一次,然后将数据从页面缓存直接发送到网络中(发送给不同的订阅者时,都可以使用同一个页面缓存),避免了重复复制操作。如果由10个消费者,传统方式下,数据复制次数为4*10=40次,而使用"零拷贝技术"只需要1+10=11次,一次为磁盘复制到页面缓存,10次标识10个消费者各自读取一次页面缓存。

image-20201218162624698

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值