Kafka 生产者与消费者分区策略全解析:从原理到实践

一、生产者分区策略

1.1 分区好处

(1)便于合理使用存储资源,每个Partition在一个Broker上存储,可以把海量的数据按照分区切割成一块一块数据存储在多台Broker上。合理控制分区的任务,可以实现负载均衡的效果。

(2)提高并行度,生产者可以以分区为单位发送数据;消费者可以以分区为单位进行消费数据。

1.2 生产者发送消息的分区策略

1 )默认的分区器 DefaultPartitioner

在 IDEA 中 ctrl +n,全局查找 DefaultPartitioner。

复制代码
/**
* The default partitioning strategy:
* <ul>
* <li>If a partition is specified in the record, use it
    假如发送消息的时候指定分区,就使用这个分区
* <li>If no partition is specified but a key is present choose a 
partition based on a hash of the key
    假如发送消息没有指定分区,指定了Key值,对Key进行hash,然后对分区数取模,得到哪个分区就使用哪个分区
* <li>If no partition or key is present choose the sticky 
partition(粘性分区) that changes when the batch is full.
   假如分区和key值都没有指定,使用粘性分区(黏住它,使用它,发送完毕为止)
* 
* See KIP-480 for details about sticky partitioning.
*/
public class DefaultPartitioner implements Partitioner {
 ... ...
}

例如:第一次随机选择0号分区,等0号分区当前批次满了(默认16k)或者linger.ms设置的时间到, Kafka再随机一个分区进行使用(如果还是0会继续随机)。

2 )案例一

将数据发往指定 partition 的情况下,例如,将所有数据发往分区 1 中。

测试:

①在 node01 上开启 Kafka 消费者。

复制代码
kafka-console-consumer.sh --bootstrap-server node01:9092 --topic first

②在 IDEA 中执行代码,观察 bigdata01 控制台中是否接收到消息。

复制代码
bin/kafka-console-consumer.sh --bootstrap-server node01:9092 --topic first

③在 IDEA 控制台观察回调信息。

复制代码
主题:first->分区:0
主题:first->分区:0 
主题:first->分区:0 
主题:first->分区:0 
主题:first->分区:0

3 )案例二

没有指明 partition 值但有 key 的情况下,将 key 的 hash 值与 topic 的 partition 数进行取余得到 partition 值。

测试:

①key="a"时,在控制台查看结果。

主题:first->分区:1

主题:first->分区:1

主题:first->分区:1

主题:first->分区:1

主题:first->分区:1

②key="b"时,在控制台查看结果。

主题:first->分区:2

主题:first->分区:2

主题:first->分区:2

主题:first->分区:2

主题:first->分区:2

③key="f"时,在控制台查看结果。

主题:first->分区:0

主题:first->分区:0

主题:first->分区:0

主题:first->分区:0

主题:first->分区:0

1.3 自定义分区器

如果研发人员可以根据企业需求,自己重新实现分区器。

1 )需求

例如我们实现一个分区器实现,发送过来的数据中如果包含 bigdata,就发往 0 号分区, 不包含bigdata,就发往 1 号分区。

2 )实现步骤

(1)定义类实现 Partitioner 接口。

(2)重写 partition()方法

复制代码
package com.bigdata.partitioner;

import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;

import java.util.Map;

public class MyPartitioner implements Partitioner {

    /**
     * 返回信息对应的分区
     * @param topic 主题
     * @param key 消息的 key
     * @param keyBytes 消息的 key 序列化后的字节数组
     * @param value 消息的 value
     * @param valueBytes 消息的 value 序列化后的字节数组
     * @param cluster 集群元数据可以查看分区信息
     * @return
     */
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        // 假如消息中含有bigdata 发送0分区,否则发送1分区
        String msg = new String(valueBytes);
        if(msg.contains("bigdata")){
            return 0;
        }
        return 1;
    }

    @Override
    public void close() {

    }

    @Override
    public void configure(Map<String, ?> configs) {

    }
}

(3)使用分区器的方法,在生产者的配置中添加分区器参数

