之前我们知道sender线程在producer初始化的时候就启动了,最终还是要去运行run方法,所以我们回过头来再看一下,代码第二次运行到这里会发生什么
void run(long now) {
//获取元数据
//因为是场景驱动方式,目前是第一次代码进来,还没有获取到元数据
//所以这个cluster里面没有元数据的
//如果这没有元数据的话,这个方法里面接下来的代码就不用看了
//是因为接下来的代码依赖这个元数据
//TODO 直接看这个代码的最后一行代码
//就是这行代码去拉取的元数据。
/**
* 因为是场景驱动的方式,现在代码是第二次进来
* 第二次进来的时候,已经有元数据了,所以cluster这是有元数据的
*
* 步骤一:
* 获取元数据
*/
Cluster cluster = metadata.fetch();
// get the list of partitions with data ready to send
/**
* 步骤二:
* 首先判断哪些partition有消息可以发送,获取到这个partition对应的leader partition
* 对应的broker主机
*/
RecordAccumulator.ReadyCheckResult result = this.accumulator.ready(cluster, now);
// if there are any partitions whose leaders are not known yet, force metadata update
/**
* 步骤三:
* 标识还没有拉取到元数据的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)) {
iter.remove();
notReadyTimeout = Math.min(notReadyTimeout, this.client.connectionDelay(node, now));
}
}
// create produce requests
/**
* 步骤五:
* 有可能要发送的partition有很多个
* 很有可能一些partition的leader partition是在同一台服务器上面
* 按照broker进行分组,同一个broker的partition为同一组
*/
Map<Integer, List<RecordBatch>> batches = this.accumulator.drain(cluster,
result.readyNodes,
this.maxRequestSize,
now);
if (guaranteeMessageOrder) {
// Mute all the partitions drained
for (List<RecordBatch> batchList : batches.values()) {
for (RecordBatch batch : batchList)
this.accumulator.mutePartition(batch.topicPartition);
}
}
/**
* 步骤六:
* 放弃超时的batch
*/
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);
/**
* 步骤七:
* 创建发送消息的请求
*/
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;
}
for (ClientRequest request : requests)
client.send(request, now);
// 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 重点就是去看这个方法
//就是用这个方法去拉取的元数据。
/**
* 步骤八:
* 真正执行网络操作的都是NetworkClient这个组件
* 包括;发送请求,接受请求
*/
this.client.poll(pollTimeout, now);
}
以上就是代码第二次运行到sender线程的run方法要做的一些事情,这里我们先整理出整体流程,接下来再去看实现细节
5.5.1一个batch什么条件下可以发送
本节我们来看一下sender的一些细节实现,上节我们已经划分出了八个步骤
步骤一
/**
* 因为是场景驱动的方式,现在代码是第二次进来
* 第二次进来的时候,已经有元数据了,所以cluster这是有元数据的
*
* 步骤一:
* 获取元数据
*/
Cluster cluster = metadata.fetch();
因为是第二次进来所以已经有元数据了
步骤二
首先我们要知道kafka生产者这有一个消息重试的机制
1.重试次数
2.重试的时间间隔
消息发送的时候,如果一直凑不齐一个批次,那我们要一直等下去吗?肯定不是的,会限定一个时间,在这个时间之内即使不满一个批次也要把批次发送出去。
// get the list of partitions with data ready to send
/**
* 步骤二:
* 首先判断哪些partition有消息可以发送,获取到这个partition对应的leader partition
* 对应的broker主机
*/
RecordAccumulator.ReadyCheckResult result = this.accumulator.ready(cluster, now);
接下来我们跟进去看一下
public ReadyCheckResult ready(Cluster cluster, long nowMs) {
Set<Node> readyNodes = new HashSet<>();
long nextReadyCheckDelayMs = Long.MAX_VALUE;
Set<String> unknownLeaderTopics = new HashSet<>();
//waiter里面有数据,说明内存池里面的内存不够了
//如果这个值等于true,说明内存池里面的内存不够了
boolean exhausted = this.free.queued() > 0;
//遍历所有的分区
for (Map.Entry<TopicPartition, Deque<RecordBatch>> entry : this.batches.entrySet()) {
TopicPartition part = entry.getKey();
//获取到分区对应的队列
Deque<RecordBatch> deque = entry.getValue();
//根据分区 可以获取到这个分区的leader partition在哪一台主机上面
Node leader = cluster.leaderFor(part);
synchronized (deque) {
//如果没有找到对应主机。unknownLeaderTopics标识一下
if (leader == null && !deque.isEmpty()) {
// This is a partition for which leader is not known, but messages are available to send.
// Note that entries are currently not removed from batches when deque is empty.
unknownLeaderTopics.add(part.topic());
} else if (!readyNodes.contains(leader) && !muted.contains(part)) {
//首先从队头获取到批次
RecordBatch batch = deque.peekFirst();
//如果这个批次不为null,我们判断一下是否可以发送这个批次
if (batch != null) {
/**
* batch.attempts:重试的次数
* batch.lastAttemptMs:上一次重试的时间
* retryBackoffMs:重试的时间间隔
*
* backingOff:重新发送数据的时间到了
*
*/
boolean backingOff = batch.attempts > 0 && batch.lastAttemptMs + retryBackoffMs > nowMs;
/**
* nowMs:当前时间
* batch.lastAttemptMs:上一次重试的时间
*
* waitedTimeMs:这个批次已经等了多久了
*/
long waitedTimeMs = nowMs - batch.lastAttemptMs;
/**
* 因为是场景驱动的方式,因为是第一次发送消息
* 所以之前没有消息发送出去过,也就没有重试
*
* 那么timeToWaitMs = lingerMs
* lingerMs默认为0
* 这个值是0的话,那就代表来一条消息就发送一条消息
* 很明显不合适
* 所以需要我们去配置这个参数
* 比如timeToWaitMs = lingerMs = 100ms
* 那么就是消息最多存100ms就必须要发送出去了
*/
long timeToWaitMs = backingOff ? retryBackoffMs : lingerMs;
/**
* timeToWaitMs:最多能等待多久
* waitedTimeMs:已经等待了多久
* timeLeftMs:还要再等待多久
*/
long timeLeftMs = Math.max(timeToWaitMs - waitedTimeMs, 0);
/**
* 如果队列大于1,说明这个队列里面至少有一个批次写满了
* 如果批次写满了就可以发送数据
* 当然也可能这个队列里面只有一个批次。然后刚好这个批次写满了
* 也可以发送数据
*
* full:是否有写满的批次
*/
boolean full = deque.size() > 1 || batch.records.isFull();
/**
* waitedTimeMs:已经等待了多久
* timeToWaitMs:最多需要等待多久
* expired:如果为true
* 代表时间到了,到了要发送消息的时候了
*/
boolean expired = waitedTimeMs >= timeToWaitMs;
/**
* full:如果一个批次写满了(无论时间有没有到)就可以发送
* expired:时间到了(批次没有写满也得发送)
*exhausted:内存不够,也要发送,这样就会释放内存
*/
boolean sendable = full || expired || exhausted || closed || flushInProgress();
//可以发送消息了
if (sendable && !backingOff) {
//把可以发送批次的partiti的leader partiti所在的主机加入到
//readyNodes
readyNodes.add(leader);
} else {
// Note that this results in a conservative estimate since an un-sendable partition may have
// a leader that will later be found to have sendable data. However, this is good enough
// since we'll just wake up and then sleep again for the remaining time.
nextReadyCheckDelayMs = Math.min(timeLeftMs, nextReadyCheckDelayMs);
}
}
}
}
}
return new ReadyCheckResult(readyNodes, nextReadyCheckDelayMs, unknownLeaderTopics);
}
这里主要就是各种判断,满足条件即可发送数据
5.5.2筛选可以发送消息的broker
上一节我们看了步骤二,接下来我们看一下步骤三
/**
* 步骤三:
* 标识还没有拉取到元数据的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();
}
这里比较简单如果没有拉取到要发送数据的对应topic的元数据信息,做一个标识,下一次去拉取过来
我们继续看步骤四
Iterator<Node> iter = result.readyNodes.iterator();
long notReadyTimeout = Long.MAX_VALUE;
while (iter.hasNext()) {
Node node = iter.next();
/**
* 步骤四:
* 检查与要发送数据的主机的网络是否已经建立好
*/
if (!this.client.ready(node, now)) {
iter.remove();
notReadyTimeout = Math.min(notReadyTimeout, this.client.connectionDelay(node, now));
}
}
跟进来看一下
public interface KafkaClient extends Closeable
public boolean ready(Node node, long now);
可以看到是一个接口,那么就要去看实现,看 NetworkClient这个实现
找到ready方法
*/
@Override
public boolean ready(Node node, long now) {
//如果当前检查的节点为null,抛异常
if (node.isEmpty())
throw new IllegalArgumentException("Cannot connect to empty node " + node);
//判断要发送消息的主机,是否具备发送消息的条件,主要就是网络有没有建立好
if (isReady(node, now))
return true;
//判断是否可以尝试去建立网络
if (connectionStates.canConnect(node.idString(), now))
// if we are interested in sending to a node and we don't have a connection to it, initiate one
//初始化连接
initiateConnect(node, now);
return false;
}
接下来看一下判断条件isReady
@Override
public boolean isReady(Node node, long now) {
// if we need to update our metadata now declare all requests unready to make metadata requests first
// priority
//!metadataUpdater.isUpdateDue(now)
//我们要发送写数据请求的时候,不能是正在更新元数据的时候
//
return !metadataUpdater.isUpdateDue(now) && canSendRequest(node.idString());
}
看一下关键的canSendRequest(node.idString())
private boolean canSendRequest(String node) {
/**
* connectionStates.isConnected(node)
* 生产者:有多个连接,缓存多个连接(跟broker节点数是一样的)
* 判断缓存里面是否已经把连接给建立好了
*
* selector.isChannelReady(node)
* java NIO:selector
* selector -》 绑定了多个KafkaChannel
* 一个KafkaChannel就代表一个连接
*
* inFlightRequests.canSendMore(node)
* 每个往broker主机上面发送消息的连接,最多能容忍5个消息,发送出去了
* 但是没有接收到响应
*这里会影响到发送数据的顺序
*/
return connectionStates.isConnected(node) && selector.isChannelReady(node) && inFlightRequests.canSendMore(node);
}
继续看如果网络没有建立好的处理
public boolean canConnect(String id, long now) {
//首先从缓存里面获取当前主机的连接
NodeConnectionState state = nodeState.get(id);
//如果值为null,说明从来没有连接过
if (state == null)
return true;
else
//可以从缓存里面获取到连接
//但是连接的状态是DISCONNECTED失去连接了并且
//now - state.lastConnectAttemptMs >= this.reconnectBackoffMs 说明可以进行重试,重试连接
return state.state == ConnectionState.DISCONNECTED && now - state.lastConnectAttemptMs >= this.reconnectBackoffMs;
}
5.5.3 筛选可以发送消息的broker流程图
5.5.4kafka网络设计
这需要JAVA NIO的基础
我们先接着上一节的代码来看
*/
@Override
public boolean ready(Node node, long now) {
//如果当前检查的节点为null,抛异常
if (node.isEmpty())
throw new IllegalArgumentException("Cannot connect to empty node " + node);
//判断要发送消息的主机,是否具备发送消息的条件,主要就是网络有没有建立好
if (isReady(node, now))
return true;
//判断是否可以尝试去建立网络
if (connectionStates.canConnect(node.idString(), now))
// if we are interested in sending to a node and we don't have a connection to it, initiate one
//初始化连接
initiateConnect(node, now);
return false;
}
initiateConnect(node, now);这个地方明显就是网络的一些操作
跟进来看一下
private void initiateConnect(Node node, long now) {
String nodeConnectionId = node.idString();
try {
log.debug("Initiating connection to node {} at {}:{}.", node.id(), node.host(), node.port());
this.connectionStates.connecting(nodeConnectionId, now);
//这里做了初始化
selector.connect(nodeConnectionId,
new InetSocketAddress(node.host(), node.port()),
this.socketSendBuffer,
this.socketReceiveBuffer);
} catch (IOException e) {
/* attempt failed, we'll try again after the backoff */
connectionStates.disconnected(nodeConnectionId, now);
/* maybe the problem is our metadata, update it */
metadataUpdater.requestUpdate();
log.debug("Error connecting to node {} at {}:{}:", node.id(), node.host(), node.port(), e);
}
}
这里我们可以看到一个selector,进来看一下就是这个
private final Selectable selector;
这里呢其实就是Selectable接口
public interface Selectable
看实现,会有 Selector这样一个实现类
这个selector是kafka自己封装的一个selector,他是基于java NIO里面的selector去封装的
//这个对象就是javaNIO里面的Selector
//Selector是负责网络的建立,发送网络请求,处理实际的网络IO
//所以他是最核心的一个组件
private final java.nio.channels.Selector nioSelector;
这里很明显就可以看出来
public class Selector implements Selectable {
public static final long NO_IDLE_TIMEOUT_MS = -1;
private static final Logger log = LoggerFactory.getLogger(Selector.class);
//这个对象就是javaNIO里面的Selector
//Selector是负责网络的建立,发送网络请求,处理实际的网络IO
//所以他是最核心的一个组件
private final java.nio.channels.Selector nioSelector;
//broker 和 Kafkachannel的映射
//这的Kafkachannel就类似于Socketchannel
//代表的就是一个网络连接
private final Map<String, KafkaChannel> channels;
//已经完成发送的请求
private final List<Send> completedSends;
//已经接收到的,并且已经处理完了的响应
private final List<NetworkReceive> completedReceives;
//已经接收到了,但是还没来得及处理的响应
//一个连接,对应一个队列
private final Map<KafkaChannel, Deque<NetworkReceive>> stagedReceives;
private final Set<SelectionKey> immediatelyConnectedKeys;
//没有建立连接的额主机
private final List<String> disconnected;
//完成建立连接的主机
private final List<String> connected;
//建立连接失败的主机
private final List<String> failedSends;
这就是他比较重要的一些参数
其实Selector就是NetworkClient的一个属性
接下来我们来看一下KafkaChannel
在上面代码里点进去
//TODO Kafkachannel就是对Socketchannel进行了封装
public class KafkaChannel {
//一个broker对应一个Kafkachannel
//这个id就是broker的id
private final String id;
//这个里面会有Socketchannel(这里要猜测一下)
private final TransportLayer transportLayer;
private final Authenticator authenticator;
private final int maxReceiveSize;
//接收到的响应
private NetworkReceive receive;
//发送出去的请求
private Send send;
public interface TransportLayer extends ScatteringByteChannel, GatheringByteChannel
/**
* returns underlying socketChannel
* 这个核心的组件就是javaNIO 里面的SocketChannel
*/
SocketChannel socketChannel();
最后我们来整理一下他的一个流程图
5.5.5如果网络没有建立会发送消息吗
之前我们分析到步骤四了,继续看下面的步骤,先回顾一下步骤四
/**
* 步骤四:
* 检查与要发送数据的主机的网络是否已经建立好
*/
if (!this.client.ready(node, now)) {
//如果返回的是false !false代码就进来了
//移除result 里面要发送消息的主机
//第一次进来这所有的主机都会被移除
iter.remove();
notReadyTimeout = Math.min(notReadyTimeout, this.client.connectionDelay(node, now));
}
接下来就是步骤五了
/**
* 步骤五:
* 有可能要发送的partition有很多个
* 很有可能一些partition的leader partition是在同一台服务器上面
* 按照broker进行分组,同一个broker的partition为同一组
*/
//第一次进来的时候,网络没有建立,这是不执行的
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);
}
}
然后是步骤六
/**
* 步骤六:
* 放弃超时的batch
*
* 第一次进来连消息都没有发送出去,也就没有超时这一说了
*/
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);
到了步骤七还是不会执行
/**
* 步骤七:
* 创建发送消息的请求
*
*/
//如果网络没有建立好的话 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;
}
for (ClientRequest request : requests)
client.send(request, now);
到了最后的步骤八
/**
* 步骤八:
* 真正执行网络操作的都是NetworkClient这个组件
* 包括;发送请求,接受请求
*/
//这就是去建立连接
this.client.poll(pollTimeout, now);
5.5.6producer与broker建立连接
由上一节可以知道,如果网络没有建立好那么后面的步骤就不会执行了,会先去步骤八建立网络连接,但是建立连接之前,会先进行一个初始化,这个初始化之前就说过,是在步骤四的ready方法进行的。
public boolean ready(Node node, long now) {
//如果当前检查的节点为null,抛异常
if (node.isEmpty())
throw new IllegalArgumentException("Cannot connect to empty node " + node);
//判断要发送消息的主机,是否具备发送消息的条件,主要就是网络有没有建立好
if (isReady(node, now))
return true;
//判断是否可以尝试去建立网络
if (connectionStates.canConnect(node.idString(), now))
// if we are interested in sending to a node and we don't have a connection to it, initiate one
//初始化连接
initiateConnect(node, now);
return false;
}
点进来会看到
rivate void initiateConnect(Node node, long now) {
String nodeConnectionId = node.idString();
try {
log.debug("Initiating connection to node {} at {}:{}.", node.id(), node.host(), node.port());
this.connectionStates.connecting(nodeConnectionId, now);
//这里做了初始化
//TODO 尝试建立连接
selector.connect(nodeConnectionId,
new InetSocketAddress(node.host(), node.port()),
this.socketSendBuffer,
this.socketReceiveBuffer);
} catch (IOException e) {
/* attempt failed, we'll try again after the backoff */
connectionStates.disconnected(nodeConnectionId, now);
/* maybe the problem is our metadata, update it */
metadataUpdater.requestUpdate();
log.debug("Error connecting to node {} at {}:{}:", node.id(), node.host(), node.port(), e);
}
}
再看connect方法,点进去还是Selectable接口,看selector实现
找到connect方法,可以看到这就是最基本的一些NIO的操作
@Override
public void connect(String id, InetSocketAddress address, int sendBufferSize, int receiveBufferSize) throws IOException {
if (this.channels.containsKey(id))
throw new IllegalStateException("There is already a connection for id " + id);
//获取到 SocketChannel
SocketChannel socketChannel = SocketChannel.open();
//设置为非阻塞模式
socketChannel.configureBlocking(false);
Socket socket = socketChannel.socket();
socket.setKeepAlive(true);
//设置一些网络参数。
if (sendBufferSize != Selectable.USE_DEFAULT_BUFFER_SIZE)
socket.setSendBufferSize(sendBufferSize);
if (receiveBufferSize != Selectable.USE_DEFAULT_BUFFER_SIZE)
socket.setReceiveBufferSize(receiveBufferSize);
//这里如果不为true,就要开启Nagle算法
//会把网络中的一些小的网络数据包,组成一个大的数据包
//然后再发送出去
//因为它认为网络中有大量小的数据包在传输其实会影响网络拥塞
//这里不能为false,因为有可能有些数据包就是比较小,如果这样处理就不发送了,就延迟了
socket.setTcpNoDelay(true);
boolean connected;
try {
//尝试与服务器去连接
//因为是非阻塞的
//有可能立马连接成功,如果成功了就返回true
//也有可能很久蔡成功,那就暂时返回false
connected = socketChannel.connect(address);
} catch (UnresolvedAddressException e) {
socketChannel.close();
throw new IOException("Can't resolve address: " + address, e);
} catch (IOException e) {
socketChannel.close();
throw e;
}
// SocketChannel往Selector上注册了一个OP_CONNECT事件
SelectionKey key = socketChannel.register(nioSelector, SelectionKey.OP_CONNECT);
//根据SocketChannel封装出来一个KafkaChannel
KafkaChannel channel = channelBuilder.buildChannel(id, key, maxReceiveSize);
//把key和KafkaChannel关联起来,后面使用起来比较方便
//可以根据key找到KafkaChannel
//也可以根据KafkaChannel找到key
key.attach(channel);
//缓存起来
this.channels.put(id, channel);
//正常情况下这的网络不能完成连接
//如果在这不能完成连接,那会在哪里完成最后的网络连接呢
//答案就是在步骤八那里
//如果立马就连接上了
if (connected) {
// OP_CONNECT won't trigger for immediately connected channels
log.debug("Immediately connected to node {}", channel.id());
immediatelyConnectedKeys.add(key);
//取消前面注册的事件OP_CONNECT
key.interestOps(0);
}
}
这里建立连接的过程还是比较曲折,到这还没有最终完成连接,我们继续探索步骤八,找到答案
终于来到了步骤八
/**
* 步骤八:
* 真正执行网络操作的都是NetworkClient这个组件
* 包括;发送请求,接受请求
*/
//这就是去建立连接
this.client.poll(pollTimeout, now);
我们进去看一下,来到NetworkClient找到poll方法
@Override
public List<ClientResponse> poll(long timeout, long now) {
//步骤一:封装了一个拉取元数据的请求
long metadataTimeout = metadataUpdater.maybeUpdate(now);
try {
/**
* 在这个方法里面涉及到kafka的网络的方法,目前没有看到网络这个模块所以先不太用去关心,
* 大概知道是如何获取到元数据即可,后面看到网络模块再来看这里的网络处理。
*
*/
//步骤二:发送请求,进行复杂的网络操作
//目前不用太过关心,先知道这里会发送网络请求就可以了
//TODO 执行网络IO操作
this.selector.poll(Utils.min(timeout, metadataTimeout, requestTimeoutMs));
} catch (IOException e) {
log.error("Unexpected error during I/O", e);
}
这里我们可以看到最终是由selector对象来执行的poll方法,那我们就去Selector这里来找一下poll方法
@Override
public void poll(long timeout) throws IOException {
if (timeout < 0)
throw new IllegalArgumentException("timeout should be >= 0");
clear();
if (hasStagedReceives() || !immediatelyConnectedKeys.isEmpty())
timeout = 0;
/* check ready keys */
long startSelect = time.nanoseconds();
//从Selector上找有多少个key注册了
int readyKeys = select(timeout);
long endSelect = time.nanoseconds();
this.sensors.selectTime.record(endSelect - startSelect, time.milliseconds());
//因为是场景驱动的方式
//刚刚确实注册了一个key
if (readyKeys > 0 || !immediatelyConnectedKeys.isEmpty()) {
//立马就要对这个Selector上面的key进行处理。
pollSelectionKeys(this.nioSelector.selectedKeys(), false, endSelect);
pollSelectionKeys(immediatelyConnectedKeys, true, endSelect);
}
addToCompletedReceives();
long endIo = time.nanoseconds();
this.sensors.ioTime.record(endIo - endSelect, time.milliseconds());
// we use the time at the end of select to ensure that we don't close any connections that
// have just been processed in pollSelectionKeys
maybeCloseOldestConnection(endSelect);
}
看一下处理方法pollSelectionKeys
private void pollSelectionKeys(Iterable<SelectionKey> selectionKeys,
boolean isImmediatelyConnected,
long currentTimeNanos) {
//获取到所有的key
Iterator<SelectionKey> iterator = selectionKeys.iterator();
//遍历所有的key
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
//根据key来找到KafkaChannel
KafkaChannel channel = channel(key);
// register all per-connection metrics at once
sensors.maybeRegisterConnectionMetrics(channel.id());
if (idleExpiryManager != null)
idleExpiryManager.update(channel.id(), currentTimeNanos);
try {
/* complete any connections that have finished their handshake (either normally or immediately) */
/**
* 代码第一次进来要走到这,因为前面注册的是
* SelectionKey key = socketChannel.register(nioSelector, SelectionKey.OP_CONNECT);
*
*/
if (isImmediatelyConnected || key.isConnectable()) {
//TODO 核心代码
//去最后完成网络的连接,
//如果初始化的时候没有完成网络连接的话,这一定会完成网络的连接
if (channel.finishConnect()) {
//网络连接完成以后,就把这个channel存储到connected对象里面
this.connected.add(channel.id());
this.sensors.connectionCreated.record();
SocketChannel socketChannel = (SocketChannel) key.channel();
log.debug("Created socket with SO_RCVBUF = {}, SO_SNDBUF = {}, SO_TIMEOUT = {} to node {}",
socketChannel.socket().getReceiveBufferSize(),
socketChannel.socket().getSendBufferSize(),
socketChannel.socket().getSoTimeout(),
channel.id());
} else
continue;
}
至此我们的网络连接就真正的建立好了
5.5.7完成连接建立的流程图
5.5.8生产者发送请求
首先我们要知道,要使用NIO发送请求,或者读取响应,那么需要往Selector上绑定两个事件
SelectionKey.OP_WRITE:写数据,发送网络请求
SelectionKey.OP_READ:读数据,接收响应
我们接着上一节最后的代码看一下
//TODO 核心代码
//去最后完成网络的连接,
//如果初始化的时候没有完成网络连接的话,这一定会完成网络的连接
if (channel.finishConnect()) {
我们来看一下这里具体做了些什么
public boolean finishConnect() throws IOException {
return transportLayer.finishConnect();
}
继续跟进去
boolean finishConnect() throws IOException;
查看实现类PlaintextTransportLayer
@Override
public boolean finishConnect() throws IOException {
//完成最后的网络的连接
boolean connected = socketChannel.finishConnect();
//如果连接完成了以后
if (connected)
//取消了OP_CONNECT事件
//增加了OP_READ事件
//这说明了这个key对应的kafkaChannel可以接受服务端发送回来的响应了。
key.interestOps(key.interestOps() & ~SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
return connected;
}
然后我们再来回顾一下sender线程,他其实就是一个死循环,第一次循环之后连接就建立好了,之后再循环的时候呢,这次再到步骤四网络就建立好了
接下来就会执行步骤五,对对分区进行分组,那么这么做有什么意义呢?
此时步骤六先跳过,来到步骤七,在这里就可以看到步骤五的意义了
/**
* 步骤七:
* 创建发送消息的请求
*
* 创建请求
* 我们往partition上面去发送消息的时候,有一些partition在同一台服务器上面
* 如果一个分区一个分区的发送网络请求,那网络请求就会非常频繁
*
* 步骤五会把发往同一个broker上面的partition的数据组合为一个请求
* 然后统一一次发送过去
* 这样就减少了网络请求
*/
//如果网络没有建立好的话 batches其实是空
//这不会执行
List<ClientRequest> requests = createProduceRequests(batches, now);
接下来就到了发送请求的代码
//发送请求
for (ClientRequest request : requests)
client.send(request, now);
跟进来看一下
public interface KafkaClient extends Closeable
public void send(ClientRequest request, long now);
还是要去看 NetworkClient的send方法
@Override
public void send(ClientRequest request, long now) {
String nodeId = request.request().destination();
if (!canSendRequest(nodeId))
throw new IllegalStateException("Attempt to send a request to node " + nodeId + " which is not ready.");
//TODO关键代码
doSend(request, now);
}
private void doSend(ClientRequest request, long now) {
request.setSendTimeMs(now);
//这往inFlightRequests组件里面存request请求
//存储的就是还没有收到响应的请求
//这里面默认最多能存5个请求
//可以猜测一下,如果请求发送出去了
//然后也成功的收到了响应,后面就会到这把这个响应移除
this.inFlightRequests.add(request);
//TODO
selector.send(request.request());
}
来看send方法
public interface Selectable
public void send(Send send);
这里就是Selector的send方法
public void send(Send send) {
//获取到一个KafkaChannel
KafkaChannel channel = channelOrFail(send.destination());
try {
//TODO 往channel上面存一个发送的请求
channel.setSend(send);
} catch (CancelledKeyException e) {
this.failedSends.add(send.destination());
close(channel);
}
}
public void setSend(Send send) {
if (this.send != null)
throw new IllegalStateException("Attempt to begin a send operation with prior send operation still in progress.");
//往KafkaChannel里面绑定一个发送出去的请求
this.send = send;
//关键代码
//这绑定了一个OP_WRITE事件
//一旦绑定了这个事件以后,我们就可以往服务端发送请求了
this.transportLayer.addInterestOps(SelectionKey.OP_WRITE);
}
再回到sender线程
因为最后把请求发送出去的还是这个selector对象,所以我们再次来到 NetworkClient的poll方法
然后找到selector的poll方法
在这里我们之前看过这要处理相关的key,用的是pollSelectionKeys方法
//核心代码,处理发送请求的事件
if (channel.ready() && key.isWritable()) {
//这里就是要往服务端发送数据了
Send send = channel.write();
if (send != null) {
this.completedSends.add(send);
this.sensors.recordBytesSent(channel.id(), send.size());
}
}
这次要关注的是这个分支
public Send write() throws IOException {
Send result = null;
//send方法就是发送网络请求的方法
if (send != null && send(send)) {
result = send;
send = null;
}
return result;
}
private boolean send(Send send) throws IOException {
//最终执行发送请求的代码是在这
send.writeTo(transportLayer);
//如果已经完成网络请求的发送
if (send.completed())
//然后就移除OP_WRITE事件
transportLayer.removeInterestOps(SelectionKey.OP_WRITE);
return send.completed();
}
至此生产者的网络请求终于发送出去了