5.5sender线程运行流程

之前我们知道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();
    }

至此生产者的网络请求终于发送出去了

5.5.9发送网络请求的流程图

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值