基于Java NIO的Kafka底层网络层源码和架构

Kafka是数据和网络IO密集型组件,尤其是服务器端,基本要求就是数据传输和网络IO的高吞吐量和低系统开销。Kafka使用java NIO 封装了一套自己的底层网络层。从这些代码中,既可以看到基本的Java NIO的运行原理和使用方式,也能看到Kafka为了满足自身业务需求而进行的封装和扩展。本文从代码层面,详细解析Kafka底层网络层的具体实现。
如果不熟悉Java NIO的同学,可以参考IBM的相关文档《深入分析 Java I/O 的工作机制》,同时,《 java NIO服务端和客户端代码实现》这篇博客也简单介绍了基于Java NIO的客户端和服务器端基本代码。

1. KSelector构造方法介绍

Kafka自己对NIO的Selector进行了封装,放在了org.apache.kafka.common.network.Selector,为了与java原生的java.nio.channels.Selector区分,我参照《Apache Kafka源码剖析》一书的叫法,将org.apache.kafka.common.network.Selector叫做KSelector。Kafka的客户端与服务器端的通信以及服务器之间的通信,底层都是用KSelector进行。

KSelector的构造方法:

public Selector(int maxReceiveSize, long connectionMaxIdleMs, Metrics metrics, Time time, String metricGrpPrefix, Map<String, String> metricTags, boolean metricsPerConnection, ChannelBuilder channelBuilder) {
        try {
            this.nioSelector = java.nio.channels.Selector.open();
        } catch (IOException e) {
            throw new KafkaException(e);
        }
        this.maxReceiveSize = maxReceiveSize;
        this.connectionsMaxIdleNanos = connectionMaxIdleMs * 1000 * 1000;
        this.time = time;
        this.metricGrpPrefix = metricGrpPrefix;
        this.metricTags = metricTags;
        this.channels = new HashMap<>();
        this.completedSends = new ArrayList<>();
        this.completedReceives = new ArrayList<>();
        this.stagedReceives = new HashMap<>();
        this.immediatelyConnectedKeys = new HashSet<>();
        this.connected = new ArrayList<>();
        this.disconnected = new ArrayList<>();
        this.failedSends = new ArrayList<>();
        this.sensors = new SelectorMetrics(metrics);
        this.channelBuilder = channelBuilder;
        // initial capacity and load factor are default, we set them explicitly because we want to set accessOrder = true
        this.lruConnections = new LinkedHashMap<>(16, .75F, true);
        currentTimeNanos = time.nanoseconds();
        nextIdleCloseCheckTime = currentTimeNanos + connectionsMaxIdleNanos;
        this.metricsPerConnection = metricsPerConnection;
    }

KSelector对java.nio.channels.Selector进行了封装,因此在构造函数中,通过java.nio.channels.Selector.open()打开了一个java.nio.channels.Selector对象nioSelector
创建完了Selector以后,需要创建SocketChannel,即通道。Selector和Channel是Java NIO中最重要的两个概念。这里有一个形象的比喻来形容Selector和SocketChannel:我们可以把从武汉到北京的交通比作java的端对端通信,那么,每一列火车都可以称作一个SocketChannel,即一个信息通道。而Selector则相当于北京西站或者武昌站的调度室,用来对所有的Channel进行调度管理。
SocketChannel注册到Selector: SocketChannel 创建完毕以后,为了将自己纳入到Selector的管理中,会向Selector注册自己,注册的时候,需要指定所关注的事件(SelectionKey.OP_READ、SelectionKey.OP_WRITE)。我们可以把火车进站看做读操作(有消息进来),把火车出站看做写操作(向远程发送消息),那么,在火车进站以前,需要提前告知终点站自己会在某个不确定时刻进站。当一个Channel提前注册的相应事件发生(火车到站),Selector(车站调度室)会通过广播告知大家车辆进站,候车旅客可以准备上车了。

java NIO有以下网络事件的定义:

