• 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变量。

  • 相关阅读:
    unix/linux中如何在vi编辑器中方便的跳转到首行和末行?
    如何在Ubuntu中用firefox浏览器查看chm文档?
    sybase数据库技术 :游标可更新与for read only/for update
    PropertyMetadata和UIPropertyMetadata的一点区别
    wpf,离线状态下部分功能不可用。
    C#操作注册服务卸载服务启动服务停止服务.. .
    ContentControl与ContentPresenter区别?
    wpf telerik中的book控件
    C#写入和读出文本文件
    WPF 点击Calendar后,需要点击两次按钮
  • 原文地址:https://www.cnblogs.com/yxym2016/p/13415727.html
Copyright © 2020-2023  润新知