Kafka producer源码解析

KafkaProducer

 从 procuder.send 说起

         try {
              val kafkaProducerRecord =new ProducerRecord[String, String]("live_order_id_info_back", sedMsg)
              procuder.send(kafkaProducerRecord)
            } catch {
              case _: Exception => procuder.close()
            }

 调用的是 KafkaProducer构造函数, 调用send发送(其实都是异步处理) ----> 调用的doSend()

 

 public Future<RecordMetadata> send(ProducerRecord<K, V> record) {
        return this.send(record, (Callback)null);
    }

    public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
        ProducerRecord<K, V> interceptedRecord = this.interceptors.onSend(record);
        return this.doSend(interceptedRecord, callback);
    }

    private void throwIfProducerClosed() {
        if (this.ioThread == null || !this.ioThread.isAlive()) {
            throw new IllegalStateException("Cannot perform operation after producer has been closed");
        }
    }

    private Future<RecordMetadata> KafkaProducer(ProducerConfig config, Serializer<K> keySerializer, Serializer<V> valueSerializer)  {
        try {
            log.trace("Starting the Kafka producer");
            Map<String, Object> userProvidedConfigs = config.originals();
            this.producerConfig = config;
            this.time = new SystemTime();

            clientId = config.getString(ProducerConfig.CLIENT_ID_CONFIG);
            //配置中解析出clientId,用于跟踪程序运行情况,在有多个KafkProducer时,若没有配置 client.id则clientId 以前 辍”producer-”后加一个从 1 递增的整数
            if (clientId.length() <= 0)
                clientId = "producer-" + PRODUCER_CLIENT_ID_SEQUENCE.getAndIncrement();
            //注册用于Kafka metrics指标收集的相关对象,用于对 Kafka 集群相关指标的追踪
            Map<String, String> metricTags = new LinkedHashMap<String, String>();
            metricTags.put("client-id", clientId);
            MetricConfig metricConfig = new MetricConfig().samples(config.getInt(ProducerConfig.METRICS_NUM_SAMPLES_CONFIG))
                    .timeWindow(config.getLong(ProducerConfig.METRICS_SAMPLE_WINDOW_MS_CONFIG), TimeUnit.MILLISECONDS)
                    .tags(metricTags);
            List<MetricsReporter> reporters = config.getConfiguredInstances(ProducerConfig.METRIC_REPORTER_CLASSES_CONFIG,
                    MetricsReporter.class);
            reporters.add(new JmxReporter(JMX_PREFIX));
            this.metrics = new Metrics(metricConfig, reporters, time);
            //初始化分区选择器 通過反射獲取
            this.partitioner = config.getConfiguredInstance(ProducerConfig.PARTITIONER_CLASS_CONFIG, Partitioner.class);
            long retryBackoffMs = config.getLong(ProducerConfig.RETRY_BACKOFF_MS_CONFIG);
            //初始集群元数据、消息缓冲区大小、压缩策略
            this.metadata = new Metadata(retryBackoffMs, config.getLong(ProducerConfig.METADATA_MAX_AGE_CONFIG));
            this.maxRequestSize = config.getInt(ProducerConfig.MAX_REQUEST_SIZE_CONFIG);
            this.totalMemorySize = config.getLong(ProducerConfig.BUFFER_MEMORY_CONFIG);
            this.compressionType = CompressionType.forName(config.getString(ProducerConfig.COMPRESSION_TYPE_CONFIG));
       
            //实例化用于存储消息的RecordAccumulator,作用类似一个队列 
            //指定每个RecordBatch的大小,单位是字节
            this.accumulator = new RecordAccumulator(config.getInt(ProducerConfig.BATCH_SIZE_CONFIG),
                    this.totalMemorySize,
                    this.compressionType,
                    config.getLong(ProducerConfig.LINGER_MS_CONFIG),
                    retryBackoffMs,
                    metrics,
                    time);
            List<InetSocketAddress> addresses = ClientUtils.parseAndValidateAddresses(config.getList(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG));
            this.metadata.update(Cluster.bootstrap(addresses), time.milliseconds());
            ChannelBuilder channelBuilder = ClientUtils.createChannelBuilder(config.values());
            //NetworkClient对象构造一个用于数据发送的Sender实例sender 线程,最后通过sender创建一个KafkaThread线 程,启动该线程,该线程是一个守护线程,在后台不断轮询,将消息发送给代理
            NetworkClient client = new NetworkClient(
                    new Selector(config.getLong(ProducerConfig.CONNECTIONS_MAX_IDLE_MS_CONFIG), this.metrics, time, "producer", channelBuilder),
                    this.metadata,
                    clientId,
                    config.getInt(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION),
                    config.getLong(ProducerConfig.RECONNECT_BACKOFF_MS_CONFIG),
                    config.getInt(ProducerConfig.SEND_BUFFER_CONFIG),
                    config.getInt(ProducerConfig.RECEIVE_BUFFER_CONFIG),
                    this.requestTimeoutMs, time);
            this.sender = new Sender(client,
                    this.metadata,
                    this.accumulator,
                    config.getInt(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION) == 1,
                    config.getInt(ProducerConfig.MAX_REQUEST_SIZE_CONFIG),
                    (short) parseAcks(config.getString(ProducerConfig.ACKS_CONFIG)),
                    config.getInt(ProducerConfig.RETRIES_CONFIG),
                    this.metrics,
                    new SystemTime(),
                    clientId,
                    this.requestTimeoutMs);
            String ioThreadName = "kafka-producer-network-thread" + (clientId.length() > 0 ? " | " + clientId : "");
            this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
            this.ioThread.start();

            this.errors = this.metrics.sensor("errors");
            //序列化key
            if (keySerializer == null) {
                this.keySerializer = config.getConfiguredInstance(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
                        Serializer.class);
                this.keySerializer.configure(config.originals(), true);
            } else {
                config.ignore(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG);
                this.keySerializer = keySerializer;
            }
            //序列化value
            if (valueSerializer == null) {
                this.valueSerializer = config.getConfiguredInstance(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
                        Serializer.class);
                this.valueSerializer.configure(config.originals(), false);
            } else {
                config.ignore(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG);
                this.valueSerializer = valueSerializer;
            }

            
            log.debug("Kafka producer started");
        } catch (Throwable t) {
            ....
        }
          
    }