复制代码
package com.bigdata.producer;

import org.apache.kafka.clients.producer.*;

import java.util.Properties;
import java.util.concurrent.ExecutionException;

public class CustomProducer06 {


    public static void main(String[] args) throws InterruptedException, ExecutionException {
        Properties properties = new Properties();
        // 设置连接kafka集群的ip和端口

        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringSerializer");
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringSerializer");
        properties.put("bootstrap.servers", "bigdata01:9092");
        properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.bigdata.partitioner.MyPartitioner");

        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);

        for (int i = 0; i < 10; i++) {
            ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>(
                    "second","abc",
                    "加油!:"+i
            );
            kafkaProducer.send(producerRecord, new Callback() {
                @Override
                public void onCompletion(RecordMetadata metadata, Exception exception) {
                    System.out.printf("消息发送给了%d分区\n",(metadata.partition()));

                }
            });
        }

        kafkaProducer.close();
    }
}

4)测试

测试按照以下几种情况:

第一种:使用了自定义分区器,并且指定分区发送

第二种:使用了自定义分区器,并且发送的时候带有 key 值

第三种:使用了自定义分区器,没有指定分区和 key

每一种测试时消息发送带有 bigdata 的,再 换成不含 bigdata 的。

①在 bigdata01 上开启 Kafka 消费者。

复制代码
kafka-console-consumer.sh --bootstrap-server bigdata01:9092 --topic first 

②在 IDEA 控制台观察回调信息。

主题:first->分区:0

主题:first->分区:0

主题:first->分区:0

主题:first->分区:0

主题:first->分区:0
注意:假如我自定义了一个分区规则,如果代码中指定了消息发送到某个分区,自定义的分区规则无效。

比如:我自定义了一个分区器,包含 bigdata 发送 0 分区,不包含发送 1 分区,但假如发送消息的时候指定消息发送到 2 分区,那么消息就必然发送 2 分区。不走咱们自定义的分区器规则了。

如果没有指定分区规则,指定了 key 值,那么依然走我们的自定义分区器,不走默认。

二、消费者分区策略

1、一个consumer group中有多个consumer组成,一个 topic有多个partition组成,现在的问题是,到底由哪个consumer来消费哪个partition的数据。

2、Kafka有四种主流的分区分配策略: Range、RoundRobin(轮询)、Sticky(粘性)、CooperativeSticky(配合的粘性)。

可以通过配置参数partition.assignment.strategy,修改分区的分配策略。默认策略是Range + CooperativeSticky

|-------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------|
| 参数名称 | 描述 |
| heartbeat.interval.ms | Kafka 消费者和 coordinator 之间的心跳时间,默认 3s。 该条目的值必须小于session.timeout.ms,也不应该高于 session.timeout.ms 的 1/3。 |
| session.timeout.ms | Kafka 消费者和 coordinator 之间连接超时时间,默认 45s。超 过该值,该消费者被移除,消费者组执行再平衡。 |
| max.poll.interval.ms | 消费者处理消息的最大时长,默认是 5 分钟。超过该值,该 消费者被移除,消费者组执行再平衡 |
| partition.assignment.strategy | 消 费 者 分 区 分 配 策 略 , 默 认 策 略 是 Range +CooperativeSticky。Kafka 可以同时使用多个分区分配策略。 可 以 选 择 的 策 略 包 括 : Range 、 RoundRobin 、 Sticky 、CooperativeSticky |

2.1 Range 以及再平衡

1)Range 分区策略原理

Range 是对每个 topic 而言的。

首先对同一个topic里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序。

假如现在有7个分区,3个消费者,排序后的分区将会是0,1,2,3,4,5,6;消费者排序完之后将会是C0,C1,C2。

通过partitions数/consumer数来决定每个消费者应该消费几个分区。如果除不尽,那么前面几个消费者将会多

消费1个分区,

例如,7/3=2余1,除不尽,那么消费者C0便会多消费1个分区。8/3=2余2,除不尽,那么C0和C1分别多