SelectionKey.OP_READ:Socket 读事件,以从远程发送过来了相应数据
SelectionKey.OP_WRITE:Socket写事件,即向远程发送数据
SelectionKey.OP_CONNECT:Socket连接事件,用来客户端同远程Server建立连接的时候注册到Selector,当连接建立以后,即对应的SocketChannel已经准备好了,用户可以从对应的key上取出SocketChannel.
SelectionKey.OP_ACCEPT:Socket连接接受事件,用来服务器端通过ServerSocketChannel绑定了对某个端口的监听,然后会让其SocketChannel对应的socket注册到服务端的Selector上,并关注该OP_ACCEPT事件。

Selector.selectedKeys()会返回对应channel已经ready(即已经发生了读写事件,可以直接获取数据或者发送数据了)的这些SelectionKey对象。通过这个SelectionKey对象的 中的SocketChannel就可以获取到通信的数据。

注意:通信是双发的,无论是客户端还是服务器端,都需要创建Selector和SocketChannel,但是,在连接建立阶段,他们有区别:

2. 服务器端创建SocketChannel和Selector

服务器端创建完毕了SocketChannel,需要在SocketChannel上监听OP_ACCEPT,以处理远程客户端的连接请求。例如,SocketServer类是负责Kafka Server端网络端口的创建、监听、通信的,SocketServer.Acceptor类使用KSelector监听9092端口以接收客户端的各种请求。SocketServer.Acceptor初始化的时候,会首先创建服务器端的SocketChannel,打开并监听9092端口:

 private def openServerSocket(host: String, port: Int): ServerSocketChannel = {
    val socketAddress =
      if(host == null || host.trim.isEmpty)
        new InetSocketAddress(port)
      else
        new InetSocketAddress(host, port)
    val serverChannel = ServerSocketChannel.open()//打开一个channel
    serverChannel.configureBlocking(false)//设置为非阻塞模式
    serverChannel.socket().setReceiveBufferSize(recvBufferSize)
    try {
      serverChannel.socket.bind(socketAddress)//监听指定端口
      info("Awaiting socket connections on %s:%d.".format(socketAddress.getHostString, serverChannel.socket.getLocalPort))
    } catch {
      case e: SocketException =>
        throw new KafkaException("Socket server failed to bind to %s:%d: %s.".format(socketAddress.getHostString, port, e.getMessage), e)
    }
    serverChannel
  }

随后,通过run()方法,将SocketChannel注册到Selector,并注册OP_ACCEPT事件,以监听客户端到该9092端口的连接请求:

def run() {
    //向selector注册channel,可以接收ACCEPT事件,只有非阻塞的serverChannel才可以注册给Selector
    serverChannel.register(nioSelector, SelectionKey.OP_ACCEPT)
    //省略
}

3. 客户端端创建SocketChannel和Selector

客户端创建完毕了SocketChannel,也需要需要将channel注册到Selector,调用SocketChannel.connect()方法,向远程发起连接请求,同时,在Selector上注册,并关注OP_CONNECT事件,当远程服务器端响应,连接建立,对应的Channel就处于Ready状态,对应客户端就可以和远程进行正式通信。KSelector.connect()java.nio.channels.Selector 进行了封装,同远程的服务器端的Socket建立连接:

@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只是一个抽象类,跟踪java内核代码,可以看到SocketChannel的具体运行时类是@Code sun.nio.ch.SocketChannelImpl
        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);
        socket.setTcpNoDelay(true);
        boolean connected;
        try {
            //在non-blocking 模式下,connect()方法返回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;
        }
        //在这个channelz注册到selector上,关注OP_CONNECT事件
        //一个key对象就代表了这次注册
        SelectionKey key = socketChannel.register(nioSelector, SelectionKey.OP_CONNECT);
      //默认情况下是PlaintextChannelBuilder
        KafkaChannel channel = channelBuilder.buildChannel(id, key, maxReceiveSize);
        key.attach(channel);
        this.channels.put(id, channel);
        //如果已经知道连接建立了,则直接将这个key添加到immediatelyConnectedKeys中,代表已经建立连接的key,后续poll()中就不需要通过
        if (connected) {
            // OP_CONNECT won't trigger for immediately connected channels
            log.debug("Immediately connected to node {}", channel.id());
            immediatelyConnectedKeys.add(key);
            key.interestOps(0);//0代表对任何key都不感兴趣,这只是暂时的
        }
    }