dosend()调用

 private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
        TopicPartition tp = null;
        try {
            // first make sure the metadata for the topic is available
            //步骤一:同步等待拉取元数据。maxBlockTimeMs 最多能等待多久。
             ClusterAndWaitTime clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), maxBlockTimeMs);
            //clusterAndWaitTime.waitedOnMetadataMs 代表的是拉取元数据用了多少时间。
            //maxBlockTimeMs -用了多少时间 = 还剩余多少时间可以使用。
            long remainingWaitMs = Math.max(0, maxBlockTimeMs - clusterAndWaitTime.waitedOnMetadataMs);
            //获取元数据里面的集群相关信息
            Cluster cluster = clusterAndWaitTime.cluster;
            //对key进行序列化
           byte[] serializedKey;
            try {
                serializedKey = keySerializer.serialize(record.topic(), 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");
            }
            //对value进行序列化
            byte[] serializedValue;
            try {
                serializedValue = valueSerializer.serialize(record.topic(), 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);
            int serializedSize = Records.LOG_OVERHEAD + Record.recordSize(serializedKey, serializedValue);
            //确认一下消息的大小是否超过了最大值, KafkaProdcuer初始化的时候,
            //指定了一个参数,代表的是Producer这儿最大能发送的是一条消息能有多大
            //默认最大是1M,我们一般都回去修改它
            ensureValidRecordSize(serializedSize);
            //根据元数据信息,封装分区对象
            tp = new TopicPartition(record.topic(), partition);
            //给每一条消息都绑定他的回调函数。因为我们使用的是异步的方式发送的消息
            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);
             //将要发送的消息追加到RecordAccmulator里面
             RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey, serializedValue, interceptCallback, remainingWaitMs);
           // 把消息放入accumulator(32M的一个内存)
           //然后有accumulator把消息封装成为一个批次一个批次的去发送。
           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);
                //唤醒sender线程,他才是真正发送数据的线程。
                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 (ApiException e) {
            // ..................省略
        }
    }