消费一个。

注意:如果只是针对1个topic而言,C0消费者多消费1个分区影响不是很大。但是如果有N多个topic,那么针对每个 topic,消费者C0都将多消费1个分区,topic越多,C0消费的分区会比其他消费者明显多消费N个分区。

容易产生数据倾斜!

2)Range 分区分配策略案例

(1)修改主题 first 为 7 个分区。

bin/kafka-topics.sh --bootstrap-server bigdata01:9092 --alter --topic first --partitions 7

2)这样可以由三个消费者

CustomConsumer、CustomConsumer1、CustomConsumer2 组成消费者组,组名都为"test", 同时启动 3 个消费者。

(3)启动生产者,发送 500 条消息,随机发送到不同的分区。

注意:分区数可以增加,但是不能减少。

一个主题,假如副本数想修改,是否可以直接修改?答案是不可以。

如果想修改,如何修改?制定计划,执行计划
Kafka 默认的分区分配策略就是 Range + CooperativeSticky,所以不需要修改策略。

默认是Range,但是在经过一次升级之后,会自动变为CooperativeSticky。这个是官方给出的解释。

默认的分配器是[RangeAssignor, CooperativeStickyAssignor],默认情况下将使用RangeAssignor,但允许通过一次滚动反弹升级到CooperativeStickyAssignor,该滚动反弹会将RangeAssignor从列表中删除。

(4)观看 3 个消费者分别消费哪些分区的数据。

假如消费情况和预想的不一样:

1、集群是否健康,比如某些kafka进程没启动

2、发送数据的时候7个分区没有使用完,因为它使用了粘性分区。如何让它发送给7个分区呢,代码中添加:

// 延迟一会会看到数据发往不同分区

Thread.sleep(20);

发现一个消费者消费了,5,6分区,一个消费了0,1,2分区,一个消费了3,4分区。

此时并没有修改分区策略,原因是默认是Range.

3 Range 分区分配再平衡案例

(1)停止掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)。

1 号消费者:消费到 3、4 号分区数据。

2 号消费者:消费到 5、6 号分区数据。

0号的数据,没人消费。

说明:0 号消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,所以需

要等待,时间到了 45s 后,判断它真的退出就会把任务分配给其他 broker 执行。

(2)再次重新发送消息观看结果(45s 以后)。

1 号消费者:消费到 0、1、2、3 号分区数据。

2 号消费者:消费到 4、5、6 号分区数据。

说明:消费者 0 已经被踢出消费者组,所以重新按照 range 方式分配。

2.2 RoundRobin(轮询) 以及再平衡

1)RoundRobin 分区策略原理

RoundRobin针对集群中所有Topic而言。

RoundRobin轮询分区策略,是把所有的partition和所有的consumer都列出来,然后按照hashcode进行排序,最后

通过轮询算法来分配partition给到各个消费者。

2 RoundRobin 分区分配策略案例

(1)依次在 CustomConsumer、CustomConsumer1、CustomConsumer2 三个消费者代 码中修改分区分配策略为 RoundRobin。

复制代码
轮询的类的全路径是:
org.apache.kafka.clients.consumer.RoundRobinAssignor

A list of class names or class types, ordered by preference, of supported partition assignment strategies that the client will use to distribute partition ownership amongst consumer instances when group management is used. Available options are:

org.apache.kafka.clients.consumer.RangeAssignor: Assigns partitions on a per-topic basis.
org.apache.kafka.clients.consumer.RoundRobinAssignor: Assigns partitions to consumers in a round-robin fashion.
org.apache.kafka.clients.consumer.StickyAssignor: Guarantees an assignment that is maximally balanced while preserving as many existing partition assignments as possible.
org.apache.kafka.clients.consumer.CooperativeStickyAssignor: Follows the same StickyAssignor logic, but allows for cooperative rebalancing.

3 RoundRobin 分区分配再平衡案例

(1)停止掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)。

1 号消费者:消费到 2、5 号分区数据

2 号消费者:消费到 4、1 号分区数据

