文章目录
- 引言
- 一)发送消息的流程解释:
- 二)发送消息的代码阅读:
- 1.1 examples.Producer 入口阅读:
- 1.2 Producer.doSend()
- 1.2.1 第一步的获取元数据waitOnMetadata()方法解释
- 1.2.2 第三步分区器的解释
- 1.2.3 第四步消息大小的计算
- 1.2.4 第七步消息放入缓冲区(重要的内容)
- 1.2.4.1 缓冲区双端队列介绍
- 1.2.4.2 缓冲区ByteBufferPool介绍
- 1.2.4.3 append方法解析
- 三)Producer的初始化的代码阅读:
- 2.1 org.apache.kafka.clients.producer.KafkaProducer
- 2.1.1 Sender线程的启动。
引言
围绕Producer的学习,主要分为两个方面:
- Producer如何初始化;
- Producer.send() 是如何工作的 ;
入门指南
Kafka 是 Java 和 Scala 语言混合的项目,目前 IDEA 对这种项目的支持度最好,想要学习源码的话推荐用这个 IDE,省去很多解决环境问题的时间
开始阅读服务端代码的话,推荐从两个地方开始:
KafkaApis 这个类是各种外部请求 Handler 的入口,从这里能看到 Kafka 各种接口的实现
KafkaServer 这个类是服务端 Broker 对应的实现类,从这里作为入口能学习到整个服务端启动的流程
阅读客户端代码的话,Producer 可以关注 RecordAccumulator 这个类,这是解耦本地消息缓存和网络发送线程的枢纽
Consumer 应该关注 ConsumerCoordinator 的实现,关于消费者组管理客户端的实现都在这里,想了解重平衡等客户端核心机制都需要在这里找答案。
一)发送消息的流程解释:
1 整个生产者客户端由两个线程进行协调运行 ,这两个线程分别是:
主线程
、sender
线程 。
2 随后通过可能的拦截器、序列化器、分区器的作用,缓存至RecordAccumulator
[əˈkjuːmjəleɪtə®] 。
3Sender线程
从RecordAccumulator
中获取消息,发送至Kafka
中。
RecordAccumulator
主要用来缓存消息,以便Sender线程可以批量发送,进而减少网络传输。在其内部,为每个分区都维护了一个双端队列,队列中的内容就是ProducerBatch,结构为:Deque<ProducerBatch>
。消息从尾巴追加进入,sender线程从队列头部获取。
ProducerBatch
并不是ProducerRecord , 每一个ProducerBatch中至少包含一个ProducerRecord。你可以理解为ProducerBatch是一个批次,而ProducerRecord是一条消息。
BufferPool
:在RecordAccumulator
中,为了复用发送消息要用到的byteBuff,而规避掉频繁创建与释放byteBuff,所以对该对象进行了池化。不过ByteBufferPool只针对特定大小的ByteBuff进行管理,默认是16KB。
ProducerBatch写入的过程:
当ProducerRecord流入RecordAccumulator时,会先寻找与消息分区对应的双端队列(如果没有则新创建),查看Producer中是否可以写入这条消息。如果可以写入则对该批次进行一次追加写入,如果无法写入则创建一个新的ProducerBatch。
在新建ProducerBatch的时候,会评估消息的大小是否超过了batchSize;
如果没有超过batchSize的大小,呢么以batchSize的大小进行创建,这样再使用完这块内存区域后,可以通过BufferPool进行管理;
如果超过了batchSize的大小,则以评估消息的大小作为创建宜居,呢么这块内存区域不会再被复用了。
Sender线程拉取RecordAccumulator:
当拉取成功后,会进一步将原本的<分区,Deque<Producer>>
转换成<Node,List<ProducerBatch>>
的形式;
其中Node表示Kafka集群的broker节点。
对于网络连接而言,生产者客户端是与具体的broker节点建立的建立,是向broker节点发送消息,并不关系是哪一个分区;
对于KafkaProducer的应用逻辑而言,我们是关系向哪个分区发送哪些消息,所以这里面会存在上述转换,其本质是应用层逻辑转述到网络IO层面。
<Node,List<ProducerBatch>>
转 <Node,Request>
,sender线程会将上述的Node,List<ProducerBatch>
进行一次转换,以具备协议请求的request对象
向kafka发送。
InFlightRequests :
请求在从Sender线程发往kafka之前,还会保存一份到InFightRequests中;
InFightRequests中,是以Map<NodeId,Deque<Request>>
进行保存的,其目的是缓存呢些已经发出去但是还是没有收到相应的请求。
InFightRequests中,还提供许多管理类的方式,比如具体可以缓存多少条发送出去但是未得到相应的消息。
以上为一个较粗的流程,弱化掉了MATEDATA的相关内容,主要是我怕我乱了。
实际上在创建 KafkaProducer 实例时,该 Sender 线程开始运行时首先会创建与 Broker 的连接,并换取元数据信息。
二)发送消息的代码阅读:
1.1 examples.Producer 入口阅读:
这里是一个简易的Producer 发送消息的代码,以此作为入口。
kafka.examples.Producer.java
public class Producer extends Thread {
private final KafkaProducer<Integer, String> producer;
private final String topic;
private final Boolean isAsync;
/**
* 构建方法,初始化生产者对象
* @param topic
* @param isAsync
*/
public Producer(String topic, Boolean isAsync) {
Properties props = new Properties();
// 用户拉取kafka的元数据
props.put("bootstrap.servers", "localhost:9092");
props.put("client.id", "DemoProducer");
//设置序列化的类。
//二进制的格式
props.put("key.serializer", "org.apache.kafka.common.serialization.IntegerSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
//消费者,消费数据的时候,就需要进行反序列化。
//TODO 初始化kafkaProducer
producer = new KafkaProducer<>(props);
this.topic = topic;
this.isAsync = isAsync;
}
public void run() {
int messageNo = 1;
// 一直会往kafka发送数据
while (true) {
String messageStr = "Message_" + messageNo;
long startTime = System.currentTimeMillis();
//构建消息体
ProducerRecord<Integer, String> record = new ProducerRecord<>(topic, messageNo, messageStr);
//isAsync , kafka发送数据的时候,有两种方式
//1: 异步发送
//2: 同步发送
//isAsync: true的时候是异步发送,false就是同步发送
if (isAsync) { // Send asynchronously
//异步发送,一直发送,消息响应结果交给回调函数处理
//这样的方式,性能比较好,我们生产代码用的就是这种方式。
producer.send(record, new DemoCallBack(startTime, messageNo, messageStr));
} else { // Send synchronously
try {
//同步发送
//发送一条消息,等这条消息所有的后续工作都完成以后才继续下一条消息的发送。
producer.send(record).get();
System.out.println("Sent message: (" + messageNo + ", " + messageStr + ")");
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
++messageNo;
}
}
}
/**
* org.apache.kafka.clients.producer.Callback
* 注意这里是Kafka提供的回调类,不是juc的。
*/
class DemoCallBack implements Callback {
private final long startTime;
private final int key;
private final String message;
public DemoCallBack(long startTime, int key, String message) {
this.startTime = startTime;
this.key = key;
this.message = message;
}
/**
* A callback method the user can implement to provide asynchronous handling of request completion. This method will
* be called when the record sent to the server has been acknowledged. Exactly one of the arguments will be
* non-null.
*
* @param metadata The metadata for the record that was sent (i.e. the partition and offset). Null if an error
* occurred.
* @param exception The exception thrown during processing of this record. Null if no error occurred.
*/
public void onCompletion(RecordMetadata metadata, Exception exception) {
long elapsedTime = System.currentTimeMillis() - startTime;
if (exception != null) {
System.out.println("有异常");
//一般我们生产里面 还会有其它的备用的链路。
} else {
System.out.println("说明没有异常信息,成功的!!");
}
if (metadata != null) {
System.out.println(
"message(" + key + ", " + message + ") sent to partition(" + metadata.partition() +
"), " +
"offset(" + metadata.offset() + ") in " + elapsedTime + " ms");
} else {
exception.printStackTrace();
}
}
}
1.2 Producer.doSend()
org.apache.kafka.clients.producer.KafkaProducer#doSend()
这里是具体发送消息的代码,流程如下:
同步等待拉取元数据
对消息的key和value进行序列化
根据分区器选择消息应该发送的分区
根据元数据信息封装分区对象
给每一条消息都绑定他的回调函数
把消息放入accumulator
唤醒sender线程
Sender线程为什么这里需要被wakeup,暂且空着
/**
* Implementation of asynchronously send a record to a topic.
*/
private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
TopicPartition tp = null;
try {
/**
* 步骤一:
* 同步等待拉取元数据。
* 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和value进行序列化。
*/
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");
}
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);
/**
* 步骤四:
* 确认一下消息的大小是否超过了最大值。
* KafkaProducer初始化的时候,指定了一个参数,代表的是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);
/**
* 步骤七:
* 把消息放入accumulator(32M的一个内存)
* 然后有accumulator把消息封装成为一个批次一个批次的去发送。
*/
RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey, serializedValue, 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);
/**
* 步骤八:
* 唤醒sender线程。他才是真正发送数据的线程。
* 还记得之前说到的Accumulator中的双端队列吗?
* sender线程从头部拉取消息,这里就是对sender线程进行一次wakeUp
* 主线程只要保证消息投递到Accumulator即可。
*/
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) {
log.debug("Exception occurred during message send:", e);
if (callback != null)
callback.onCompletion(null, e);
this.errors.record();
if (this.interceptors != null)
this.interceptors.onSendError(record, tp, e);
return new FutureFailure(e);
} catch (InterruptedException e) {
this.errors.record();
if (this.interceptors != null)
this.interceptors.onSendError(record, tp, e);
throw new InterruptException(e);
} catch (BufferExhaustedException e) {
this.errors.record();
this.metrics.sensor("buffer-exhausted-records").record();
if (this.interceptors != null)
this.interceptors.onSendError(record, tp, e);
throw e;
} catch (KafkaException e) {
this.errors.record();
if (this.interceptors != null)
this.interceptors.onSendError(record, tp, e);
throw e;
} catch (Exception e) {
// we notify interceptor about all exceptions, since onSend is called before anything else in this method
if (this.interceptors != null)
this.interceptors.onSendError(record, tp, e);
throw e;
}
}
1.2.1 第一步的获取元数据waitOnMetadata()方法解释
ClusterAndWaitTime clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), maxBlockTimeMs);
org.apache.kafka.clients.Metadata
/**
* Wait for metadata update until the current version is larger than the last version we know of
*/
public synchronized void awaitUpdate(final int lastVersion, final long maxWaitMs) throws InterruptedException {
if (maxWaitMs < 0) {
throw new IllegalArgumentException("Max time to wait for metadata updates should not be < 0 milli seconds");
}
//获取当前时间
long begin = System.currentTimeMillis();
//看剩余可以使用的时间,一开始是最大等待的时间。
long remainingWaitMs = maxWaitMs;
//version是元数据的版本号。
//如果当前的这个version小于等于上一次的version。
//说明元数据还没更新。
//因为如果sender线程那儿 更新元数据,如果更新成功了,sender线程肯定回去累加这个version。
while (this.version <= lastVersion) {
//如果还有剩余的时间。
if (remainingWaitMs != 0)
//主线程休眠
//这儿被唤醒有两个情况:要么获取到元数据了,被sender线程唤醒,要么就是时间到了。
//唤醒的代码见org.apache.kafka.clients.Metadata#update()
wait(remainingWaitMs);
//如果代码执行到这儿 说明就要么就被唤醒了,要么就到点了。
//计算一下花了多少时间。
long elapsed = System.currentTimeMillis() - begin;
//已经超时了
if (elapsed >= maxWaitMs)
//报一个超时的异常。
throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms.");
//再次计算 可以使用的时间。
remainingWaitMs = maxWaitMs - elapsed;
}
}
在发送消息中获取元数据这一步,当前线程进行了wait() ;这个点记住,等会回答。
1.2.2 第三步分区器的解释
org.apache.kafka.clients.producer.internals#DefaultPartitioner#partition
/**
* Compute the partition for the given record.
*
* @param topic The topic name
* @param key The key to partition on (or null if no key)
* @param keyBytes serialized key to partition on (or null if no key)
* @param value The value to partition on or null
* @param valueBytes serialized value to partition on or null
* @param cluster The current cluster metadata
*/
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();
/**
* producer 发送数据的时候:
* message
*
* key,message
* message
*
*/
//策略一: 如果发送消息的时候,没有指定key
if (keyBytes == null) {
//这儿有一个计数器
//每次执行都会加一
//10
int nextValue = counter.getAndIncrement();
//获取可用的分区数
List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
if (availablePartitions.size() > 0) {
//计算我们要发送到哪个分区上。
//一个数如果对10进行取模, 0-9之间
//11 % 9
//12 % 9
//13 % 9
//实现一个轮训的效果,能达到负载均衡。
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 {
//策略二:这个地方就是指定了key
// hash the keyBytes to choose a partition
//直接对key取一个hash值 % 分区的总数取模
//如果是同一个key,计算出来的分区肯定是同一个分区。
//如果我们想要让消息能发送到同一个分区上面,那么我们就
//必须指定key. 这一点非常重要 。
//公司二方库包装的KafkaProducer就是用这种方式的
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
}
1.2.3 第四步消息大小的计算
/**
* Validate that the record size isn't too large
*/
private void ensureValidRecordSize(int size) {
//如果一条消息的大小超过了 1M,那么就会报错
if (size > this.maxRequestSize)
//自定义了异常。
throw new RecordTooLargeException("The message is " + size +
" bytes when serialized which is larger than the maximum request size you have configured with the " +
ProducerConfig.MAX_REQUEST_SIZE_CONFIG +
" configuration.");
//如果你的一条消息的大小超过32M也会报错。
if (size > this.totalMemorySize)
throw new RecordTooLargeException("The message is " + size +
" bytes when serialized which is larger than the total memory buffer you have configured with the " +
ProducerConfig.BUFFER_MEMORY_CONFIG +
" configuration.");
}
1.2.4 第七步消息放入缓冲区(重要的内容)
1.2.4.1 缓冲区双端队列介绍
我们在第五步中,已经将消息调整为<话题,分区> 的数据格式了
/**
* 步骤五:
* 根据元数据信息,封装分区对象
*/
tp = new TopicPartition(record.topic(), partition);
话题与双端队列的关系
:一个话题如果具备N个分区,则现在会形成N个双端队列,既话题的每一个分区会有一个双端队列;
1.2.4.2 缓冲区ByteBufferPool介绍
每个RecordBatch的如果用时申请内存,呢么一定是一个频繁创建且频繁销毁的内存使用场景,Kafka对其进行了池化的优化。
ByterBuffPool中缓存了与RecordBatch大小相等的byteBuffer。当需要使用ByteBuffer时从池中获取,使用完毕做归还,进而避免了ByteBuffer频繁创建的过程。
1.2.4.3 append方法解析
org.apache.kafka.clients.producer.internals.RecordAccumulator#append
/**
* Add a record to the accumulator, return the append result
* <p>
* The append result will contain the future metadata, and flag for whether the appended batch is full or a new batch is created
* <p>
*
* @param tp The topic/partition to which this record is being sent
* @param timestamp The timestamp of the record
* @param key The key for the record
* @param value The value for the record
* @param callback The user-supplied callback to execute when the request is complete
* @param maxTimeToBlock The maximum time in milliseconds to block for buffer memory to be available
*/
public RecordAppendResult append(TopicPartition tp,
long timestamp,
byte[] key,
byte[] value,
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();
try {
// check if we have an in-progress batch
/**
* 步骤一:先根据分区找到应该插入到哪个队列里面。
* 如果有已经存在的队列,那么我们就使用存在队列
* 如果队列不存在,那么我们新创建一个队列
*
* 我们肯定是有了存储批次的队列,但是大家一定要知道一个事
* 我们代码第一次执行到这儿,获取其实就是一个空的队列。
*
* 现在代码第二次执行进来。
* 假设 分区还是之前的那个分区。
*
* 这个方法里面我们之前分析,里面就是针对batchs进行的操作
* 里面kafka自己封装了一个数据结构:CopyOnWriteMap (这个数据结构本来就是线程安全的)
*
*
* 根据当前消息的信息,获取一个队列,也可以说每有一条消息都要获取一次队列
*/
Deque<RecordBatch> dq = getOrCreateDeque(tp);
/**
* 假设我们现在有线程一,线程二,线程三
*
*/
synchronized (dq) {
if (closed)
throw new IllegalStateException("Cannot send after the producer is closed.");
/**
* 步骤二:
* 尝试往队列里面的批次里添加数据
*
* 一开始添加数据肯定是失败的,我们目前只是创建了队列
* 数据是需要存储在批次对象里面(这个批次对象是需要分配内存的)
* 我们目前还没有分配内存,所以如果按场景驱动的方式,
* 代码第一次运行到这儿其实是不成功的。
*/
RecordAppendResult appendResult = tryAppend(timestamp, key, value, callback, dq);
//第一次进来的时候appendResult的值就为null
if (appendResult != null)
return appendResult;
}
// we don't have an in-progress record batch try to allocate a new batch
/**
* 步骤三:计算一个批次的大小
* 在消息的大小和批次的大小之间取一个最大值,用这个值作为当前这个批次的大小。
* 有可能我们的一个消息的大小比一个设定好的批次的大小还要大。
* 默认一个批次的大小是16K。
* 所以我们看到这段代码以后,应该给我们一个启示。
* 如果我们生产者发送数的时候,如果我们的消息的大小都是超过16K,
* 说明其实就是一条消息就是一个批次,那也就是说消息是一条一条被发送出去的。
* 那如果是这样的话,批次这个概念的设计就没有意义了
* 所以大家一定要根据自定公司的数据大小的情况去设置批次的大小。
*
*
*
*/
int size = Math.max(this.batchSize, Records.LOG_OVERHEAD + Record.recordSize(key, value));
log.trace("Allocating a new {} byte message buffer for topic {} partition {}", size, tp.topic(), tp.partition());
/**
* 步骤四:
* 根据批次的大小去分配内存
*
* 这里分配内存,在多线程场景下会为”同一个“批次申请多次内存。
* 但在后续代码创建队列追加批次后, if (appendResult != null)
* 又再次释放掉。
*/
ByteBuffer buffer = free.allocate(size, maxTimeToBlock);
synchronized (dq) {
// Need to check if producer is closed again after grabbing the dequeue lock.
if (closed)
throw new IllegalStateException("Cannot send after the producer is closed.");
/**
* 步骤五:
* 尝试把数据写入到批次里面。
* 代码第一次执行到这儿的时候 依然还是失败的(appendResult==null)
* 目前虽然已经分配了内存
* 但是还没有创建批次,那我们向往批次里面写数据
* 还是不能写的。
*
* 线程二进来执行这段代码的时候,是成功的。
*/
RecordAppendResult appendResult = tryAppend(timestamp, key, value, callback, dq);
//失败的意思就是appendResult 还是会等于null
if (appendResult != null) {
//释放内存,对应前面创建了多个内存的场景。
free.deallocate(buffer);
return appendResult;
}
/**
* 步骤六:
* 根据内存大小封装批次
*
*
* 线程一到这儿 会根据内存封装出来一个批次。
*/
MemoryRecords records = MemoryRecords.emptyRecords(buffer, compression, this.batchSize);
RecordBatch batch = new RecordBatch(tp, records, time.milliseconds());
//尝试往这个批次里面写数据,到这个时候 我们的代码会执行成功。
FutureRecordMetadata future = Utils.notNull(batch.tryAppend(timestamp, key, value, callback, time.milliseconds()));
/**
* 步骤七:
* 把这个批次放入到这个队列的队尾
*
*/
dq.addLast(batch);
incomplete.add(batch);
return new RecordAppendResult(future, dq.size() > 1 || batch.records.isFull(), true);
}//释放锁
} finally {
appendsInProgress.decrementAndGet();
}
}
RecordAccumulator#getOrCreateDeque(TopicPartition tp)
/**
* Get the deque for the given topic-partition, creating it if necessary.
*/
private Deque<RecordBatch> getOrCreateDeque(TopicPartition tp) {
/**
* CopyonWriteMap:
* get
* put
*
*/
//直接从batches里面获取当前分区对应的存储队列
Deque<RecordBatch> d = this.batches.get(tp);
//代码第一次执行到这里是获取不到队列的,也就是说d这个变量的值为null
if (d != null)
return d;
//代码继续执行,创建出来一个新的空队列,
d = new ArrayDeque<>();
//把这个空的队列存入batches 这个数据结构里面
Deque<RecordBatch> previous = this.batches.putIfAbsent(tp, d);
if (previous == null)
return d;
else
//直接返回新的结果
return previous;
}
RecordAccumulator#tryAppend(long timestamp, byte[] key, byte[] value, Callback callback, Deque deque)
/**
* If `RecordBatch.tryAppend` fails (i.e. the record batch is full), close its memory records to release temporary
* resources (like compression streams buffers).
*/
private RecordAppendResult tryAppend(long timestamp, byte[] key, byte[] value, Callback callback, Deque<RecordBatch> deque) {
//首先要获取到队列里面一个批次
RecordBatch last = deque.peekLast();
//第一次进来是没有批次的,所以last肯定为null
//线程二进来的时候,这个last不为空
if (last != null) {
//线程二就插入数据就ok了
FutureRecordMetadata future = last.tryAppend(timestamp, key, value, callback, time.milliseconds());
if (future == null)
last.records.close();
else
//返回值就不为null了
return new RecordAppendResult(future, deque.size() > 1 || last.records.isFull(), false);
}
//返回结果就是一个null值
return null;
}
三)Producer的初始化的代码阅读:
我们摘要下关键的初始化属性:
- partitioner 分区器
- interceptorList 过滤器链
- metadata 元数据
内部结构如下
- compressionType 消息压缩方式
- accumulator 消息缓冲区
- NetworkClient 网络连接池
- Sender 线程的初始化;
2.1 org.apache.kafka.clients.producer.KafkaProducer
org.apache.kafka.clients.producer.KafkaProducer
@SuppressWarnings({"unchecked", "deprecation"})
private 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
clientId = config.getString(ProducerConfig.CLIENT_ID_CONFIG);
if (clientId.length() <= 0)
clientId = "producer-" + PRODUCER_CLIENT_ID_SEQUENCE.getAndIncrement();
Map<String, String> metricTags = new LinkedHashMap<String, String>();
metricTags.put("client-id", clientId);
//metric一些东西,我们一般分析源码的时候 不需要去关心
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);
/**
*
* Producer发送消息的时候,我们代码里面一般会设置重试机制的
*
* 分布式,网络 不稳定
* Producer 指定 重试机制
*/
//重试时间 retry.backoff.ms 默认100ms
long retryBackoffMs = config.getLong(ProducerConfig.RETRY_BACKOFF_MS_CONFIG);
//设置序列化器
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;
}
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;
}
// load interceptors and make sure they get clientId
//
userProvidedConfigs.put(ProducerConfig.CLIENT_ID_CONFIG, clientId);
//设置拦截器
//类似于一个过滤器
List<ProducerInterceptor<K, V>> interceptorList = (List) (new ProducerConfig(userProvidedConfigs)).getConfiguredInstances(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,
ProducerInterceptor.class);
this.interceptors = interceptorList.isEmpty() ? null : new ProducerInterceptors<>(interceptorList);
ClusterResourceListeners clusterResourceListeners = configureClusterResourceListeners(keySerializer, valueSerializer, interceptorList, reporters);
//生产者从服务端那儿拉取过来的kafka的元数据。
//生产者要想去拉取元数据, 发送网络请求,重试,
//metadata.max.age.ms(默认5分钟)
//生产者每隔一段时间都要去更新一下集群的元数据。
this.metadata = new Metadata(retryBackoffMs, config.getLong(ProducerConfig.METADATA_MAX_AGE_CONFIG), true, clusterResourceListeners);
//max.request.size 生产者往服务端发送消息的时候,规定一条消息最大多大?
//如果你超过了这个规定消息的大小,你的消息就不能发送过去。
//默认是1M,这个值偏小,在生产环境中,我们需要修改这个值。
//经验值是10M。但是大家也可以根据自己公司的情况来。
this.maxRequestSize = config.getInt(ProducerConfig.MAX_REQUEST_SIZE_CONFIG);
//指的是缓存大小
//默认值是32M,这个值一般是够用,如果有特殊情况的时候,我们可以去修改这个值。
this.totalMemorySize = config.getLong(ProducerConfig.BUFFER_MEMORY_CONFIG);
//kafka是支持压缩数据的,这儿设置压缩格式。
//提高你的系统的吞吐量,你可以设置压缩格式。
//一次发送出去的消息就更多。生产者这儿会消耗更多的cpu.
this.compressionType = CompressionType.forName(config.getString(ProducerConfig.COMPRESSION_TYPE_CONFIG));
/* check for user defined settings.
* If the BLOCK_ON_BUFFER_FULL is set to true,we do not honor METADATA_FETCH_TIMEOUT_CONFIG.
* This should be removed with release 0.9 when the deprecated configs are removed.
*/
if (userProvidedConfigs.containsKey(ProducerConfig.BLOCK_ON_BUFFER_FULL_CONFIG)) {
log.warn(ProducerConfig.BLOCK_ON_BUFFER_FULL_CONFIG + " config is deprecated and will be removed soon. " +
"Please use " + ProducerConfig.MAX_BLOCK_MS_CONFIG);
boolean blockOnBufferFull = config.getBoolean(ProducerConfig.BLOCK_ON_BUFFER_FULL_CONFIG);
if (blockOnBufferFull) {
this.maxBlockTimeMs = Long.MAX_VALUE;
} else if (userProvidedConfigs.containsKey(ProducerConfig.METADATA_FETCH_TIMEOUT_CONFIG)) {
log.warn(ProducerConfig.METADATA_FETCH_TIMEOUT_CONFIG + " config is deprecated and will be removed soon. " +
"Please use " + ProducerConfig.MAX_BLOCK_MS_CONFIG);
this.maxBlockTimeMs = config.getLong(ProducerConfig.METADATA_FETCH_TIMEOUT_CONFIG);
} else {
this.maxBlockTimeMs = config.getLong(ProducerConfig.MAX_BLOCK_MS_CONFIG);
}
} else if (userProvidedConfigs.containsKey(ProducerConfig.METADATA_FETCH_TIMEOUT_CONFIG)) {
log.warn(ProducerConfig.METADATA_FETCH_TIMEOUT_CONFIG + " config is deprecated and will be removed soon. " +
"Please use " + ProducerConfig.MAX_BLOCK_MS_CONFIG);
this.maxBlockTimeMs = config.getLong(ProducerConfig.METADATA_FETCH_TIMEOUT_CONFIG);
} else {
this.maxBlockTimeMs = config.getLong(ProducerConfig.MAX_BLOCK_MS_CONFIG);
}
/* check for user defined settings.
* If the TIME_OUT config is set use that for request timeout.
* This should be removed with release 0.9
*/
if (userProvidedConfigs.containsKey(ProducerConfig.TIMEOUT_CONFIG)) {
log.warn(ProducerConfig.TIMEOUT_CONFIG + " config is deprecated and will be removed soon. Please use " +
ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG);
this.requestTimeoutMs = config.getInt(ProducerConfig.TIMEOUT_CONFIG);
} else {
this.requestTimeoutMs = config.getInt(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG);
}
//TODO 创建了一个核心的组件
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));
//去更新元数据
//addresses 这个地址其实就是我们写producer代码的时候,传参数的时候,传进去了一个broker的地址。
//所以这段代码看起来像是去服务端拉取元数据,所以我们去验证一下,是否真的去拉取元数据。
//TODO update方法初始化的时候并没有去服务端拉取元数据。
this.metadata.update(Cluster.bootstrap(addresses), time.milliseconds());
ChannelBuilder channelBuilder = ClientUtils.createChannelBuilder(config.values());
//TODO 初始化了一个重要的管理网路的组件。
/**
* (1)connections.max.idle.ms: 默认值是9分钟
* 一个网络连接最多空闲多久,超过这个空闲时间,就关闭这个网络连接。
*
* (2)max.in.flight.requests.per.connection:默认是5
* producer向broker发送数据的时候,其实是有多个网络连接。
* 每个网络连接可以忍受 producer端发送给broker 消息然后消息没有响应的个数。
*
*
* 因为kafka有重试机制,所以有可能会造成数据乱序,如果想要保证有序,这个值要把设置为1.
*
* (3)send.buffer.bytes:socket发送数据的缓冲区的大小,默认值是128K
* (4)receive.buffer.bytes:socket接受数据的缓冲区的大小,默认值是32K。
*/
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);
//
//我们在项目中一般都会去设置重试,
/**
*
* (1) retries:重试的次数
* (2) acks:
* 0:
* producer发送数据到broker后,就完了,没有返回值,不管写成功还是写失败都不管了。
* 1:
* producer发送数据到broker后,数据成功写入leader partition以后返回响应。
* -1:
* producer发送数据到broker后,数据要写入到leader partition里面,并且数据同步到所有的
* follower partition里面以后,才返回响应。
*
*/
//这个就是一个线程
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 : "");
//创建了一个线程,然后里面传进去了一个sender对象。
//把业务的代码和关于线程的代码给隔离开来。
//关于线程的这种代码设计的方式,其实也值得大家积累的。
this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
//启动线程。
this.ioThread.start();
this.errors = this.metrics.sensor("errors");
config.logUnused();
AppInfoParser.registerAppInfo(JMX_PREFIX, clientId);
log.debug("Kafka producer started");
} catch (Throwable t) {
// call close methods if internal objects are already constructed
// this is to prevent resource leak. see KAFKA-2121
close(0, TimeUnit.MILLISECONDS, true);
// now propagate the exception
throw new KafkaException("Failed to construct kafka producer", t);
}
}
2.1.1 Sender线程的启动。
org.apache.kafka.clients.producer.KafkaProducer#
KafkaProducer(ProducerConfig config, Serializer<K> keySerializer, Serializer<V> valueSerializer)
在上述构造方法中,我们摘录下ioThread的构建与启动:
this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
//启动线程。
this.ioThread.start();
之前说过Producer构建成功后,就会创建连接。
这里的Sender线程被run了,呢么具体的执行逻辑里面应该包含连接建立的相关内容。
org.apache.kafka.clients.producer.internals.Sender#run
/**
* The main run loop for the sender thread
*/
public void run() {
log.debug("Starting Kafka producer I/O thread.");
//其实代码就是一个死循环,然后一直在运行。
//所以我们要知道sender线程启动起来一以后是一直在运行的。
while (running) {
try {
//TODO 核心代码
run(time.milliseconds());
} catch (Exception e) {
log.error("Uncaught error in kafka producer I/O thread: ", e);
}
}
log.debug("Beginning shutdown of Kafka producer I/O thread, sending remaining records.");
// okay we stopped accepting requests but there may still be
// requests in the accumulator or waiting for acknowledgment,
// wait until these are completed.
while (!forceClose && (this.accumulator.hasUnsent() || this.client.inFlightRequestCount() > 0)) {
try {
run(time.milliseconds());
} catch (Exception e) {
log.error("Uncaught error in kafka producer I/O thread: ", e);
}
}
if (forceClose) {
// We need to fail all the incomplete batches and wake up the threads waiting on
// the futures.
this.accumulator.abortIncompleteBatches();
}
try {
this.client.close();
} catch (Exception e) {
log.error("Failed to close network client", e);
}
log.debug("Shutdown of Kafka producer I/O thread has completed.");
}
org.apache.kafka.clients.producer.internals.Sender#run(long now)
/**
* Run a single iteration of sending
*
* @param now
* The current POSIX time in milliseconds
*/
void run(long now) {
/**
*
* (1)代码第一次进来:
* 获取元数据,因为我们是根据场景驱动的方式,目前是我们第一次代码进来,还没有获取到元数据
* 所以这个cluster里面是没有元数据,如果这儿没有元数据的话,这个方法里面接下来的代码就不用看了
* 因为接下来的这些代码都依赖这个元数据。
*
* (2)代码第二次进来:
* 我们用场景驱动的方式,现在我们的代码是第二次进来
* 第二次进来的时候,已经有元数据了,所以cluster这儿是有元数据。
*
* 步骤一:
* 获取元数据
*
* 这个方法就是我们今天晚上主要分析到一个方法了
* 我们先大概了看一下里面有哪些功能?
*
*
* 场景驱动方式
* 获取到元数据
*/
Cluster cluster = metadata.fetch();
// get the list of partitions with data ready to send
/**
* 步骤二:
* 首先是判断哪些partition有消息可以发送:
* 我们看一下一个批次可以发送出去的条件
*
* 获取到这个partition的leader partition对应的broker主机(根据元数据信息来就可以了)
*
* 哪些broker上面需要我们去发送消息?
*/
RecordAccumulator.ReadyCheckResult result = this.accumulator.ready(cluster, now);
/**
* 步骤三:
* 标识还没有拉取到元数据的topic
*/
if (!result.unknownLeaderTopics.isEmpty()) {
// The set of topics with unknown leader contains topics with leader election pending as well as
// topics which may have expired. Add the topic again to metadata to ensure it is included
// and request metadata update, since there are messages to send to the topic.
for (String topic : result.unknownLeaderTopics)
this.metadata.add(topic);
this.metadata.requestUpdate();
}
// remove any nodes we aren't ready to send to
Iterator<Node> iter = result.readyNodes.iterator();
long notReadyTimeout = Long.MAX_VALUE;
while (iter.hasNext()) {
Node node = iter.next();
/**
* 步骤四:检查与要发送数据的主机的网络是否已经建立好。
*/
if (!this.client.ready(node, now)) {
//如果返回的是false !false 代码就进来
//移除result 里面要发送消息的主机。
//所以我们会看到这儿所有的主机都会被移除
iter.remove();
notReadyTimeout = Math.min(notReadyTimeout, this.client.connectionDelay(node, now));
}
}
/**
* 步骤五:
*
* 我们有可能要发送的partition有很多个,
* 很有可能有一些partition的leader partition是在同一台服务器上面。
* 假设我们集群只有3台服务器 0 1 2
* 主题:p0 p1 p2 p3
*
* p0:leader -> 0
* p1:leader -> 0
* p2:leader -> 1
* p3:leader -> 2
*
* 当我们的分区的个数大于集群的节点的个数的时候,一定会有多个leader partition在同一台服务器上面。
*
* 按照broker进行分组,同一个broker的partition为同一组
* 0:{p0,p1} -> 批次
* 1:{p2}
* 2:{p3}
*
* 一个批次就一个请求 -> broker
*
* 减少网络传输到次数
*
*
*/
//所以我们发现 如果网络没有建立的话,这儿的代码是不执行的
Map<Integer, List<RecordBatch>> batches = this.accumulator.drain(cluster,
result.readyNodes,
this.maxRequestSize,
now);
if (guaranteeMessageOrder) {
// Mute all the partitions drained
//如果batches 空的话,这而的代码也就不执行了。
for (List<RecordBatch> batchList : batches.values()) {
for (RecordBatch batch : batchList)
this.accumulator.mutePartition(batch.topicPartition);
}
}
/**
* 步骤六:
* 对超时的批次是如何处理的?
*
*/
List<RecordBatch> expiredBatches = this.accumulator.abortExpiredBatches(this.requestTimeout, now);
// update sensors
for (RecordBatch expiredBatch : expiredBatches)
this.sensors.recordErrors(expiredBatch.topicPartition.topic(), expiredBatch.recordCount);
sensors.updateProduceRequestMetrics(batches);
/**
* 步骤七:
* 创建发送消息的请求
*
*
* 创建请求
* 我们往partition上面去发送消息的时候,有一些partition他们在同一台服务器上面
* ,如果我们一分区一个分区的发送我们网络请求,那网络请求就会有一些频繁
* 我们要知道,我们集群里面网络资源是非常珍贵的。
* 会把发往同个broker上面partition的数据 组合成为一个请求。
* 然后统一一次发送过去,这样子就减少了网络请求。
*/
//如果网络连接没有建立好 batches其实是为空。
//也就说其实这段代码也是不会执行。
List<ClientRequest> requests = createProduceRequests(batches, now);
// If we have any nodes that are ready to send + have sendable data, poll with 0 timeout so this can immediately
// loop and try sending more data. Otherwise, the timeout is determined by nodes that have partitions with data
// that isn't yet sendable (e.g. lingering, backing off). Note that this specifically does not include nodes
// with sendable data that aren't ready to send since they would cause busy looping.
long pollTimeout = Math.min(result.nextReadyCheckDelayMs, notReadyTimeout);
if (result.readyNodes.size() > 0) {
log.trace("Nodes with data ready to send: {}", result.readyNodes);
log.trace("Created {} produce requests: {}", requests.size(), requests);
pollTimeout = 0;
}
//TODO 发送请求的操作
for (ClientRequest request : requests)
//绑定 op_write
client.send(request, now);
/**
* 解接下来要发送网络请求了,把把数据写到服务端?
*
* Selector
*
* write
*
*/
// if some partitions are already ready to be sent, the select time would be 0;
// otherwise if some partition already has some data accumulated but not ready yet,
// the select time will be the time difference between now and its linger expiry time;
// otherwise the select time will be the time difference between now and the metadata expiry time;
//TODO 重点就是去看这个方法
//就是用这个方法拉取的元数据。
/**
* 步骤八:
* 真正执行网络操作的都是这个NetWordClient这个组件
* 包括:发送请求,接受响应(处理响应)
*
* 拉取元数据信息,靠的就是这段代码
*/
//我们猜这儿可能就是去建立连接。
this.client.poll(pollTimeout, now);
}
org.apache.kafka.clients.producer.internals.Sender#run#this.client.poll(pollTimeout, now)
上述为真正建立连接的方法,其实现为
org.apache.kafka.clients.NetworkClient#poll
/**
* Do actual reads and writes to sockets.
*
* @param timeout The maximum amount of time to wait (in ms) for responses if there are none immediately,
* must be non-negative. The actual timeout will be the minimum of timeout, request timeout and
* metadata timeout
* @param now The current time in milliseconds
* @return The list of responses received
*/
@Override
public List<ClientResponse> poll(long timeout, long now) {
/**
* 在这个方法里面有涉及到kafka的网络的方法
*/
//步骤一:封装了一个要拉取元数据请求 ,˙这一步有点难,我们先跳过//todo
long metadataTimeout = metadataUpdater.maybeUpdate(now);
try {
//步骤二: 发送请求,进行复杂的网络操作
//TODO 执行网络IO的操作。 NIO
this.selector.poll(Utils.min(timeout, metadataTimeout, requestTimeoutMs));
} catch (IOException e) {
log.error("Unexpected error during I/O", e);
}
// process completed actions
long updatedNow = this.time.milliseconds();
List<ClientResponse> responses = new ArrayList<>();
handleCompletedSends(responses, updatedNow);
//步骤三:处理响应,响应里面就会有我们需要的元数据。
/**
* 这个地方是生产者是如何获取元数据.
* 其实Kafak获取元数据的流程跟我们发送消息的流程是一模一样。
* 获取元数据 -》 判断网络连接是否建立好 -》 建立网络连接
* -》 发送请求(获取元数据的请求) -》 服务端发送回来响应(带了集群的元数据信息)
*
*/
handleCompletedReceives(responses, updatedNow);
handleDisconnections(responses, updatedNow);
handleConnections();
//TODO 处理长时间没有接受到响应
handleTimedOutRequests(responses, updatedNow);
// invoke callbacks
for (ClientResponse response : responses) {
if (response.request().hasCallback()) {
try {
//调用的响应的里面的我们之前发送出去的请求的回调函数
//看到了这儿,我们回头再去看一下
//我们当时发送请求的时候,是如何封装这个请求。
//不过虽然目前我们还没看到,但是我们可以大胆猜一下。
//当时封装网络请求的时候,肯定是给他绑定了一个回调函数。
response.request().callback().onComplete(response);
} catch (Exception e) {
log.error("Uncaught error in request completion:", e);
}
}
}
return responses;
}
呢么之前存疑的wait方法是在哪里被唤醒的呢?
org.apache.kafka.clients.Metadata#update()
/**
* Updates the cluster metadata. If topic expiry is enabled, expiry time
* is set for topics if required and expired topics are removed from the metadata.
*/
public synchronized void update(Cluster cluster, long now) {
Objects.requireNonNull(cluster, "cluster should not be null");
this.needUpdate = false;
this.lastRefreshMs = now;
this.lastSuccessfulRefreshMs = now;
this.version += 1;
//这个默认值是true
if (topicExpiryEnabled) {
// Handle expiry of topics from the metadata refresh set.
//但是我们目前topics是空的
//所以下面的代码是不会被运行的。
//到现在我们的代码是不是第二次进来了呀?
//如果第二次进来,此时此刻进来,我们 producer.send(topics,)方法
//要去拉取元数据 -》 sender -》 代码走到的这儿。
//第二次进来的时候,topics其实不为空了,因为我们已经给它赋值了
//所以这儿的代码是会继续运行的。
for (Iterator<Map.Entry<String, Long>> it = topics.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<String, Long> entry = it.next();
long expireMs = entry.getValue();
if (expireMs == TOPIC_EXPIRY_NEEDS_UPDATE)
entry.setValue(now + TOPIC_EXPIRY_MS);
else if (expireMs <= now) {
it.remove();
log.debug("Removing unused topic {} from the metadata list, expiryMs {} now {}", entry.getKey(), expireMs, now);
}
}
}
for (Listener listener: listeners)
listener.onMetadataUpdate(cluster);
String previousClusterId = cluster.clusterResource().clusterId();
//这个的默认值是false,所以这个分支的代码不会被运行。
if (this.needMetadataForAllTopics) {
// the listener may change the interested topics, which could cause another metadata refresh.
// If we have already fetched all topics, however, another fetch should be unnecessary.
this.needUpdate = false;
this.cluster = getClusterForCurrentTopics(cluster);
} else {
//所以代码执行的是这儿。
//直接把刚刚传进来的对象赋值给了这个cluster。
//cluster代表的是kafka集群的元数据。
//初始化的时候,update这个方法没有去服务端拉取数据。
this.cluster = cluster;//address
}
// The bootstrap cluster is guaranteed not to have any useful information
if (!cluster.isBootstrapConfigured()) {
String clusterId = cluster.clusterResource().clusterId();
if (clusterId == null ? previousClusterId != null : !clusterId.equals(previousClusterId))
log.info("Cluster ID: {}", cluster.clusterResource().clusterId());
clusterResourceListeners.onUpdate(cluster.clusterResource());
}
//大家发现这儿会有一个notifyAll,这个最重要的一个作用是不是就是唤醒,我们上一讲
//看到那个wait的线程。
notifyAll();
log.debug("Updated cluster metadata version {} to {}", this.version, this.cluster);
}
以上为第一次记录Kafka消息发送相关代码 ;