生产者分区策略
1.分区的原因
(1)方便在集群中扩展,每个Partition可以通过调整以适应它所在的机器,而一个topic又可以有多个Partition组成,因此整个集群就可以适应任意大小的数据了;
(2)可以提高并发,因为可以以Partition为单位读写了
2.分区的原则
(1) 指明 partition 的情况下,直接将指明的值直接作为 partiton 值;
(2)没有指明 partition 值但有 key 的情况下,将 key 的 hash 值与 topic 的 partition 数进行取余得到 partition 值;
(3) 既没有 partition 值又没有 key 值的情况下, kafka采用Sticky Partition(黏性分区器),会随机选择一个分区,并尽可能一直使用该分区,待该分区的batch已满或者已完成,kafka再随机一个分区进行使用.(以前是一条条的轮询,现在是一批次的轮询)
3.Kafka生产者3种分区分配策略
1. DefaultPartitioner 默认分区策略
全路径类名:org.apache.kafka.clients.producer.internals.DefaultPartitioner
如果消息中指定了分区,则使用它
如果未指定分区但存在key,则根据序列化key使用murmur2哈希算法对分区数取模。
如果不存在分区或key,则会使用粘性分区策略,关于粘性分区请参阅 KIP-480。
粘性分区Sticky Partitioner
为什么会有粘性分区的概念?
首先,我们指定,Producer在发送消息的时候,会将消息放到一个ProducerBatch中, 这个Batch可能包含多条消息,然后再将Batch打包发送。关于这一块可以看看我之前的文章 图解Kafka Producer 消息缓存模型
消息缓存
这样做的好处就是能够提高吞吐量,减少发起请求的次数。
但是有一个问题就是, 因为消息的发送它必须要你的一个Batch满了或者linger.ms时间到了,才会发送。如果生产的消息比较少的话,迟迟难以让Batch塞满,那么就意味着更高的延迟。
在之前的消息发送中,就将消息轮询到各个分区的, 本来消息就少,你还给所有分区遍历的分配,那么每个ProducerBatch都很难满足条件。
那么假如我先让一个ProducerBatch塞满了之后,再给其他的分区分配是不是可以降低这个延迟呢?
详细的可以看看下面这张图、
这张图的前提是:
Topic1 有3分区, 此时给Topic1 发9条无key的消息, 这9条消息加起来都不超过batch.size .
那么以前的分配方式和粘性分区的分配方式如下
可以看到,使用粘性分区之后,至少是先把一个Batch填满了发送然后再去填充另一个Batch。不至于向之前那样,虽然平均分配了,但是导致一个Batch都没有放满,不能立即发送。这不就增大了延迟了吗(只能通过linger.ms时间到了才发送)
划重点:
当一个Batch发送之后,需要选择一个新的粘性分区的时候
①. 可用分区<1 ;那么选择分区的逻辑是在所有分区中随机选择。
②. 可用分区=1; 那么直接选择这个分区。
③. 可用分区>1 ; 那么在所有可用分区中随机选择。
当选择下一个粘性分区的时候,不是按照分区平均的原则来分配。而是随机原则(当然不能跟上一次的分区相同)
例如刚刚发送到的Batch是 1号分区,等Batch满了,发送之后,新的消息可能会发到2或者3, 如果选择的是2,等2的Batch满了之后,下一次选择的Batch仍旧可能是1,而不是说为了平均,选择3分区。
2.UniformStickyPartitioner 纯粹的粘性分区策略
全路径类名:org.apache.kafka.clients.producer.internals.UniformStickyPartitioner
他跟DefaultPartitioner 分区策略的唯一区别就是。
DefaultPartitionerd 如果有key的话,那么它是按照key来决定分区的,这个时候并不会使用粘性分区
UniformStickyPartitioner 是不管你有没有key, 统一都用粘性分区来分配。
3. RoundRobinPartitioner 分区策略
全路径类名:org.apache.kafka.clients.producer.internals.RoundRobinPartitioner
如果消息中指定了分区,则使用它
将消息平均的分配到每个分区中。
与key无关
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
int numPartitions = partitions.size();
int nextValue = nextValue(topic);
List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
if (!availablePartitions.isEmpty()) {
int part = Utils.toPositive(nextValue) % availablePartitions.size();
return availablePartitions.get(part).partition();
} else {
// no partitions are available, give a non-available partition
return Utils.toPositive(nextValue) % numPartitions;
}
}
上面是具体代码。有个地方需要注意;
当可用分区是0的话,那么就是遍历的是所有分区中的。
当有可用分区的话,那么遍历的是所有可用分区的。
消费者分区策略
Kafka默认的消费逻辑是,位于某个主题中的一个分区只能被同一个消费者组中的一个消费者消费。
当主题分区发生变化时、或有新消费者加入群组时、或群组中有消费者挂掉时,Kafka会触发分区再均衡操作。 那分区再均衡有哪些优缺点呢?分区再平衡的优势在于能为消费者群组带来高可用性与伸缩性;但其缺点在于在发生再均衡这一期间内,消费者是无法读取信息的,所以这将会造成消费者群组会出现一小段时间不可用的情形。所以在应用Kafka的过程中,需要避免无用的分区再均衡发生。
1. RoundRobinAssignor分配策略
本策略的原理是,如果同一个消费者组内所有消费者订阅的主题都是相同的,那么策略的分区分配也是均匀的。假设在如下场景中,群组中的消费者订阅的主题都是相同的,消费者组中存在两个消费者C1、C2都订阅了主题T0、T1,若每个主题中都有3个分区,那么所订阅的所有分区可以标识为:
最终分配的结果为:
下面再看隶属于同一群组中的消费者订阅的主题不同的情况,若群组中内消费者订阅的主题是不同的,那么在执行分区分配的时候就不是完全的轮询分配了,会出现分区分配不均匀的情况。此处需要注意的是,如果某个消费者没有订阅某个Topic,那么在分区分配时,此消费者是分配不到这个Topic中的任何分区的。假设有如下场景,消费者组内有3个消费者C1、C2、C3,共同订阅了三个主题T0、T1、T2,若每个主题中分别有1、2、3个分区,其中消费者C1订阅了主题T0,消费者C2订阅了主题T0、T1,消费者C3订阅了主题T0、T1、T2,那么订阅的所有分区可以标识为:
最终分配的结果为:
之所以会产生这样的分配结果,是因为由于消费者C1只订阅了T0主题,那么分区T0P0会被分配给C1;消费者C2订阅了主题T0、T1,但由于T0中的分区已经被C1消费了,那么分区T1P0会被分配给消费者C2;已知T1分区同时被消费者C2、C3订阅,那么分区T1P1会分配给消费者C3;T2主题仅被消费者C3订阅了,所以T2主题中的所有分区都只能交给消费者C3消费。
2. RangeAssignor分配策略
本策略是以主题为基础的,在主题中根据数字编号对可用的分区排序,在消费者组中将消费者根据字典顺序排列。分区分配的依据是采用Topic中的分区总数除以消费者总数,来确定分配给每个消费者的分区个数,需要注意的是若不能平均分配的话,额外的分区将会分配给前几个消费者。假设有如下场景,消费者C1、C2都订阅了主题T0和T1且每个主题都有四个分区,那么要订阅的所有分区的标识为:
最终分配的结果为:
之所以产生这样的分配结果,是因为群组中存在消费者C1、C2,已知T0、T1主题中都存在四个分区,在这两个主题内部分别用分区数除以消费者数,最终每个消费者都将从主题中取出两个分区订阅。这样的分配是很均匀的,那么此分配策略是否能一直保持如此均匀的分配呢,我们再看另外一种情况,也就是分区数和消费者数不能整除的情况。假设有如下场景,群组中存在消费者C1、C2,有两个主题T0、T1其中都有3个分区,那么要订阅的所有分区标识为:
当不能整除时,前面的消费者会分配到额外的分区,那么此时的分配结果为:
综上可见这样的分配其实是不均匀的,如果类似的情况不断扩大则会出现部分消费者过载的情况。
3. StickyAssignor分区分配策略
本策略有两个目标, 首先是要实现分区分配要尽可能地均匀,其次当发生分区再平衡发生时,分区的分配会尽可能的与上次的分配结果保持一致,目的是为了防止分区的消费者发生变化,这有助于节约开销,也有助于避免消息重复消费的问题发生。需要注意的是,当以上两点发生冲突的时候,第一个目标是优先于第二个目标的。假设有如下场景,有三个消费者C1、C2、C3,它们都订阅了4个主题T0、T1、T2、T3且每个主题有2个分区,那么要订阅的所有分区的标识为:
最终的分配结果如下:
这样的分配结果似乎看上去与RoundRobinAssignor策略分配的结果相同,现假设此时消费者C2脱离了群组,那么群组将会执行分区再平衡操作,若采用的是RoundRobinAssignor策略,那么分配结果将是:
上述的分配结果中保留了上一次分配中对于消费者C1和C3的所有分配结果,并将原来消费者C2中分配的分区转移到了C1和C3中,最终消费者C1和C3的分区分配保持了均衡。若发生了分区再平衡,对于同一个分区而言,可能会出现之前的消费者和新指派的消费者不是同一个的情况,这就会导致消息的重复消费和浪费系统资源的情况。StickyAssignor策略如同其名字中的单词"sticky"一样,让分配的策略具备一定"粘性",以实现尽可能的让两次分配的结果是相同的,进而减少系统资源的损耗和其他异常情况的发生。
下面看一下隶属于同一群组中的消费者订阅的主题不相同时分区分配策略结果是怎样的。假设有如下场景,群组内有3个消费者C1、C2、C3同时有三个主题T0、T1、T2,这三个主题分别有1、2、3个分区,消费者C1订阅了主题T0,消费者C2订阅了主题T0、T1,消费者C3订阅了主题T0、T1、T2,那么所要订阅的所有分区标识为:
如果此时采用RoundRobinAssignor策略,那么最终的分配结果如下:
若此时消费者C1脱离了群组,那么RoundRobinAssignor策略的分配结果为:
若采用的是StickyAssignor策略,那么最终的分配结果为:
若此时消费者C1离开了群组,那么StickyAssignor策略分配的结果是:
以上的分区分配结果较为理想,体现出了StickyAssignor策略所具有的两大特点,一是分区要尽可能的均衡,二是在发生分区再平衡时,分区的分配会尽可能的与上次的分配结果保持相同。
https://www.modb.pro/db/242469
https://di1shuai.com/Kafka%E2%80%94%E2%80%94%E5%88%86%E5%8C%BA%E7%AD%96%E7%95%A5.html#DefaultPartitioner-%E9%BB%98%E8%AE%A4%E5%88%86%E5%8C%BA%E5%99%A8
https://developer.aliyun.com/article/890329
https://qinjl.blog.csdn.net/article/details/108013204?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.pc_relevant_default&utm_relevant_index=2