Kafka—Storm之KafkaSpout和KafkaBolt源码解释

来源:互联网 发布:whisper是什么软件 编辑:程序博客网 时间:2024/06/07 03:34

转载来自:http://blog.csdn.net/ransom0512/article/details/50497261

另一个比较详细的KafkaSpout详解见:http://www.cnblogs.com/cruze/p/4241181.html


Storm-Kafka源代码解析


说明:本文所有代码基于Storm 0.10版本,本文描述内容只涉及KafkaSpout和KafkaBolt相关,不包含trident特性。

Kafka Spout

KafkaSpout的构造函数如下:

public KafkaSpout(SpoutConfig spoutConf) {    _spoutConfig = spoutConf;}
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

其构造参数来自于SpoutConfig对象,Spout中用到的所有参数都来自于该对象。该对象参数说明如下:

SpoutConfig

SpoutConfig继承自KafkaConfig。两个类内部所有参数及说明如下:

/** * Kafka地址和分区关系对应信息 * 在kafka的分区信息和地址信息都很清楚的情况下,可以以直接使用StaticHosts * 但是该对象参数很难构建,需要的信息很多,所以我们一般情况下并不使用它。 * 我们主要用的是ZKHosts的实例。可以在其中设置Zookeeper地址等信息,然后动态获取kafka元数据 * ZKHost的参数信息见下面一段。 * 必选参数 **/public final BrokerHosts hosts;/** * 要从kafka中读取的topic队列名称 * 必选参数 **/public final String topic;/** * Kafka的客户端id参数,该参数一般不需要设置 * 默认值为kafka.api.OffsetRequest.DefaultClientId() * 空字符串 **/public final String clientId;/** * Kafka Consumer每次请求获取的数据量大小 * 每次获取的数据消费完毕之后,才会再获取数据 * 默认1MB **/public int fetchSizeBytes = 1024 * 1024;/** * Kafka SimpleConsumer 客户端和服务端连接的超时时间 * 单位:毫秒 **/public int socketTimeoutMs = 10000;/** * Consumer每次获取数据的超时时间 * 单位:毫秒 **/public int fetchMaxWait = 10000;/** * Consumer通过网络IO获取数据的socket buffet大小, * 默认1MB **/public int bufferSizeBytes = 1024 * 1024;/** * 该参数有两个作用: * 1:申明输出的数据字段 declareoutputFileds * 2:对从kafka中读到的数据进行反序列化,即将byte字节数组转为tuple对象。 * 对kafka存入数据的key和message都比较关心的,可以使用KeyValueSchemeAsMultiScheme, * 如果不关心,可以使用SchemeAsMultiScheme * 默认接口实现一般都只会输出一个字段或者两个字段,很多时候,我们需要直接从kafka中读取到数据之后,就将每个字段解析了,然后进行简单处理再emit * 这个时候,建议自己实现MultiScheme接口 * 必选参数 **/public MultiScheme scheme = new RawMultiScheme();/** * 在拓扑提交之后,KafkaSpout会从zookeeper中读取以前的offset值,以便沿着上次位置继续读取数据。 * KafkaSpout会检查拓扑ID和zookeeper中保存的拓扑id是否相同。 * 如果不同,并且ignoreZkOffsets=true,那么就会从startOffsetTime参数位置读取数据 * 否则,沿着zookeeper中保存的offset位置继续读取数据。 * 也就是说,当ignoreZkOffsets=true的时候,kafkaspout只能保证在拓扑不杀掉的情况下,当worker进程异常退出的时候,会沿着上次读取位置继续读取数据,当拓扑重新提交的时候,就会从队列最早位置开始读取数据。 * 这样就会存在重复读取数据的问题,所以正式场景,该参数还是应该设置为false。以保证任何场景数据的只被读取一次。 **/public boolean ignoreZkOffsets = false;/** * 拓扑第一次提交,zookeeper中没有保存对应offset的情况下,默认从kafka中读取的offset位置。默认从队列最早位置开始读取数据,即从队列最开始位置读取数据。 **/public long startOffsetTime = kafka.api.OffsetRequest.EarliestTime();/** *  * 如果当前的(offset值-failed offsets中最小值) < maxOffsetBehind * 那么就会清理failed列表中所有大于maxOffsetBehind的offset值。 * 这是为了防止failed过多,重发太多导致内存溢出 * 不过默认为了保证数据不丢失,所以maxOffsetBehind设置的最大 **/public long maxOffsetBehind = Long.MAX_VALUE;/** * 当KafkaSpout初始化之后,使用从zookeeper中读取的上次记录的offset * 从kafka中获取数据失败,返回offsetOutofRange错误之后, * 是否使用startOffset从队列最早位置重新获取数据。 * offsetOutofrange一般发生在topic被重建,分片被删除的场景。 **/public boolean useStartOffsetTimeIfOffsetOutOfRange = true;/** * metric监控信息采集间隔 **/public int metricsTimeBucketSizeInSecs = 60;/** * KafkaSpout保存offset的zookeeper所在地址 * 独立出来这个属性是为了防止offset保存位置不在kafka集群中 * 如果kafka和storm在一个集群,该属性可以忽略 **/public List<String> zkServers = null;/** * KafkaSpout保存offset的zookeeper端口 * 如果kafka和storm在一个集群,该属性可以忽略 **/public Integer zkPort = null;/** * offset在zookeeper中保存的路径 * 路径计算方式为:${zkRoot}/${id}/${partitionId} * 必选参数 **/public String zkRoot = null;/** * kafkaSpout保存offset的不同客户端区分标志 * 建议每个拓扑使用固定的,不同的参数,以保证拓扑重新提交之后,可以从上次位置继续读取数据 * 如果两个拓扑公用同一个id,那么可能会被重复读取 * 如果在拓扑中使用了动态生成的uuid来作为id,那么每次提交的拓扑,都会从队列最开始位置读取数据 * 必选参数 **/public String id = null;/** * offset刷新到zookeeper中的时间间隔 * 单位:毫秒 **/public long stateUpdateIntervalMs = 2000;/** * 数据发送失败之后重试策略相关参数 **/public long retryInitialDelayMs = 0;/** * 数据发送失败之后重试策略相关参数 **/public double retryDelayMultiplier = 1.0;/** * 数据发送失败之后重试策略相关参数 **/public long retryDelayMaxMs = 60 * 1000;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126

