1 KafkaProducer的几个重要成员变量
1)Partitioner
用来获取消息应该发往哪个分区
private final Partitioner partitioner;
2)ProducerMetadata
kafka元数据
private final ProducerMetadata metadata;
3)RecordAccumulator
消息累加器,储存生产者生产的消息,再从这里取出发向kafka
private final RecordAccumulator accumulator;
4) Sender
实现了runnable,消息发送对象,作为参数传入下面的Thread
private final Sender sender;
5)Thread
消息发送的线程
private final Thread ioThread;
2 KafkaProducer的构造方法
在构造方法中,关注下面代码
1)创建了sender
2)创建了ioThread
3)ioThread.run启动了线程
也就是说,在创建一个KafkaProducer对象的时候,会创建一个线程,并且跑起来,跑的是sender 里面的run方法。
这个线程做什么事情,后面再说
this.sender = newSender(logContext, kafkaClient, this.metadata);
String ioThreadName = NETWORK_THREAD_PREFIX + " | " + clientId;
this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
this.ioThread.start();
3 KafkaProducer.send
下面,梳理send方法,只看主要的节点
3.1 对消息的key进行序列化
serializedKey = keySerializer.serialize(record.topic(), record.headers(), record.key());
3.2 对消息的value进行序列化
serializedValue = valueSerializer.serialize(record.topic(), record.headers(), record.value());
3.3 获取消息应该发往哪个分区
int partition = partition(record, serializedKey, serializedValue, cluster);
1)首先判断消息是否指定了partion
指定了,直接返回这个partion的id,没有指定,再去判断
return partition != null ?
partition :
partitioner.partition(
record.topic(), record.key(), serializedKey, record.value(), serializedValue, cluster);
2)判断是否指定了key,没有指定key
轮询
存在连通的partion,在连通的partion里轮询
不存在连通的partion,在不连通的partion里轮询
if (keyBytes == null) {
int nextValue = nextValue(topic);
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;
}
}
3)指定了key,根据key的hash获取partion
else {
// hash the keyBytes to choose a partition
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
3.4 将消息放入消息累加器中accumulator.append
RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey,
serializedValue, headers, interceptCallback, remainingWaitMs);
3.4.1 RecordAccumulator-消息累加器的数据结构
它会为每个topic的每个partion都维护一个队列。每个队列里面会有一个或者多个batch(是一个集合)来存储消息。
3.4.2 RecordAccumulator消息累加器的数据结构大小限制和相关参数
1)Batch
默认大小是16K
相关的可配置参数是BATCH_SIZE_CONFIG
public static final String BATCH_SIZE_CONFIG = "batch.size";
说明:
(1)msg进来,大小小于一个batch的剩余大小,放进去
(2)msg进来,大小大于一个batch的剩余大小,新创建一个batch放进去
(3)msg大于16k,创建一个新的batch存放这个消息,也就是说,batch的大小在这种情况下可以大于配置的大小
2)RecordAccumulator
默认大小是32M,满了消息就存不进去,就会阻塞
可配置参数是BUFFER_MEMORY_CONFIG
public static final String BUFFER_MEMORY_CONFIG = "buffer.memory";
3)阻塞超时时间
既然会阻塞,那么必然会有超时时间,RecordAcculator满了,消息放不进来,阻塞的超时时间,消息阻塞超过这个时间,发送失败
可配置参数MAX_BLOCK_MS_CONFIG
public static final String MAX_BLOCK_MS_CONFIG = "max.block.ms";
3.4.3 append方法
1)获取或创建队列
根据partion去获取队列
TopicPartition tp
Deque<ProducerBatch> dq = getOrCreateDeque(tp);
存在,直接获取
不存在,创建
可以看到这个队列是ArrayDeque
Deque<ProducerBatch> d = this.batches.get(tp);
if (d != null)
return d;
d = new ArrayDeque<>();
2)加锁tryAppend
加锁是为了保证线程安全,因为多个线程持有同一个producer对象,调用send方法的话,就会存在线程安全问题
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;
}
3)进入tryAppend方法
先去获取了Batch
获取到了,尝试添加
获取不到tryAppend方法返回null,再去创建Batch,再调用tryAppend。如何去创建这里就不做说明了。
ProducerBatch last = deque.peekLast();
if (last != null) {
FutureRecordMetadata future = last.tryAppend(timestamp, key, value, headers, callback, time.milliseconds());
4)获取到了batch之后,就把消息存进去
如何存的细节就不做说明了
3.5 append方法结束后
append方法结束后,send方法后面就没有做什么其它的事情了,后面就不细究了
3.6 send方法做的事情
根据上面可以看出,send方法就是把消息被放入了RecordAccumulator
并没有涉及到将消息发送给kafka
4 ioThread和sender
4.1 简介
上面,已经知道producer的send方法只是把消息放入了RecordAccumulator,并没有发向kafka。
实际上,发向kafka的操作是在ioThread这个线程进行的
在producer创建的时候,这个线程就跑起来了,实际运行的是sender的run方法
下面,我们来看run方法
4.2 run方法简介
1)循环
一进run方法,就看见一个循环,不停地去调用runOnce方法
while (running) {
try {
runOnce();
} catch (Exception e) {
log.error("Uncaught error in kafka producer I/O thread: ", e);
}
}
2)进入runOnce方法,看到最重要的两行代码
snendProductData :发送数据
client.poll :更新元数据,建立连接,把连接注册到多路复用器上,处理io时间
long pollTimeout = sendProducerData(currentTimeMs);
client.poll(pollTimeout, currentTimeMs);
3)run方法图解
下面再具体介绍sendProducerData和poll两个方法
4.3 sendProducerData
下面,梳理下sendProducerData方法
1)获取kafka元数据
包括节点信息,topic、partion等等信息
获取元数据,如果元数据已经从节点获取到了,则这里拿到的是完整信息,否则只能获取到节点信息
Cluster cluster = metadata.fetch();
2)获取可发送消息的kafka节点
RecordAccumulator.ReadyCheckResult result = this.accumulator.ready(cluster, now);
Iterator<Node> iter = result.readyNodes.iterator();
while (iter.hasNext()) {
Node node = iter.next();
if (!this.client.ready(node, now)) {
iter.remove();
notReadyTimeout = Math.min(notReadyTimeout, this.client.pollDelayMs(node, now));
}
}
3)从消息累加器中获取各个partion的batchs
key是partion的id
value是partion对应的batch集合,这个batch集合称为一个包
注意:这里有一个参数maxRequestSize
指的是这个包的最大大小
默认是1M
相关可配置参数是MAX_REQUEST_SIZE_CONFIG
Map<Integer, List<ProducerBatch>> batches = this.accumulator.drain(cluster, result.readyNodes, this.maxRequestSize, now);
也就是说,从消息累加器中去获取各个partion的batchs,可能只能获取一部分,因为这个包是有大小限制的
4)发送数据
sendProduceRequests(batches, now);
5)进入sendProduceRequests方法
遍历了batchs,一个包一个包的去发送
for (Map.Entry<Integer, List<ProducerBatch>> entry : collated.entrySet())
sendProduceRequest(now, entry.getKey(), acks, requestTimeoutMs, entry.getValue());
6)进入sendProduceRequest方法
先构建了一个ClientRequest对象
里面存储了要发送到的topic、partion还有消息等信息
for (ProducerBatch batch : batches) {
TopicPartition tp = batch.topicPartition;
MemoryRecords records = batch.records();
if (!records.hasMatchingMagic(minUsedMagic))
records = batch.records().downConvert(minUsedMagic, 0, time).records();
produceRecordsByPartition.put(tp, records);
recordsByPartition.put(tp, batch);
}
ProduceRequest.Builder requestBuilder = ProduceRequest.Builder.forMagic(minUsedMagic, acks, timeout,
produceRecordsByPartition, transactionalId);
ClientRequest clientRequest = client.newClientRequest(nodeId, requestBuilder, now, acks != 0,
requestTimeoutMs, callback);
然后,client.send(clientRequest, now);
7)进入client.send方法
进入dosend方法
它先做了个判断,是否能够发送消息到kafka
因为生产者这一端最开始是不知道kafka的元数据信息的。只知道kafka的节点的ip端口信息。
所以它需要先去获取到元信息后,建立连接,才能发送
获取元数据连接连接的操作不在sendProducerData这个里面,而是在poll方法里面
if (!canSendRequest(nodeId, now))
throw new IllegalStateException("Attempt to send a request to node " + nodeId + " which is not ready.");
8)然后一直来到下面这个doSend方法里面
doSend(clientRequest, isInternalRequest, now, builder.build(version));
看到下面代码
在发送之前,创建了一个inFlightRequest,并且存入了inFlightRequests中
这是用来向kafka记录发出了多少个请求,kafka还没有响应,这个请求数和一个配置有关MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION
默认是5,当发出还没有响应的请求数达到这个值,将会阻塞阻塞等待响应之后才会继续发送请求
InFlightRequest inFlightRequest = new InFlightRequest(
clientRequest,
header,
isInternalRequest,
request,
send,
now);
this.inFlightRequests.add(inFlightRequest);
selector.send(send);
9)selector.send(send)
最后,通过selector将包发向kafka
至此,sendProducerData方法就走完了
4.4 client.poll
1)进入poll方法,就发现它尝试去更新元数据信息
long metadataTimeout = metadataUpdater.maybeUpdate(now);
2)进入maybeUpdate方法
看Default的那个
先去获取了最近的一个连通的Node
Node node = leastLoadedNode(now);
3)进入maybeUpdate(now, node)方法
首先,去判断是否可以发送请求,如果可以,直接发送请求
if (canSendRequest(nodeConnectionId, now)) {
Metadata.MetadataRequestAndVersion requestAndVersion = metadata.newMetadataRequestAndVersion();
this.inProgressRequestVersion = requestAndVersion.requestVersion;
MetadataRequest.Builder metadataRequest = requestAndVersion.requestBuilder;
log.debug("Sending metadata request {} to node {}", metadataRequest, node);
sendInternalMetadataRequest(metadataRequest, nodeConnectionId, now);
return defaultRequestTimeoutMs;
}
如果不行,去建立连接,更新元数据
if (connectionStates.canConnect(nodeConnectionId, now)) {
// We don't have a connection to this node right now, make one
log.debug("Initialize connection to node {} for sending metadata request", node);
initiateConnect(node, now);
return reconnectBackoffMs;
}
4)进入initiateConnect(node, now)方法
先通过上面获取的最近的连通的那个node去建立连接,更新元数据
selector.connect(nodeConnectionId,
new InetSocketAddress(address, node.port()),
this.socketSendBuffer,
this.socketReceiveBuffer);
5)再回到poll方法
maybeUpdate方法走完后
调用了selector.poll
这是去处理io事件
this.selector.poll(Utils.min(timeout, metadataTimeout, defaultRequestTimeoutMs));
进入selector.poll方法,看到下面代码
select(timeout)这里传入了一个超时时间
select等待事件的超时时间,超过时间不再阻塞
可配置参数是REQUEST_TIMEOUT_MS_CONFIG
int numReadyKeys = select(timeout);
Set<SelectionKey> readyKeys = this.nioSelector.selectedKeys();
6) poll方法图解