Kafka源码分析8-网络设计

977 阅读9分钟

欢迎大家关注 github.com/hsfxuebao/j… ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

Kafka源码分析7-筛选可以发送消息的broker 中如何建立网络连接,本章将详细讲解这部分内容。

网络核心组件分析

我们先来介绍下网络设计的核心组件,NetworkClientSelectorKafkaChannelTransportLayer 对应的代码如下:

public class NetworkClient implements KafkaClient {

    private static final Logger log = LoggerFactory.getLogger(NetworkClient.class);

    /* the selector used to perform network i/o */
    private final Selectable selector;

    private final MetadataUpdater metadataUpdater;

    private final Random randOffset;

    /* the state of each node's connection */
    private final ClusterConnectionStates connectionStates;

    /* the set of requests currently being sent or awaiting a response */
    private final InFlightRequests inFlightRequests;

   // 省略。。。
}

NetworkClient初始化而言,大家就应该已经搞清楚了,NetworkClient主要是一个网络通信组件,底层核心的Selector负责最最核心的建立连接、发起请求、处理实际的网络IO,初始化的入口初步找到了,kafka自己封装的一些组件,但是,他的底层是有最最核心的Java NIO的Selector。

/***
 * todo 这个Selector是kafka自己封装出来
 * 基于java nio 里面的selector去封装的
 */
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 Map<String, KafkaChannel> closingChannels;
    //没有建立连接的主机
    private final List<String> disconnected;
    // 建立连接的主机
    private final List<String> connected;
    //建立连接失败的主机。
    private final List<String> failedSends;
    private final Time time;
    private final SelectorMetrics sensors;
    private final String metricGrpPrefix;
    private final Map<String, String> metricTags;
    private final ChannelBuilder channelBuilder;
    private final int maxReceiveSize;
    private final boolean metricsPerConnection;
    private final IdleExpiryManager idleExpiryManager;
}

最最核心的一点,就是在KafkaSelector的底层,其实就是封装了原生的Java NIO的Selector,很关键的组件,就是一个多路复用组件,他会一个线程调用他直接监听多个网络连接的请求和响应。相关参数的定义如下:

  • maxReceiveSize,最大可以接收的数据量的大小

  • connectionsMaxIdle,每个网络连接最多可以空闲的时间的大小,就要回收掉

  • Map<String, KafkaChannel> channels,这里保存了每个broker id到Channel的映射关系,对于每个broker都有一个网络连接,每个连接在NIO的语义里,都有一个对应的SocketChannel,我们估计,KafkaChannel封装了SocketChannel

  • List completedSends,已经成功发送出去的请求

  • List completedReceives,已经接收回来的响应而且被处理完了

  • Map<KafkaChannel, Dequeue>,每个Broker的收到的但是还没有被处理的响应

  • conneted、disconnected、failedSends,已经成功建立连接的brokers,以及还没成功建立连接的brokers,发送请求失败的brokers

  • channels broker id对应一个网络连接,一个网络连接对应一个KafkaChannel,底层对应的是SocketChannel,SocketChannel对应的是最最底层的网络通信层面的一个Socket,套接字通信,Socket通信,TCP

  • Send,应该是说要交给这个底层的Channel发送出去的请求,可能会不断的变换的,因为发送完一个请求需要发送下一个请求

  • NetworkReceive,这个Channel最近一次读出来的响应,先暂存在这里,也是会不断的变换的,因为会不断的读取新的响应数据

  • TransportLayer是封装了底层的Java NIO的SocketChannel

Selector内部的源码一定要带着大家深入到每个细节的研究,因为这是完全经历过全世界大量的、复杂的、大规模的场景考验的一套网络通信的框架,基于NIO封装的一套网络通信的框架

kafkaChannel 和PlaintextTransportLayer如下:

public class KafkaChannel {
    private final String id;
    private final TransportLayer transportLayer;
    private final Authenticator authenticator;
    private final int maxReceiveSize;
    // 读缓存,底层使用ByteBuffer实现
    private NetworkReceive receive;
    // 写缓存,底层使用ByteBuffer实现
    private Send send;
    // Track connection and mute state of channels to enable outstanding requests on channels to be
    // processed after the channel is disconnected.
    private boolean disconnected;
    private boolean muted;
}
public class PlaintextTransportLayer implements TransportLayer {
    private final SelectionKey key;
    private final SocketChannel socketChannel;
    private final Principal principal = KafkaPrincipal.ANONYMOUS;
}

Network、Selector、Channel他们是如何初始化的,kafka如何封装的,以及与原生Java NIO的Selector、Channel的关系是如何的,结合如下框图进行理解:

代码分析

接着 Kafka源码分析7-筛选可以发送消息的broker 进行如下分析,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);
        //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);
    }
}