在分析waitOnMetadata之前,先说一下kafka集群的元数据,我们知道,每个topic有多个分区,每个分区有多个副本,而每个分区的副本里面都需要有一个Leader副本,其他副本只需要同步leader副本的数据即可,而Kafak的元数据就是记录了比如某个分区有哪些副本,leader副本在哪台机器上,follow副本在哪台机器上,哪些副本在ISR(可以理解为follower副本中数据和Leader副本数据相差不大的副本节点)里面 , 在kafka里面主要通过下面的几个类来进行元数据的维护

     接下来我们回到waitOnMetadata,来看一下元数据是如何更新的

private ClusterAndWaitTime waitOnMetadata(String topic, Integer partition, long maxWaitMs) throws InterruptedException {
        // add topic to metadata topic list if it is not there already and reset expiry
        // 把当前的topic存入到元数据里面 
        metadata.add(topic);
        //我们使用的是场景驱动的方式,然后我们目前代码执行到的producer端初始化完成。
        //我们知道这个cluster里面其实没有元数据,只是有我们写代码的时候设置address
        Cluster cluster = metadata.fetch();
        //根据当前的topic从这个集群的cluster元数据信息里面查看分区的信息。
        //因为我们目前是第一次执行这段代码,所以这儿肯定是没有对应的分区的信息的。
        Integer partitionsCount = cluster.partitionCountForTopic(topic);
        // Return cached metadata if we have it, and if the record's partition is either undefined
        // or within the known partition range
        //如果在元数据里面获取到了 分区的信息
        //我们用场景驱动的方式,我们知道如果是第一次代码进来这儿,代码是不会运行这儿。
        if (partitionsCount != null && (partition == null || partition < partitionsCount))
            //直接返回cluster元数据信息,拉取元数据花的时间。
            return new ClusterAndWaitTime(cluster, 0);
        //如果代码执行到这儿,说明,真的需要去服务端拉取元数据。
        //记录当前时间
        long begin = time.milliseconds();
        //剩余多少时间,默认值给的是 最多可以等待的时间。
        long remainingWaitMs = maxWaitMs;
        //已经花了多少时间。
        long elapsed;
        // Issue metadata requests until we have metadata for the topic or maxWaitTimeMs is exceeded.
        // In case we already have cached metadata for the topic, but the requested partition is greater
        // than expected, issue an update request only once. This is necessary in case the metadata
        // is stale and the number of partitions for this topic has increased in the meantime.
        do {
            log.trace("Requesting metadata update for topic {}.", topic);
            //1)获取当前元数据的版本
            //在Producer管理元数据时候,对于他来说元数据是有版本号的。
            //每次成功更新元数据,都会递增这个版本号。
            //2)把needUpdate 标识赋值为true
            int version = metadata.requestUpdate();
            /**
             * 我们发现这儿去唤醒sender线程。
             * 其实是因为,拉取有拉取元数据这个操作是有sender线程去完成的。
             * 这个地方把线程给唤醒了以后
             * 我们知道sender线程肯定就开始进行干活了!!
             * 很明显,真正去获取元数据是这个线程完成。
             */
            sender.wakeup();
            try {
                //TODO 等待元数据
                //同步的等待
                //等待这sender线程获取到元数据。
                metadata.awaitUpdate(version, remainingWaitMs);
            } catch (TimeoutException ex) {
                // Rethrow with original maxWaitMs to prevent logging exception with remainingWaitMs
                throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms.");
            }
            //尝试获取一下集群的元数据信息。
            cluster = metadata.fetch();
            //计算一下 拉取元数据已经花了多少时间
            elapsed = time.milliseconds() - begin;
            //如果花的时间大于 最大等待的时间,那么就报超时。
            if (elapsed >= maxWaitMs)
                throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms.");
            //如果已经获取到了元数据,但是发现topic没有授权
            if (cluster.unauthorizedTopics().contains(topic))
                throw new TopicAuthorizationException(topic);
            //计算出来 还可以用的时间。
            remainingWaitMs = maxWaitMs - elapsed;
            //尝试获取一下,我们要发送消息的这个topic对应分区的信息。
            //如果这个值不为null,说明前面sender线程已经获取到了元数据了。
            partitionsCount = cluster.partitionCountForTopic(topic);
            //如果获取到了元数据以后,这儿代码就会退出。
            //
        } while (partitionsCount == null);

        if (partition != null && partition >= partitionsCount) {
            throw new KafkaException(
                    String.format("Invalid partition given with record: %d is not in the range [0...%d).", partition, partitionsCount));
        }
      //代码就执行到这儿,返回一个对象
        //有两个参数:
        //cluster: 集群的元数据
        //elapsed: 代表的是拉取元数据花了多少时间。
        return new ClusterAndWaitTime(cluster, elapsed);
    }

 当元数据更新后,下一步选择一个分区用来存放咱们的消息,

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

   如果你发过来的消息已经指定了某个分区,那么直接返回即可。

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);

   如果没有指定,调用partitioner.partition进行判断,kafka提供了默认的实现,当然你可以自己定制分发策略

