经过序列化,计算分区号之后KafkaProducer主线程调用RecordAccumulator的append方法将消息追加到缓存。并唤醒sender线程处理。流程参照博客:
send方法中更新元数据源码分析KafkaProducer发送消息简要流程

这里sender.wakeup()方法就是最终调用了nioSelector的wakeup方法,selector监听channel事件,会发送阻塞。关于sender线程的【分析博客地址】

1、RecordAccumulator简单介绍:

RecordAccumulator就是producer的缓存,Kafka借助RecordAccumulator实现消息批量发送,从而提高网络性能。具体就是RecordAccumulator内部维护了一个ConcurrentMap, ConcurrentMap<TopicPartition, Deque> batches,发往同一个topic的分区消息放在一个双端队列中,形成一个消息组,这样sender线程就能将这个组的消息批量发送。

问题就是producer如何将消息放入RecordAccumulator缓存中的,消息又是经过怎么样的处理呢?分析源码:

2、RecordAccumulator初始化时机

要分析RecordAccumulator的api,需要分析RecordAccumulator实例化

在KafkaProducer的核心构造方法中初始化,也就是实例化KafkaProducer会为每个producer创建一个自己的RecordAccumulator

this.accumulator = new RecordAccumulator(logContext,
        config.getInt(ProducerConfig.BATCH_SIZE_CONFIG),
        this.totalMemorySize,
        this.compressionType,
        config.getLong(ProducerConfig.LINGER_MS_CONFIG),
        retryBackoffMs,
        metrics,
        time,
        apiVersions,
        transactionM:anager);

通过整理初始化可以知道RecordAccumulator读取了以下配置:

batch.size,buffer.memory,compression.type,linger.msretry.backoff.ms,Metrics,

初始化到RecordAccumulator中。注册Metrics,具体详细配置在配置中专门分析。

2、append方法源码分析
public RecordAppendResult append(TopicPartition tp, long timestamp, byte[] key, byte[] value, Header[] headers,
    Callback callback, long maxTimeToBlock) throws InterruptedException {
    // We keep track of the number of appending thread to make sure we do not miss batches in
    // abortIncompleteBatches().
    // 线程数计数器
    appendsInProgress.incrementAndGet();
    ByteBuffer buffer = null;
    if (headers == null)
        headers = Record.EMPTY_HEADERS;
    try {
        // check if we have an in-progress batch
        /**
         * 获取该消息发往的分区 对应的队列 如果不存咋 就新建一个
         */
        Deque<ProducerBatch> dq = getOrCreateDeque(tp);
        synchronized (dq) {
            if (closed)
                throw new KafkaException("Producer closed while send in progress");
            /**
             * 尝试加入队列中
             */
            RecordAppendResult appendResult = tryAppend(timestamp, key, value, headers, callback, dq);
            if (appendResult != null)
                /**
                 * 加入成功
                 */
                return appendResult;
        }

        // we don't have an in-progress record batch try to allocate a new batch
        byte maxUsableMagic = apiVersions.maxUsableProduceMagic();
        int size = Math.max(this.batchSize,
            AbstractRecords.estimateSizeInBytesUpperBound(maxUsableMagic, compression, key, value, headers));
        log.trace("Allocating a new {} byte message buffer for topic {} partition {}", size, tp.topic(),
            tp.partition());
        /**
         * 按照 batch.size或者消息的最大size 加入失败,新开辟一个buffer
         */
        buffer = free.allocate(size, maxTimeToBlock);
        synchronized (dq) {
            // Need to check if producer is closed again after grabbing the dequeue lock.
            if (closed)
                throw new KafkaException("Producer closed while send in progress");

            RecordAppendResult appendResult = tryAppend(timestamp, key, value, headers, callback, dq);
            if (appendResult != null) {
                // Somebody else found us a batch, return the one we waited for! Hopefully this doesn't happen
                // often...
                return appendResult;
            }

            MemoryRecordsBuilder recordsBuilder = recordsBuilder(buffer, maxUsableMagic);
            // 新建一个ProducerBatch
            ProducerBatch batch = new ProducerBatch(tp, recordsBuilder, time.milliseconds());
            FutureRecordMetadata future =
                Utils.notNull(batch.tryAppend(timestamp, key, value, headers, callback, time.milliseconds()));
            // 将消息batch加入到队列尾部
            dq.addLast(batch);
            //4  将消息放到IncompleteBatches Set集合中
            incomplete.add(batch);

            // Don't deallocate this buffer in the finally block as it's being used in the record batch
            buffer = null;

            return new RecordAppendResult(future, dq.size() > 1 || batch.isFull(), true);
        }
    } finally {
        if (buffer != null)
            free.deallocate(buffer);
        appendsInProgress.decrementAndGet();
    }
}
2.1放入缓存的过程:

1,先从map中获取该消息有无对应的队列Deque,没有则新建一个ArrayDeque。

因为ArrayDeque不是线程安全的集合,在加入消息时候,对Deque进行加锁处理。

2,将消息尝试加入到队列中,加入成功则返回。

3,加入失败则新建一个buffer,然后新建一个ProducerBatch对象并调用tryAppend方法将消息封装到ProducerBatch中,然后将消息加入到Deque尾部。

4,将消息放进IncompleteBatches的set集合中,维护未发送完成的ProducerBatch集合。

至于

2.3要对放入缓存进行深入分析还需要分析以下几个问题的源码:

1,返回的RecordAppendResult对象包含了哪些信息,不同条件的返回情况如何

2,步骤2中尝试将消息加入到Deque中的步骤是怎么样的

3,ProducerBatch是如何将消息封装的,有哪些过程

4,这里从源码上看Deque就是一个承载ProducerBatch的容器,使用普通队列就可以,为什么需要使用双端队列, 这里主要是是解决发送失败重试问题,当消息发送失败,需要把消息优先放入队列头部重新发送。

问题一: RecordAppendResult

RecordAppendResult就是RecordAccumulator的一个内部类封装了一下信息

future就是KafkaProducer send方法中返回的future,可以获取到返回的元数据RecordMetadata

对于RecordAppendResult这里暂时分析到这里,后期对FutureRecordMetadata会进行分析

public final static class RecordAppendResult {
    //添加消息的future
    public final FutureRecordMetadata future;
    //Batch是否满了
    public final boolean batchIsFull;
    //Batch是否新建
    public final boolean newBatchCreated;
问题二:tryAppend分析
private RecordAppendResult tryAppend(long timestamp, byte[] key, byte[] value, Header[] headers,
                                     Callback callback, Deque<ProducerBatch> deque) {
    //取出队列尾部的ProducerBatch
    ProducerBatch last = deque.peekLast();
    if (last != null) {
        //调用了ProducerBatch的tryAppend方法
        FutureRecordMetadata future = last.tryAppend(timestamp, key, value, headers, callback, time.milliseconds());
        if (future == null)
            last.closeForRecordAppends();
        else
            return new RecordAppendResult(future, deque.size() > 1 || last.isFull(), false);
    }
    return null;
}

向队列中尝试加入消息,这个时候是从队列的尾部取出一个ProducerBatch,然后向该ProducerBatch中加入消息,如果ProducerBatch已经满了,那么就会返回一个null.

问题三:ProducerBatch.tryAppend分析
public FutureRecordMetadata tryAppend(long timestamp, byte[] key, byte[] value, Header[] headers, Callback callback,
    long now) {
    /**
     * 检查是否还有足够的空间追加该消息
     */
    if (!recordsBuilder.hasRoomFor(timestamp, key, value, headers)) {
        return null;
    } else {
        /**
         * 追加消息返回偏移量
         */
        Long checksum = this.recordsBuilder.append(timestamp, key, value, headers);

        this.maxRecordSize = Math.max(this.maxRecordSize, AbstractRecords.estimateSizeInBytesUpperBound(magic(),
            recordsBuilder.compressionType(), key, value, headers));
       //记录追加时间
        this.lastAppendTime = now;
        //封装FutureRecordMetadata
        FutureRecordMetadata future = new FutureRecordMetadata(this.produceFuture, this.recordCount, timestamp,
            checksum, key == null ? -1 : key.length, value == null ? -1 : value.length);
        // we have to keep every future returned to the users in case the batch needs to be
        // split to several new batches and resent.
        thunks.add(new Thunk(callback, future));
        this.recordCount++;
        return future;
    }
}

后续的调用链就比较复杂了

this.recordsBuilder.append(timestamp, key, value, headers);
 //wrapNullable(key) 就是将缓冲区的数据会存放在byte数组中,使用缓冲区
    --->MemoryRecordsBuilder.append()
        ---appendWithOffset();    //计算绝对偏移量
           ---->appendWithOffset();
              --->appendDefaultRecord();
                  --->DefaultRecord.writeTo  将buffer中的数据写进流

问题思考:
1,主线程一边将消息放入缓存,sender线程一边发送,当两者的速率不同时ProducerBatch如何变化
2,同步发送消息时候ProducerBatch如何变化