0 号消费者 以前对应的数据没有人消费

说明:0 号消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,所以需要等待,时间到了 45s 后,判断它真的退出就会把任务分配给其他 broker 执行。

(2)再次重新发送消息观看结果(45s 以后)。

1 号消费者:消费到 0、2、4、6 号分区数据

2 号消费者:消费到 1、3、5 号分区数据

说明:消费者 0 已经被踢出消费者组,所以重新按照 RoundRobin 方式分配。

2.3 Sticky 以及再平衡

**粘性分区定义:**可以理解为分配的结果带有"粘性的"。即在执行一次新的分配之前, 考虑上一次分配的结果,尽量少的调整分配的变动,可以节省大量的开销。 粘性分区是 Kafka 从 0.11.x 版本开始引入这种分配策略,首先会尽量均衡的放置分区 到消费者上面,在出现同一消费者组内消费者出现问题的时候,会尽量保持原有分配的分区不变化。
比如分区有 0 1 2 3 4 5 6

消费者有 c1 c2 c3

c1 消费 3个 c2 消费2个 c3 消费2个分区

跟以前不一样的是,c1 消费的3个分区是随机的,不是按照 0 1 2 这样的顺序来的。

1)需求

设置主题为 first,7 个分区;准备 3 个消费者,采用粘性分区策略,并进行消费,观察

消费分配情况。然后再停止其中一个消费者,再次观察消费分配情况。

2)步骤

(1)修改分区分配策略为粘性。

注意:3 个消费者都应该注释掉,之后重启 3 个消费者,如果出现报错,全部停止等

会再重启,或者修改为全新的消费者组。

复制代码
// 修改分区分配策略
ArrayList<String> startegys = new ArrayList<>();
startegys.add("org.apache.kafka.clients.consumer.StickyAssignor");
properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, startegys);

(2)使用同样的生产者发送 500 条消息。

可以看到会尽量保持分区的个数近似划分分区。

3 Sticky 分区分配再平衡案例

(1)停止掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)。

1 号消费者:消费到 2、5、3 号分区数据。

2 号消费者:消费到 4、6 号分区数据。

0 号消费者的任务没人顶替它消费

说明:0 号消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,所以需

要等待,时间到了 45s 后,判断它真的退出就会把任务分配给其他 broker 执行。

(2)再次重新发送消息观看结果(45s 以后)。

1 号消费者:消费到 2、3、5 号分区数据。

2 号消费者:消费到 0、1、4、6 号分区数据。

说明:消费者 0 已经被踢出消费者组,所以重新按照粘性方式分配。

2.4 CooperativeSticky 的解释【新的kafka中刚添加的策略】

在消费过程中,会根据消费的偏移量情况进行重新再平衡,也就是粘性分区,运行过程中还会根据消费的实际情况重新分配消费者,直到平衡为止。

好处是:负载均衡,不好的地方是:多次平衡浪费性能。

动态平衡,在消费过程中,实施再平衡,而不是定下来,等某个消费者退出再平衡。

相关推荐
会飞的架狗师4 小时前
【Kafka系列】第二篇| Kafka 的核心概念、架构设计、底层原理
分布式·kafka
lifallen5 小时前
Hadoop MapReduce过程
大数据·数据结构·hadoop·分布式·apache
婷儿z12 小时前
部署 Zabbix 企业级分布式监控
分布式·zabbix
abigalexy14 小时前
百万并发QPS-分布式架构设计
分布式·架构
ClouGence14 小时前
构建秒级响应的实时数据架构
数据库·kafka
武子康14 小时前
大数据-64 Kafka 深入理解 Kafka 分区与重分配机制:高并发与高可用的核心 实机测试
大数据·后端·kafka
@Jackasher16 小时前
Redis如何实现一个分布式锁?
redis·分布式·wpf
hqxstudying1 天前
java分布式定时任务
java·开发语言·分布式
前端世界1 天前
鸿蒙分布式任务调度深度剖析:跨设备并行计算的最佳实践
分布式·华为·harmonyos