KafKa生产者-分区

  生产者(producer)采用推(push)模式将消息发布到broker,每条消息都被追加(append)到分区(patition)中,属于顺序写磁盘(顺序写磁盘效率比随机写内存要高,保障kafka吞吐率)。消息发送时都被发送到一个topic,其本质就是一个目录,而topic是由一些Partition Logs(分区日志)组成,其组织结构如下图所示:

  

 

  可以看出,每个Partition中的消息都是有序的,生产的消息被不断追加到Partition log上,其中的每一个消息都被赋予了一个唯一的offset值。每个分区内部也独立地维护了一个从0开始的offset值,offset只保证区内有序,即生产顺序和消费顺序一致。

  1、分区的原因

    (1)方便在集群中扩展,每个Partition可以通过调整以适应它所在的机器,而一个topic又可以有多个Partition组成,因此整个集群就可以适应任意大小的数据了;

    (2)可以提高并发度,因为可以以Partition为单位读写了。

  2、分区的方式

    生产者向broker发送数据,需要将要发送的数据封装成一个ProducerRecord对象,ProducerRecord这个类含有多个重载的构造方法,每一种构造方法都有不同的参数,也就代表了不同的分区方法。

 

 

     通过观察源码发现,这几种构造方法最终执行的都是上图的第一个构造方法,只不过在各自方法内部调用时,自身没有包含的参数在调用第一个构造方法的时候都设置为了null,这些重载的含有不同参数的构造方法调用第一个方法,第一个方法会根据传进来的参数值为成员变量赋值,没有值的则赋值为null,我们来看一下第一个构造方法的源码:

    public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value, Iterable<Header> headers) {
        if (topic == null)
            throw new IllegalArgumentException("Topic cannot be null.");
        if (timestamp != null && timestamp < 0)
            throw new IllegalArgumentException(
                    String.format("Invalid timestamp: %d. Timestamp should always be non-negative or null.", timestamp));
        if (partition != null && partition < 0)
            throw new IllegalArgumentException(
                    String.format("Invalid partition: %d. Partition number should always be non-negative or null.", partition));
        this.topic = topic;
        this.partition = partition;
        this.key = key;
        this.value = value;
        this.timestamp = timestamp;
        this.headers = new RecordHeaders(headers);
    }

  由代码可知,该方法对ProducerRecord对象做一个初始化的处理,将各个参数赋值给对应的成员变量,其中有的成员变量可能会被赋值为null。这些信息既有消息的的元数据,也有实际要发布的数据。会通过一个生产者对象,调用send方法发送。下面是KafkaProducer的send方法的源码:

  send方法是通过异步的方式将record发送到主题的,其最终调用的也是第二个send方法

    /**
     * Asynchronously send a record to a topic. Equivalent to <code>send(record, null)</code>.
     * See {@link #send(ProducerRecord, Callback)} for details.
     */
    @Override
    public Future<RecordMetadata> send(ProducerRecord<K, V> record) {
        return send(record, null);
    }

    ……

    @Override
    public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
        // intercept the record, which can be potentially modified; this method does not throw exceptions
        ProducerRecord<K, V> interceptedRecord = this.interceptors == null ? record : this.interceptors.onSend(record);
        return doSend(interceptedRecord, callback);
    }

 

  观察第二个send方法发现,它会先判断是否有拦截器对象,如果没有就直接返回doSend方法的返回值,如果有拦截器对象则会去执行拦截器对象的onSend方法。这里继续观察探索doSend方法,这个方法是异步发送消息的主题的具体实现:

  可看出该方法会依次将key和value进行序列化操作,然后计算一下分区,然后根据record的主题和partition函数返回的分区构建一个TopicPartition对象,最终这个对象会和时间戳信息、序列化之后的key、value等信息一起追加到accumulator这个线程共享变量中,等待sender线程将accumulator中的消息发送给broker

 

 private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
        TopicPartition tp = null;
        try {
         ……
            byte[] serializedKey;
            try {
                serializedKey = keySerializer.serialize(record.topic(), record.headers(), record.key());
            } catch (ClassCastException cce) {
                throw new SerializationException("Can't convert key of class " + record.key().getClass().getName() +
                        " to class " + producerConfig.getClass(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG).getName() +
                        " specified in key.serializer");
            }
            byte[] serializedValue;
            try {
                serializedValue = valueSerializer.serialize(record.topic(), record.headers(), record.value());
            } catch (ClassCastException cce) {
                throw new SerializationException("Can't convert value of class " + record.value().getClass().getName() +
                        " to class " + producerConfig.getClass(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG).getName() +
                        " specified in value.serializer");
            }
            int partition = partition(record, serializedKey, serializedValue, cluster);
            tp = new TopicPartition(record.topic(), partition);

            setReadOnly(record.headers());
            Header[] headers = record.headers().toArray();

            int serializedSize = AbstractRecords.estimateSizeInBytesUpperBound(apiVersions.maxUsableProduceMagic(),
                    compressionType, serializedKey, serializedValue, headers);
            ensureValidRecordSize(serializedSize);
            long timestamp = record.timestamp() == null ? time.milliseconds() : record.timestamp();
            log.trace("Sending record {} with callback {} to topic {} partition {}", record, callback, record.topic(), partition);
            // producer callback will make sure to call both 'callback' and interceptor callback
            Callback interceptCallback = this.interceptors == null ? callback : new InterceptorCallback<>(callback, this.interceptors, tp);

            if (transactionManager != null && transactionManager.isTransactional())
                transactionManager.maybeAddPartitionToTransaction(tp);

            RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey,
                    serializedValue, headers, interceptCallback, remainingWaitMs);
            if (result.batchIsFull || result.newBatchCreated) {
                log.trace("Waking up the sender since topic {} partition {} is either full or getting a new batch", record.topic(), partition);
                this.sender.wakeup();
            }
            return result.future;
            // handling exceptions and record the errors;
            // for API exceptions return them in the future,
            // for other exceptions throw directly
        } catch(){
         ……
    }
    }        

  进一步分析partition函数,这个函数用于计算给定的record的分区,如果record的分区给定了,那么就直接返回给定的分区值,如果没有则会调用已经配置好的分区类去计算分区。

    private int partition(ProducerRecord<K, V> record, byte[] serializedKey, byte[] serializedValue, Cluster cluster) {
        Integer partition = record.partition();
        return partition != null ?
                partition :
                partitioner.partition(
                        record.topic(), record.key(), serializedKey, record.value(), serializedValue, cluster);
    }

  直接进一步分析分区类是如何计算分区的,源码如下:

    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();
        if (keyBytes == null) {
            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 {
            // hash the keyBytes to choose a partition
            return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
        }
    }

  可以看出,计算分区的规则如下:

  (1)给定分区值的时候,直接将指明的分区值作为partition值;

  (2)没有指明partition值,但是有key,则会将key的hash值与主题的分区数进行取模运算,得到partition值;

  (3)既没有partition值,也没有key,则在第一次调用时随机生成一个整数(后面每次调用在这个整数上自增),将这个值与 topic 可用的 partition 总数取余得到 partition 值,也就是常说的 round-robin 算法。

  需要说明的是,Kafka的Producer发送消息采用的是异步发送的方式。在消息发送的过程中,涉及到了两个线程 main线程和Sender线程 ,以及一个线程共享变量 RecordAccumulator。main线程将消息发送给 RecordAccumulator,Sender线程不断从RecordAccumulator中拉取消息发送到 Kafka broker。

 

 

  

 

  这也就对应了上面为何会将主题、分区、头信息、时间戳添加到RecordAccumulator变量。

posted @ 2020-08-02 08:55  有心有梦  阅读(379)  评论(0编辑  收藏  举报