当服务器端已经打开并监听了9092端口,并且收到了客户端的连接请求(即OP_ACCEPT事件发生),就开始进入读或者写状态。同时,客户单的SelectionKey.OP_CONNECT发生以后,相当于连接建立成功,也开始进行读写。

注意:客户端与服务器端地位的差别,只是在建立连接的时候体现,即,建立连接的时候,服务器端会创建对应的Selector和SocketChannel,同时打开端口并监听,而客户端是创建了对应的Selector和SocketChannel以后,向远程服务器端的SocketChannel对应的端口发起连接请求。请求建立以后,在网络层,服务器端和客户端没有地位上的差别,都需要处理对方的读写请求

我们以服务端为例,看看SocketChannel如何向Selector注册OP_READ:
基于Reactor模式的Kafka服务端设计,每一个客户端连接,会创建一个SocketServerver.Processor()对象(也是一个线程)来处理客户端的请求,通过调用KSelector.register()方法,开始对远程客户端或者其它服务器的读请求(OP_READ)进行绑定和处理:

public void register(String id, SocketChannel socketChannel) throws ClosedChannelException {
        SelectionKey key = socketChannel.register(nioSelector, SelectionKey.OP_READ);
        //如果是SocketServer创建的这个对象并且是纯文本,则channelBuilder是@Code PlainTextChannelBuilder
        KafkaChannel channel = channelBuilder.buildChannel(id, key, maxReceiveSize);//构造一个KafkaChannel
        key.attach(channel);//将KafkaChannel对象attach到这个registration,以后可以通过调用SelectionKey.attachment()获得这个对象
        this.channels.put(id, channel);//记录这个Channel
    }

KSelect.register()方法,会将服务端的SocketChannel注册到服务器端的nioSelector,并关注SelectionKey.OP_READ,即,如果发生读请求,可以取出对应的Channel进行处理。这里的Channel也是Kafka经过封装以后的KafkaChannel对象,通过KafkaChannel channel = channelBuilder.buildChannel(id, key, maxReceiveSize);,然后通过调用SelectionKey.attach()方法,将KafkaChannel 附加到这个SelectionKey上。随后,任何时候,我们只需要通过调用SelectionKey.attachment()方法,就可以取出这个附加对象。注意,一个SelectionKey对象的attach()方法只可以附加一个对象,多次附加操作会使当前对象覆盖前一个对象。我们从下面buildChannel()的代码可以看到,其实KafkaChannel对象只不过是对Java原生的java.nio.channels.SocketChannel的封装而已:

public KafkaChannel buildChannel(String id, SelectionKey key, int maxReceiveSize) throws KafkaException {
        KafkaChannel channel = null;
        try {
            //transportLayer的构造函数中传入SelectionKey对象,其实就是封装了这个socket,因为一个SelectionKey与一个Socket一一对应
            PlaintextTransportLayer transportLayer = new PlaintextTransportLayer(key);
            Authenticator authenticator = new DefaultAuthenticator();
            authenticator.configure(transportLayer, this.principalBuilder, this.configs);
            //channel封装了transportLayer对象
            channel = new KafkaChannel(id, transportLayer, authenticator, maxReceiveSize);
        } catch (Exception e) {
            log.warn("Failed to create channel due to ", e);
            throw new KafkaException(e);
        }
        return channel;
    }
    public PlaintextTransportLayer(SelectionKey key) throws IOException {
        this.key = key;
        this.socketChannel = (SocketChannel) key.channel();//取出服务器端的Java NIO SocketChannel
    }