接着看org.apache.kafka.common.network.Selector 的connect() 方法:

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);

    //要是了解java NIO编程的同学,这些代码就是一些基本的
    //代码,跟我们NIO编程的代码是一模一样。

    //获取到SocketChannel
    SocketChannel socketChannel = SocketChannel.open();
    //设置为非阻塞的模式
    socketChannel.configureBlocking(false);
    Socket socket = socketChannel.socket();
    socket.setKeepAlive(true);
    //设置一些参数
    //这些网络的参数,我们之前在分析Producer的时候给大家看过
    //都有一些默认值。
    if (sendBufferSize != Selectable.USE_DEFAULT_BUFFER_SIZE)
        socket.setSendBufferSize(sendBufferSize);
    if (receiveBufferSize != Selectable.USE_DEFAULT_BUFFER_SIZE)
        socket.setReceiveBufferSize(receiveBufferSize);

    //这个的默认值是false,代表要开启Nagle的算法
    //它会把网络中的一些小的数据包收集起来,组合成一个大的数据包
    //然后再发送出去。因为它认为如果网络中有大量的小的数据包在传输
    //其实是会影响网络拥塞。

    //kafka一定不能把这儿设置为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);
    //所以正常情况下,这儿网络不能完成连接。
    //如果这儿不能完成连接。大家猜一下
    //kafka会在哪儿完成网络最后的连接呢?

    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);
    }
}

SocketChannel的参数

  • configureBlocking 设置为非阻塞模式

  • epalive的意思,主要是避免客户端和服务端任何一方如果断开连接之后,别人不知道,一直保持着网络连接的资源;所以设置这个之后,2小时内如果双方没有任何通信,那么发送一个探测包,根据探测包的结果保持连接、重新连接或者断开连接

  • 设置socket的发送和接收的缓冲区的大小,分别是128kb和32kb,这个缓冲区的大小一般都是在NIO编程里需要自己去设置的

  • TcpNoDelay,如果默认是设置为false的话,那么就开启Nagle算法,就是把网络通信中的一些小的数据包给收集起来,组装成一个大的数据包然后再一次性的发送出去,如果大量的小包在传递,会导致网络拥塞;如果设置为true的话,意思就是关闭Nagle,让你发送出去的数据包立马就是通过网络传输过去,所以这个参数大家也要注意下

NetworkClient、Selector、KafkaChannel、ConnectStates,这些东西是极为值得我们来研究的,对我们的技术底层的功底的夯实极为有好处,假设我们真的要去开发一个网络通信的程序,打算基于NIO来做

对于客户端而言,他的SocketChannel到底应该如何来设置呢?你就可以参考人家做法:KeepAlive、TcpNoDelay、SocketBuffer

connect(address)

如果这个SocketChannel是被设置为非阻塞模式的话,那么对这个connect方法的调用,会初始化一个非阻塞的连接请求,如果这个发起的连接立马就成功了,比如说客户端跟要连接的服务端都在一台机器上,此时就会出现一个立马就连接成功的情况,然后就会返回一个true;否则只要不是那种立马可以连接成功的情况,就会返回一个false,接着就需要在后面去调用SocketChannel的finishConnect方法,去完成最终的连接

缓存kafkaChannel

初始化了一个SocketChannel然后就发起了一个连接请求,接着不管连接请求是成功还是暂时没成功,都需要把这个SocketChannel给缓存起来,接下来你才可以基于这个东西去完成连接,或者是发起读写请求

发起连接之后,直接就把这个SocketChannel给注册到Selector上去了,让Selector监视这个SocketChannel的OP_CONNECT事件,就是是否有人同意跟他建立连接,会获取到一个SelectionKey

接着就是将SelectionKey、brokerid封装为了KafkaChannel,他是先把SelectionKey封装到TransportLayer里面去(SelectionKey底层是跟SocketChannel是一一对应起来),Authenticator,brokerid,直接封装一个KafkaChannel

大概可以认为是把一个核心的组件跟SelectionKey给关联起来,后续在通过SelectionKey进行网络请求和相应的处理的时候,就可以从SelectionKey里获取出来SocketChannel,可以获取出来之前attach过的一个核心组件,复制请求响应的处理。缓存起来立即建立好连接的SelectionKey

总结

原生NIO编程的时候,可以把他原生的 API和组件封装一下,就可以基于你自己的需求实现不同的功能了,你一定要学会进行一定的缓存机制的设计,比如说针对多个机器进行连接,那么对应的连接组件就需要进行缓存

参考文档:

史上最详细kafka源码注释(kafka-0.10.2.0-src)

kafka技术内幕-图文详解Kafka源码设计与实现

Kafka 源码分析系列