public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
       //根据指定topic获取所有分区信息
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        //获取分区个数
         int numPartitions = partitions.size();
         //如果没有指定消息key
          if (keyBytes == null) {
            
            int nextValue = counter.getAndIncrement();
           //获取指定topic对应的可利用的分区信息,这些可利用是说副本有leader副本的,有些分区他是没有leader副本的,有可能因为一些原因导致
           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;
            }
       //如果指定了消息key,直接对key进行hash后然后对分区数大小进行取模操作   
       } else {
            // hash the keyBytes to choose a partition
            return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
        }
    }

 

  总结一下:

    1、如果你指定了分区,那么只会将这条消息发送到指定分区

    2、如果你同时指定了分区和消息key,也是指发送到这个分区

    3、如果没有指定分区,指定了消息key,那么对key进行hash后对当前分区数进行取模后得出消息应该放到哪个分区

    4、如果没有分区,也没有指定key,则按照一定的轮询方式(counter和分区数取模,counter每次递增,确保消息不会发送到同一个分区里面)来获取分区数

   说完如何获取消息发送的分区后,下一步就是将消息放到暂存区RecordAccumulator,我们下一节RecordAccumulator
具体说明。

doSend在这个方法中一共做了下面这几件事:
1、waitOnMetadata阻塞方式采用RPC方式获取到broker cluster 上broker cluster的信息 

2、将key和value序列化 (内置了基于String、Integer、Long、Double、Bytes、ByteBuffer、ByteArray的序列化工具。)
3、为当前消息选择一个合适的分区
4、确保消息的大小合法
   MAX_REQUEST_SIZE_CONFIG=”max.request.size”
   BUFFER_MEMORY_CONFIG=”buffer.memory”
5、创建要给TopicPartition对象
6、将该record压缩后放到BufferPool中
    关于record的压缩方式,kafka producer在支持了几种方式:
    ·NONE:就是不压缩。
    ·GZIP:压缩率为50%
    ·SNAPPY:压缩率为50%
    ·LZ4:压缩率为50%
这一步是由RecordAccumulator来完成的。RecordAccumulator中为每一个topic维护了一个双端队列Deque<RecordBatch>,队列中的元素是RecordBatch(RecordBatch则由多个record压缩而成)。RecordAccumulator要做的就是将record压缩后放到与之topic关联的那个Deque的最后面。具体源码可以查看CopyOnWriteMap类
7、唤醒Sender线程、(这个一步的目的就是唤醒NIO Selector)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值