可以看到,其实创建的这个KafakChannel管理了一个TransportLayer对象,同时,TransportLayer对象管理了当前这个SelectionKey对象,这个SelectionKey对象是注册OP_READ的时候返回值,其实就代表了当前的这个SocketChannel。那么,所以,其实KafkaChannel就是管理了当前这个对应了OP_READ请求的SocketChannel:

KafkaChannel与底层SocketChannel类关系图

4.连接建立以后的数据传输

当客户端扮演Client角色的KSelector和服务器端扮演Server角色的KSelector 已经通过SocketChannel建立了连接,并且服务器端已经为他们之间的SocketChannel注册到Selector并且监听OP_ACCEPT事件,此时无论是服务器端,还是客户端,就可以正式开始传输数据了。

客户端或者服务器端,都是通过调用KSelector.send()发送数据:

    public void send(Send send) {
        KafkaChannel channel = channelOrFail(send.destination());//返回指向对应节点的一个Channel
        try {
            channel.setSend(send);//将send对象放到KafkaChannel.send中去,并没有立刻发送
        } catch (CancelledKeyException e) {
            this.failedSends.add(send.destination());
            close(channel);
        }
    }

channelOrFail(send.destination())通过远程服务器的id取出与远程终端(可能是服务器到客户端,也可能是客户端到服务器端)所建立的连接的KafkaChannel,然后通过 channel.setSend(send);将数据‘发送’出去,注意,这里并没有执行发送,真正的数据发送还是通过调用Selector对象的Selector.poll()进行的:

    @Override
    public void poll(long timeout) throws IOException {
        if (timeout < 0)
            throw new IllegalArgumentException("timeout should be >= 0");
        //在poll之前,会清空CompletesSends等等队列,这意味着,该方法的调用是不可重复的,每次poll出来的请求,都必须处理,否则下一次poll进行的时候这些结果就会丢失
        clear();
        //略
        /* check ready keys 检查已经就绪的 SelectionKey*/
        long startSelect = time.nanoseconds();
        int readyKeys = select(timeout);//等待并选择至少一个已经就绪的Key
        //略
        //如果存在已经准备好的keys
        if (readyKeys > 0 || !immediatelyConnectedKeys.isEmpty()) {
            //nioSelector.selectedKeys()是已经准备就绪的通道对应的选择键,是keys的子集
            pollSelectionKeys(this.nioSelector.selectedKeys(), false);
            pollSelectionKeys(immediatelyConnectedKeys, true);
        }
        addToCompletedReceives();//将已经完成的read请求添加到completedReceived
       //略
    }

poll()方法是KSelector真正执行数据读写的方法,职责是从自己维护的java.nio.channels.Selector中选出已经就绪的SelectionKey,通过pollSelectionKeys()方法,来对这些SelectionKey对应的SocketChannel上的数据进行处理。可见,具体的处理细节在pollSelectionKeys()方法中完成的,因此我们对该方法进行了特别详细的注释:

/**
     * 对于每一个selectionKey,从这个key中取出attach到这个key上面的KafkaChannel,channel的状态迁移时从未连接、建立连接未认证、已经认证、
     */
    private void pollSelectionKeys(Iterable<SelectionKey> selectionKeys, boolean isImmediatelyConnected) {
        Iterator<SelectionKey> iterator = selectionKeys.iterator();
        while (iterator.hasNext()) {
            SelectionKey key = iterator.next();
            iterator.remove();
            KafkaChannel channel = channel(key);//取出attach到这个key上面的KafkaChannel,注意,一个Channel只可以attach一个channel
            //略
            try {
                /* complete any connections that have finished their handshake (either normally or immediately) */
                if (isImmediatelyConnected || key.isConnectable()) {  
                    if (channel.finishConnect()) {//这个channel的连接已经建立完成
                        this.connected.add(channel.id());
                        this.sensors.connectionCreated.record();
                    } else
                        continue;//这个channel的连接尚未建立,则忽略,继续遍历下一个key
                }
                /* if channel is not ready finish prepare */
                if (channel.isConnected() && !channel.ready())
                    //如果已经建立连接但是还没有进行认证,则进行认证
                    channel.prepare();
                /* if channel is ready read from any connections that have readable data */
                //hasStagedReceive(channel)代表这个channel已经进行过读取操作,读取到的对象放入stagedReceives中
                if (channel.ready() && key.isReadable() && !hasStagedReceive(channel)) {
                    //通过KafkaChannel.read()的代码可以看到,如果返回null,代表这个channel没有读完
                    NetworkReceive networkReceive;
                    while ((networkReceive = channel.read()) != null)
                        //将channel和对应的收到的响应放入stagedReceives中,每一个channel对应一个List<networkReceive>
                         //每执行一次channel.read(),都会把读取到的部分对象封装为NetworkReceive对象,添加到这个channel对应的队列中去
                        addToStagedReceives(channel, networkReceive);

                    //当跳出循环,则stagedReceives中实际上保存着已经完成的read
                }
                /* if channel is ready write to any sockets that have space in their buffer and for which we have data */
                if (channel.ready() && key.isWritable()) {
                    Send send = channel.write();//进行真正的写操作。
                    if (send != null) {//send!=null代表发送完成
                        this.completedSends.add(send);
                        this.sensors.recordBytesSent(channel.id(), send.size());
                    }
                }
                /* cancel any defunct sockets */
                if (!key.isValid()) {//如果这个key已经不可用了,则把这个channel添加到disconnected中去
                    close(channel);
                    this.disconnected.add(channel.id());
                }

            } catch (Exception e) {
                //异常处理流程
            }
        }
    }

该方法其实是轮询处于ready状态的所有SelectionKey,对他们一一进行了线性化的处理流程:

KSelector.pollSelectionKeys方法处理流程图!]

经过一轮poll()操作,Selector会将处理结果分别保存在以下变量中,主要包括了断开连接的channel(disconnected)、建立连接的channel(connected)、接收到的数据(completedReceives)和发送出去的数据(completedSends):

- List<String> connected:已经建立连接的KafkaChannel的id的list,如果是客户端,可以通过connected知道已经和哪些远程的server建立了连接
- List<String> disconnected:出现异常的KafkaChannel的id的list,同样,如果是客户端,可以通过disconnected知道自己已经和哪些远程的server断开了连接,从而不再往这个broker上发请求,服务端如果发现远程的客户端(比如Consumer)断开了连接,则可以进行rebalance操作,将分配给这个离开的consumer的partition再重新分配给其它处于正常连接状态的broker
- List<Send> completedSends:已经完成的发送请求,客户端或者服务端可以通过查询completedSends,知道哪些请求已经发送出去。我们在上文中已经说过,在调用KSelector.send()方法的时候,只是将待发送的对象交付给KafkaChannel,并没有真正执行网络IO,直到在poll()中才会执行。因此completedSends可以告知KSelector的使用者,这个请求是否**真正已经发送了出去**。
- List<NetworkReceive> completedReceives:已经接收到的数据,服务端拿到这些已经接收到的数据以后,会将这些数据交付给具体数据的处理者进行处理。我会在我的另外一篇博客中讲解这些数据的交付过程。

这样,Selector的上层使用者,在调用完一次poll()操作以后,就可以通过查询这些变量的值获取这次poll()操作的各种结果,进而进行进一步操作。

以KafkaConsumer发送消息消费请求的过程为例,解析客户端和服务端对消息的解析过程