ZKHost中保存了kafka集群所在的zookeeper地址等信息

ZKHost

/** * kafka集群zookeeper地址,允许包含chroot * 比如:192.168.0.10:2181,192.168.0.11:2181,192.168.0.12:2181/kafka **/public String brokerZkStr = null;/** * kafka集群中broker元数据所在地址 * 默认为/brokers * 如果配置了chroot,那么就是/kafka/brokers * 这个和kakfa服务端配置默认是一样的,如果服务端采用默认配置,该属性也可以使用默认值 **/public String brokerZkPath = null; // e.g., /kafka/brokers/** * kafka broker分区信息刷新时间间隔, * 单位:秒 * 当kafka有broker节点重启或者分区信息发生变化而导致数据读取失败的时候, * 都会重新触发一次分区信息刷新 **/public int refreshFreqSecs = 60;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

KafkaSpout初始化

public void open(Map conf, final TopologyContext context, final SpoutOutputCollector collector) {        _collector = collector;        Map stateConf = new HashMap(conf);        /*         * offset保存位置的zookeeper地址         * 如果该地址为空,则默认使用Storm集群的zookeeper         */        List<String> zkServers = _spoutConfig.zkServers;        if (zkServers == null) {            zkServers = (List<String>) conf.get(Config.STORM_ZOOKEEPER_SERVERS);        }        Integer zkPort = _spoutConfig.zkPort;        if (zkPort == null) {            zkPort = ((Number) conf.get(Config.STORM_ZOOKEEPER_PORT)).intValue();        }        stateConf.put(Config.TRANSACTIONAL_ZOOKEEPER_SERVERS, zkServers);        stateConf.put(Config.TRANSACTIONAL_ZOOKEEPER_PORT, zkPort);        stateConf.put(Config.TRANSACTIONAL_ZOOKEEPER_ROOT, _spoutConfig.zkRoot);        //保存offset信息到zookeeper        _state = new ZkState(stateConf);        //kafka集群的连接器        _connections = new DynamicPartitionConnections(_spoutConfig, KafkaUtils.makeBrokerReader(conf, _spoutConfig));        // using TransactionalState like this is a hack        int totalTasks = context.getComponentTasks(context.getThisComponentId()).size();        if (_spoutConfig.hosts instanceof StaticHosts) {            _coordinator = new StaticCoordinator(_connections, conf, _spoutConfig, _state, context.getThisTaskIndex(), totalTasks, _uuid);        } else {        //从zookeeper中读取kafka的broker信息,只保存自身实例需要用到的分区信息            _coordinator = new ZkCoordinator(_connections, conf, _spoutConfig, _state, context.getThisTaskIndex(), totalTasks, _uuid);        }        //两个metrics监控信息,忽略        context.registerMetric("kafkaOffset", new IMetric() { ...}, _spoutConfig.metricsTimeBucketSizeInSecs);        context.registerMetric("kafkaPartition", new IMetric() {...}, _spoutConfig.metricsTimeBucketSizeInSecs);    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

以上是kafkaSpout的初始化方法,主要是完成对自身管理分区信息的刷新。 
这里有一个问题,就是会创建3个zookeeper客户端连接,一个用来从kafka中读取数据,一个保存offset,一个是metrics监控信息,每个zookeeper客户端连接会创建3个线程,这样,光一个kafkaSpout就会存在9个zookeeper线程!当worker进程中有多个spout实例的时候,就会产生更多的线程,这就会很消耗性能,这个还是建议对zookeeper连接进行合并处理。

系统通过KafkaUtils.calculatePartitionsForTask方法来获取自己需要管理的分区列表:

for (int i = taskIndex; i < numPartitions; i += totalTasks) {            Partition taskPartition = partitions.get(i);            taskPartitions.add(taskPartition);        }
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

其中,taskIndex就对应自身spout实例的序号,比如该spout并发度为3,那么这个spout实例就可能为0,1,2。当kafka的topic有5个分区的时候,第一个spout实例管理0,3的分区;第二个spout实例管理编号为1,4的分区,第三个spout实例管理编号为2的分区。 
taskId保存在Spout的Open方法的context参数中。context.getThisTaskIndex()

KafkaSpout从Kafka中如何读取数据并发送

kafkaSpout主要在nextTuple方法中读取数据并emit。
 public void nextTuple() {        //获取自身实例管理的分区列表        List<PartitionManager> managers = _coordinator.getMyManagedPartitions();        for (int i = 0; i < managers.size(); i++) {            try {                //_currPartitionIndex永远小于manager的大小                // in case the number of managers decreased                _currPartitionIndex = _currPartitionIndex % managers.size();                //获取数据并emit                EmitState state = managers.get(_currPartitionIndex).next(_collector);                /*                 * 检查此次数据发送状态                 * 如果没有取到数据或者取到的数据都已经emit完毕                 * 那么就增加_currPartitionIndex值,然后就可以从下个分区中读取数据了。                 */                if (state != EmitState.EMITTED_MORE_LEFT) {                    _currPartitionIndex = (_currPartitionIndex + 1) % managers.size();                }                /*                 * 如果还有数据没有emit,就退出此次循环,等待下次nexttuple调用                 * 然后仍然从当前分区中取获取数据并emit                 */                if (state != EmitState.NO_EMITTED) {                    break;                }            } catch (FailedFetchException e) {                LOG.warn("Fetch failed", e);                _coordinator.refresh();            }        }        //定期保存offset数据到zookeeper        long now = System.currentTimeMillis();        if ((now - _lastUpdateMs) > _spoutConfig.stateUpdateIntervalMs) {            commit();        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

数据发送状态EmitState一共有三种状态

  • EMITTED_MORE_LEFT 
    上次取到的数据还没有emit完毕
  • EMITTED_END, 
    上次取到的数据已经全部emit完毕
  • NO_EMITTED 
    本次没有取到数据,没有可供emit的数据

再来看下PartitionManager.next方法,里面就包含如何获取数据已经如何emit

public EmitState next(SpoutOutputCollector collector) {        //如果等待发送的队列为空,那么就从kafka中再取一次数据        if (_waitingToEmit.isEmpty()) {            fill();        }        while (true) {        //从等待发送的队列中获取第一个数据            MessageAndRealOffset toEmit = _waitingToEmit.pollFirst();            //如果没有可供发送的数据,那么返回emit状态为没有可以emit的数据            if (toEmit == null) {                return EmitState.NO_EMITTED;            }            //根据KeyValueSchemeAsMultiScheme接口实现,将kafka中取到的数据转为tuple            Iterable<List<Object>> tups = KafkaUtils.generateTuples(_spoutConfig, toEmit.msg);            if (tups != null) {            //发送所有的tuple,因为kafka一条数据可能对应storm的多条                for (List<Object> tup : tups) {                    collector.emit(tup, new KafkaMessageId(_partition, toEmit.offset));                }                break;            } else {            //如果tuple转化失败,返回null,直接告诉storm该条已经处理成功,即忽略数据错误                ack(toEmit.offset);            }        }        /*         * 每次从等待队列中取一条数据反序列化并emit,         * 然后判断等待队列是否还有数据,         * 如果还有数据,就告诉spout,数据还没有发送完,不要切换分区         * 如果数据已经发送完毕,就告诉spout,数据已经发送完毕,可以切换到下个分区了。         */        if (!_waitingToEmit.isEmpty()) {            return EmitState.EMITTED_MORE_LEFT;        } else {            return EmitState.EMITTED_END;        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

当有数据发送失败的时候,失败的数据又会重新加入到_waitingToEmit队列中,这样就会产生一个问题,就是当数据发送失败的时候,kakfaSpout会永远只读一个分区,前天分区都不会读取,从而产生数据消费不均匀的问题。

在0.9.6以前老版本的时候哟一个问题,就是当较多数据emit失败的时候,会有很多的数据在不断重试,然后重试不断超时,又不断重新加入重试列表,从而导致一个数据发送的死循环。这个问题也就是offset超时的问题。见Storm-643, 这个问题目前在最新版本中已经解决。

KafkaBolt

KafkaBolt就比较简单,0.10版本还是使用old Producer API。 
Storm所有的配置属性,都在kafka.broker.properties中保存着,这就要求在submitTopology的时候,在topologyConf中再put一个kafka.broker.properties属性,形成一个map中套map的结构。这样有一点不好的就是一个拓扑中数据只能写到一个kafka集群中,不支持同事写到多个kafka集群中。不过这个在0.11新版本中已经解决了,kafka.broker.properties被作为了一个局部变量,可以在不同的bolt实例中保存不同的配置属性。 
数据写入方法如下:

 public void execute(Tuple input) {        if (TupleUtils.isTick(input)) {          collector.ack(input);          return; // Do not try to send ticks to Kafka        }        K key = null;        V message = null;        String topic = null;        try {            //消息的键值,不同的值在kafka中对应不同的分发方式,这个在KafkaBolt的FAQ中有介绍。            key = mapper.getKeyFromTuple(input);            //消息体            message = mapper.getMessageFromTuple(input);            //topic名称            topic = topicSelector.getTopic(input);            if(topic != null ) {                producer.send(new KeyedMessage<K, V>(topic, key, message));            } else {                LOG.warn("skipping key = " + key + ", topic selector returned null.");            }            collector.ack(input);        } catch (Exception ex) {            collector.reportError(ex);            collector.fail(input);        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

Storm-Kafka FAQ

KafkaSpout

  • KafkaSpout excutor数量和Kafka topic分区数量的关系 
    当executor并发度大于topic数量的时候,就会存在有的spout实例可以读到数据, 有的spout实例读不到数据。 
    当executor并发度小于topic数量的时候,就会存在一个spout实例对应多个分区的情况;kafka会先从一个分区中取一次数据,当这次获取的数据emit完毕之后,就会再从下个分区中取数据。 
    当executor并发度等于topic数量的时候,一个spout实例对应一个分区。在实际应用中,我们也推荐这种配置方式。
  • 如何从kafka中读取数据,每次读取多少数据 
    根据fetchSizeBytes参数的配置,默认每次取1MB数据。
  • 数据读取失败如何处理 
    KafkaSpout每个PartitionManager内部保存一个重试队列,当数据发送失败的时候,加入重试队列,然后重新发送,直到成功为止。 
    通过maxOffsetBehind参数来解决failed数量过多导致内存溢出问题。
  • Topic不存在如何处理 
    直接报错。
  • 拓扑重新提交,会不会接着上次位置继续读取数据 
    重新提交的时候,只要id这个参数不变,那么就会沿着上次位置继续读取数据。
  • zookeeper中保存的kafka的offset位置有错误怎么办? 
    会抛出offsetOutofRange异常,然后默认从kafka分区队列最早位置开始读取数据。
  • 能不能在一个spout中从多个topic读取数据? 
    在0.10版本不行,在0.11版本中,支持按照正则方式匹配topic名称,可以从所有满足正则条件的topic中读取数据。
  • topic分区主备信息发生变化,如何处理 
    抛出异常,然后马上更新分区信息,再次读取数据。

KafkaBolt

  • 写入数据,kafka topic不存在怎么办? 
    如果kakfa服务端允许自动创建topic,那么就会自动创建topic。 
    如果不允许自动创建,那么就会抛出异常
  • 如何写数据到指定分区? 
    取决于tupleToKafkaMapper的接口实现。 
    kafka 0.10版本使用的是old producer的API,0.11版本使用的是new Producer的API 
    对于old Producer 
    如果key == null,那么在kafka中,会随机寸照一个分区去写入数据,之后只要不重启,就都会往这个分区写入数据 
    如果key != null,那么就会在写入数据的时候,以utils.abs(key.hashCode)%numPartitions规则计算分区id 
    对于New Producer 
    如果key = null,那么就会使用一个递增的int值,每次发送数据的时候递增,然后执行utils.abs(nextValue)%availablePartitions.size(),数据写入会比较均衡。 
    如果key != null,那么就会按照Utils.abs(Utils.murmur2(record.key()))%numPartitions的规则计算分区。 
    当然,New Producer API也可以手工指定分区id。
原创粉丝点击