8. kafka分区

分区策略

构造KafkaProducer代码如下:

Properties props = new Properties();
props.put("bootstrap.servers", "10.0.55.229:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

// 定义分区策略(默认实现: org.apache.kafka.clients.producer.internals.DefaultPartitioner)
props.put("partitioner.class", "com.afei.kafka.KafkaCustomPartitioner");
kafkaProducer = new KafkaProducer<>(props);

属性partitioner.class就是决定消息如何分区的,默认实现类是DefaultPartitioner,源码注释如下:

The default partitioning strategy:
If a partition is specified in the record, use it;
If no partition is specified but a key is present choose a partition based on a hash of the key;
If no partition or key is present choose a partition in a round-robin fashion;

源码分析

在调用send()方法发送消息时,会调用如下代码选择分区:

int partition = partition(record, serializedKey, serializedValue, cluster);

partition()方法源码如下:

/**
 * computes partition for given record.
 * if the record has partition returns the value otherwise
 * calls configured partitioner class to compute the partition.
 */
private int partition(ProducerRecord<K, V> record, byte[] serializedKey, byte[] serializedValue, Cluster cluster) {
    Integer partition = record.partition();
    // 如果构造的ProducerRecord指定了partition,则发送到指定的分区;否则调用partitioner选择一个分区。(如果通过参数partitioner.class指定了自定义分区实现则用其选择分区,否则用默认的DefaultPartitioner选择分区)
    return partition != null ?
            partition :
            partitioner.partition(
                    record.topic(), record.key(), serializedKey, record.value(), serializedValue, cluster);
}

DefaultPartitioner即默认分区选取策略的源码如下:


public class DefaultPartitioner implements Partitioner {

    private final ConcurrentMap<String, AtomicInteger> topicCounterMap = new ConcurrentHashMap<>();

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

    /**
     * Compute the partition for the given record.
     *
     * @param topic The topic name
     * @param key The key to partition on (or null if no key)
     * @param keyBytes serialized key to partition on (or null if no key)
     * @param value The value to partition on or null
     * @param valueBytes serialized value to partition on or null
     * @param cluster The current cluster metadata
     */
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        // 得到topic所有的分区信息
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        // 得到topic分区数量
        int numPartitions = partitions.size();
        // 如果发送的消息ProducerRecord没有key的话
        if (keyBytes == null) {
            // 得到topic对应的值,由值的计算方式(counter.getAndIncrement())可知,每个topic下每个客户端采取轮询策略往分区中写入消息
            int nextValue = nextValue(topic);
            List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
            // 如果有效的分区集合不为空,那么轮询有效的分区写入消息(即有效分区集合优先原则)
            if (availablePartitions.size() > 0) {
                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;
            }
        } else {
            // 如果发送的消息ProducerRecord有key的话, 根据key的murmur2 hash结果对分区数量取模就是最终选择的分区(所以如果想让消息有序,只需给消息指定相同的key即可,有相同的key,就会有相同的hash值,就会选择相同的分区,同一个分区的消息又是有序的)
            // hash the keyBytes to choose a partition
            return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
        }
    }

    private int nextValue(String topic) {
        // 每个topic对应不同的counter
        AtomicInteger counter = topicCounterMap.get(topic);
        if (null == counter) {
            // 如果topic还从没没发过消息,或者客户端重启,那么counter为空,那么给这个topic初始化一个AtomicInteger
            counter = new AtomicInteger(ThreadLocalRandom.current().nextInt());
            AtomicInteger currentCounter = topicCounterMap.putIfAbsent(topic, counter);
            if (currentCounter != null) {
                counter = currentCounter;
            }
        }
        return counter.getAndIncrement();
    }

    @Override
    public void close() {}

}

通过设置相同key来保证消息有序性,这里可能还会有一点小小的缺陷。例如消息发送设置了重试机制,并且异步发送,消息A和B设置相同的key,业务上A先发,B后发。由于网络或者其他原因A发送失败,B发送成功;A由于发送失败就会重试且重试成功,这时候消息顺序B在前A在后,与业务发送顺序不一致。如果需要解决这个问题,需要设置参数max.in.flight.requests.per.connection=1,其含义是限制客户端在单个连接上能够发送的未响应请求的个数。设置此值是1表示kafka broker在响应请求之前client不能再向同一个broker发送请求,这个参数默认值是5。官方文档说明如下,这个参数如果大于1,由于重试消息顺序可能重排:

The maximum number of unacknowledged requests the client will send on a single connection before blocking. Note that if this setting is set to be greater than 1 and there are failed sends, there is a risk of message re-ordering due to retries (i.e., if retries are enabled).

自定义

如过要自定义分区实现非常简单,只需要自定义类实现接口org.apache.kafka.clients.producer.Partitioner,并作如下配置即可:

props.put("partitioner.class", "com.afei.kafka.KafkaCustomPartitioner");

KafkaCustomPartitioner就是自定义实现类,假定分区策略如下:

分区要求至少5个,如果key以vip开头,表示是重要消息,重要消息在除了最后两个分区之外的分区中遍历寻找分区并写入;否则,表示是一般消息,一般消息只在最后两个分区中遍历寻找分区并写入;

分区实现的核心源码如下:

/**
 * @author afei
 * @version 1.0.0
 * @since 2018年06月18日
 */
public class KafkaCustomPartitioner implements Partitioner{

    /**
     * 保存普通消息topic与其对应的编号(每次取编号时需要自增),通过编号能够得到目标分区
     */
    private final ConcurrentMap<String, AtomicInteger> normalTopicCounterMap = new ConcurrentHashMap<>();
    /**
     * 保存重要消息topic与其对应的编号(每次取编号时需要自增),通过编号能够得到目标分区
     */
    private final ConcurrentMap<String, AtomicInteger> importTopicCounterMap = new ConcurrentHashMap<>();

    private static final int MIN_PARTITION_NUM = 5;

    @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();
        // MIN_PARTITION_NUM即需求定义的要求的最小的分区数5
        if (numPartitions< MIN_PARTITION_NUM){
            throw new IllegalStateException("The num of partition must be greater than "+ MIN_PARTITION_NUM);
        }

        int index;
        boolean vipMsg = keyBytes != null && new String(keyBytes).startsWith("vip");
        int nextValue = this.nextValue(topic, vipMsg);
        if (vipMsg) {
            // 重要消息在除了最后两个分区之外的分区中遍历寻找分区并写入
            index = Utils.toPositive(nextValue) % (numPartitions-2);
        } else {
            // 一般消息只在最后两个分区中遍历寻找分区并写入
            index = Utils.toPositive(nextValue) % 2 + (numPartitions-2);
        }
        System.out.println("topic = "+topic+" ,index = "+index+" ,vip?"+vipMsg+", key = "+new String(keyBytes));
        return index;
    }

    private int nextValue(String topic, boolean vip) {
        AtomicInteger counter = vip? importTopicCounterMap.get(topic): normalTopicCounterMap.get(topic);
        if (null == counter) {
            counter = new AtomicInteger(0);
            AtomicInteger currentCounter = vip? importTopicCounterMap.putIfAbsent(topic, counter): normalTopicCounterMap.putIfAbsent(topic, counter);
            if (currentCounter != null) {
                counter = currentCounter;
            }
        }
        return counter.getAndIncrement();
    }

    @Override
    public void close() {

    }

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

    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • Kafka入门经典教程-Kafka-about云开发 http://www.aboutyun.com/threa...
    葡萄喃喃呓语阅读 10,748评论 4 54
  • kafka的定义:是一个分布式消息系统,由LinkedIn使用Scala编写,用作LinkedIn的活动流(Act...
    时待吾阅读 5,234评论 1 15
  • 姓名:周小蓬 16019110037 转载自:http://blog.csdn.net/YChenFeng/art...
    aeytifiw阅读 34,524评论 13 425
  • 妈妈,你知道吗?最近我很烦。一方面是来自于你们无微不至的关心。另一方面也是对你们的不满。最近你老提关于结婚的话题,...
    小坤爸阅读 113评论 0 0