Kafka核心源码分析-生产者-RecordAccumulator

1.简单介绍

主线程调用KafkaProducer.send()方法发送消息的时候,先将消息放到RecordAccumulator中暂存,然后主线程就可以从send()方法中返回了,此时消息并没有真正发送给Kafka,而是在RecordAccumulator中暂存,当RecordAccumulator中存储量达到一个条件后,便会唤醒Sender线程发送RecordAccumulator中的消息。
在这里插入图片描述
RecordAccumulator中用一个ConcurrentHashMap来缓存我们的数据,其中key保存的分区的信息,value保存的是具体的消息记录,value使用ArrayQueue来保存,RecordBatch又使用MemoryRecords来保存。数据结构如上图。

2.源码分析

大体了解了RecordAccumulator的数据结构后,我们从底层的MemoryRecords开始分析。

2.1 MemoryRecords

MemoryRecords表示的是对多个消息的集合。底层是使用ByteBuffer来存储数据的。简单介绍一下属性

	// 仅用于附加的压缩器,对消息进行压缩,将压缩后的数据输出到buffer
    private final Compressor compressor;

    // 记录buffer字段最多可以写入多少个字节的数据
    private final int writeLimit;

    // 初始缓冲区的容量,仅用于可写记录的取消分配
    private final int initialCapacity;

    // 用于读取的底层缓冲区; 当记录仍然可写时,它为null
    private ByteBuffer buffer;

    // 此MemoryRecords是可读还是可写模式,在MemoryRecords发送前,是只读模式
    private boolean writable;

Compressor的压缩类型是由"compress.type"配置参数指定的,目前版本主要支持GZIP,SNAPPY,LZ4三种压缩方式。

MemoryRecords的构造方法是私有的,只能通过empty()方法得到其对象。主要的方法有append(),close()

public long append(long offset, long timestamp, byte[] key, byte[] value) {
   
        if (!writable)
            throw new IllegalStateException("Memory records is not writable");

        int size = Record.recordSize(key, value);
        //偏移量
        compressor.putLong(offset);
        //消息的size
        compressor.putInt(size);
        //写入数据
        long crc = compressor.putRecord(timestamp, key, value);
        //指明此消息体的大小
        compressor.recordWritten(size + Records.LOG_OVERHEAD);
        //返回数据量大小
        return crc;
    }

close()方法中,会将MemoryRecords.buffer字段指向扩容后的ByteBuffer对象,同时将writable设置为false,即只读模式。

2.2 RecordBatch

我们继续分析上层的结构,,每个RecordBatch对象中封装了一个MemoryRecords对象,除此之外,还封装了很多控制信息和统计信息,下面简单介绍一下。

	//记录了保存的Record个数
    public int recordCount = 0;
    //最大的Record的字节数
    public int maxRecordSize = 0;
    //尝试发送当前RecordBatch的次数
    public volatile int attempts = 0;
    public final long createdMs;
    public long drainedMs;
    //最后一次尝试发送的时间戳
    public long lastAttemptMs;
    //拥有一个MemoryRecords的引用,这个才是消息真正存放的地方,指向存储数据的MemoryRecords对象
    public final MemoryRecords records;
    //当前RecordBatch中缓存的消息,都会发送给此TopicPartition
    public final TopicPartition topicPartition;
    //标识RecordBatch状态的Future对象
    public final ProduceRequestResult produceFuture;
    //最后一次向RecordBatch追加消息的时间戳
    public long lastAppendTime;
    //Thunk对象的集合,可以理解为消息的回调对象对列,Thunk中的callback字段就指向对应消息的Callback对象
    private final List<Thunk> thunks;
    //用来记录某消息在RecordBatch中的偏移量
    private long offsetCounter = 0L;
    //是否正在重试,如果RecordBatch中的数据发送失败,则会重新尝试发送
    private boolean retry;

我们来看下接收到响应时的流程图
在这里插入图片描述
以RecordBatch中的done()方法画的,ProduceRequestResult这个类实现了类似Future的功能,使用count值为1的CountdownLatch对象,来标记此RecordBatch是否发送完成。当RecordBatch的消息被全部响应,是会调用ProduceRequestResult.done()方法的,用error字段标识是否正常完成,之后调用CountdownLatch.countDown(),唤醒所有等待此消息发送完成的线程。简单看下源码

   /**
     * 将此请求标记为已完成,并取消阻止所有等待其完成的线程。
     * error表示是正常完成还是异常完成
     *
     */
    public void done(TopicPartition topicPartition, long baseOffset, RuntimeException error) {
   
        this.topicPartition = topicPartition;
        this.baseOffset = baseOffset;
        this.error = error;
        //countDown-1
        this.latch.countDown();
    }

ProduceRequestResult中有一个属性,baseOffset,这里保存的是服务端为此RecordBatch中第一条消息分配的offset的值,为什么需要这个值?因为我们在发送完消息后,需要知道这个消息的在服务端的offset,这样我们就不需要知道这个RecordBatch中所有消息的offset了,只需要知道第一个消息的offset,再集合自身在RecordBatch中的相对偏移量,就可以计算出来当前消息在服务端分区中的偏移量了。

Thunk指的是消息的回调对象队列,Thunk中的callback字段就指向对应消息的Callback对象

 /**
     * A callback and the associated FutureRecordMetadata argument to pass to it.
     * RecordBatch的内部类
     * 回调以及要传递给它的关联的FutureRecordMetadata参数。
     */
    final private static class Thunk {
   
        final Callback callback;

        final FutureRecordMetadata future;

        public Thunk(Callback callback, FutureRecordMetadata future) {
   
            this.callback = callback;
            this.future = future;
        }
    }

在这个类中还有一个FutureRecordMetadata的属性,这个类有两个关键字段,

	//指向对应消息所在RecordBatch的produceFuture字段
    private final ProduceRequestResult result;
    //记录了对应消息在RecordBatch中的偏移量
    private final long relativeOffset;

这个类的基本操作,都是委托给了ProduceRequestResult对应的方法,当生产者收到某条消息的响应时,FutureRecordMetadata.get()方法就会返回RecordMetadata对象,这个对象包含offset,时间戳等数据,可供用户自定义Callback使用。

下面我们看看RecordBatch类的核心方法,tryAppend(),主要功能是追加到当前记录集并返回该记录集内的相对偏移量

/**
     * Append the record to the current record set and return the relative offset within that record set
     * 将记录追加到当前记录集并返回该记录集内的相对偏移量
     * 
     * @return The RecordSend corresponding to this record or null if there isn't sufficient room.
     * 与此记录对应的RecordSend;如果没有足够的空间,则为null。
     */
    public FutureRecordMetadata tryAppend(long timestamp, byte[] key, byte[] value, Callback callback, long now) {
   
        //估算剩余空间不足,前面说过,这不是一个准确值
        if (!this.records.hasRoomFor(key, value)) {
   
            return null;
        } else {
   
            //向MemoryRecords中添加数据,注意,offsetCounter是在RecordBatch中的偏移量
            long checksum = this.records.append(offsetCounter++, timestamp, key, value);
            this.maxRecordSize = Math.max(this.maxRecordSize, Record.recordSize(key, value));
            this.lastAppendTime = now;
            //更新统计信息(省略)
            //创建FutureRecordMetadata对象
            FutureRecordMetadata future = new FutureRecordMetadata(this.produceFuture, this.recordCount,
                                                                   timestamp, checksum,
                                                                   key == null ? -1 : key
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值