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