这里,我用KafkaConsumer在消费数据的时候向远程Kafka Broker发送数据消费请求的过程为例,解析客户端(Kafka Consumer)如何将请求发送出去的,以及请求的数据格式。
KafkaConsumer使用ConsumerNetworkClient作为自己的NIO客户端,在ConsumerNetworkClient.trySend()中进行消息的发送:

    private boolean trySend(long now) {
        // send any requests that can be sent now
        boolean requestsSent = false;
        for (Map.Entry<Node, List<ClientRequest>> requestEntry: unsent.entrySet()) {
            Node node = requestEntry.getKey();
            Iterator<ClientRequest> iterator = requestEntry.getValue().iterator();
            while (iterator.hasNext()) {
                ClientRequest request = iterator.next();
                if (client.ready(node, now)) {
                    client.send(request, now);//NetworkClient.send()是真正执行发送,并不是先放到unsent中
                    iterator.remove();//从unsent中删除这个请求
                    requestsSent = true;
                }
            }
        }
        return requestsSent;
    }

client.send(request, now);执行了消息的发送,这里的client是NetworkClient对象,NetworkClient.send()方法会调用刚才讲到的KSelector.send()方法,将请求(ClientRequest对象)放到对应的Channel的缓存中去。
当KafkaConsumer调用KSelector.poll() -> KSelector.pollSelectionKeys(),就会将这些缓存的请求发送出去,即,真正的执行发送,是通过调用KafkaChannel.send()方法:

    private boolean send(Send send) throws IOException {
        send.writeTo(transportLayer);
        if (send.completed())
            transportLayer.removeInterestOps(SelectionKey.OP_WRITE);

        return send.completed();
    }

对应刚才举例的KafkaConsumer发送消费消息请求而言,这里的Send对象的运行时对象是RequestSend对象:

这里写图片描述

我们一起来看RequestSend的构造方法:

    public RequestSend(String destination, RequestHeader header, Struct body) {
        super(destination, serialize(header, body));
        this.header = header;
        this.body = body;
    }


   public static ByteBuffer serialize(RequestHeader header, Struct body) {
        ByteBuffer buffer = ByteBuffer.allocate(header.sizeOf() + body.sizeOf());
        header.writeTo(buffer);
        body.writeTo(buffer);
        buffer.rewind();
        return buffer;
    }

通过调用serialize()方法,将header和body信息拼接到一个ByteBuffer对象中作为统一的body,然后调用了父类NetworkSend的构造方法进行消息的组装:

 public NetworkSend(String destination, ByteBuffer... buffers) {
        super(destination, sizeDelimit(buffers));
    }

    private static ByteBuffer[] sizeDelimit(ByteBuffer[] buffers) {
        int size = 0;
        for (int i = 0; i < buffers.length; i++)
            size += buffers[i].remaining();
        ByteBuffer[] delimited = new ByteBuffer[buffers.length + 1];
        delimited[0] = ByteBuffer.allocate(4);//固定4字节,存放消息长度信息
        delimited[0].putInt(size);
        delimited[0].rewind();
        System.arraycopy(buffers, 0, delimited, 1, buffers.length);
        return delimited;
    }

通过sizeDelimit()进行了消息的重新组装,其实 就是计算了每个body的消息长度存放为4个字节,并放在body前面。这样,服务器端在解析数据的时候,首先提取消息中的前4个字节,获取消息长度,然后根据消息的size创建指定大小的ByteBuffer接收消息,这个解析过程,我们通过跟踪上面的poll()方法,知道读取消息是通过KafkaChannel.read()方法,将消息组装成NetworkReceive()对象,放到stagedReceives中的过程。我们跟踪KafkaChannel.read() -> KafkaChannel.receive() -> NetworkReceive.readFrom() -> NetworkReceive.readFromReadableChannel(),可以看到服务器端的解析过程,即先读取头四字节的消息长度字段,然后根据消息长度,创建指定长度的ByteBuffer,读取消息体存入其中。具体代码读者有兴趣自己阅读,不再赘述。


以上就是Kafka的网络层代码的基本处理流程。Kafka基于Java NIO实现了端到端通信,Java NIO的高效通信为Kafka这种网络密集型应用提供了网络层的通信保障。Kafka对Java NIO的封装也非常清晰,对这部分代码的学习可以让我们看到高手是如何使用Java NIO构造一个高效的网络通信系统的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值