一、总体概述
大家都知道在 Java NIO 有个三剑客,即「SocketChannel通道」、「Buffer读写」、「Selector多路复用器」,上篇已经讲解了前2个角色,今天我们来聊聊最后一个重要的角色。
Kafka Selector 是对 Java NIO Selector 的二次封装,主要功能如下:
- 提供网络连接以及读写操作
- 对准备好的事件进行收集并进行网络操作
为了方便大家理解,所有的源码只保留骨干。
二、Selector 封装过程
github 源码地址如下:
https://github.com/apache/kafka/blob/2.7/clients/src/main/java/org/apache/kafka/common/network/Selector.java。
org.apache.kafka.common.network.Selector,该类是 Kafka 网络层最重要最核心的实现,也是非常经典的工业级通信框架实现,为了简化,这里称为 Kselector, 接下来我们先来看看该类的重要属性字段:
public class Selector implements Selectable, AutoCloseable { // 在 Java NIO 中用来监听网络I/O事件 private final java.nio.channels.Selector nioSelector; // channels 管理 private final Map<String, KafkaChannel> channels; // 发送完成的Send集合 private final List<Send> completedSends; // 已经接收完毕的请求集合 private final LinkedHashMap<String, NetworkReceive> completedReceives; // 立即连接的集合 private final Set<SelectionKey> immediatelyConnectedKeys; // 关闭连接的 channel 集合 private final Map<String, KafkaChannel> closingChannels; // 断开连接的节点集合 private final Map<String, ChannelState> disconnected; // 连接成功的节点集合 private final List<String> connected; // 发送失败的请求集合 private final List<String> failedSends; // 用来构建 KafkaChannel 的工具类 private final ChannelBuilder channelBuilder; // 最大可以接收的数据量大小 private final int maxReceiveSize; // 空闲超时到期连接管理器 private final IdleExpiryManager idleExpiryManager; // 用来管理 ByteBuffer 的内存池 private final MemoryPool memoryPool; // 初始化 Selector public Selector(int maxReceiveSize, long connectionMaxIdleMs, int failedAuthenticationDelayMs, Metrics metrics, Time time, String metricGrpPrefix, Map<String, String> metricTags, boolean metricsPerConnection, boolean recordTimePerConnection, ChannelBuilder channelBuilder, MemoryPool memoryPool, LogContext logContext) { try { this.nioSelector = java.nio.channels.Selector.open(); } catch (IOException e) { throw new KafkaException(e); } this.maxReceiveSize = maxReceiveSize; this.time = time; this.channels = new HashMap<>(); this.explicitlyMutedChannels = new HashSet<>(); this.outOfMemory = false; this.completedSends = new ArrayList<>(); this.completedReceives = new LinkedHashMap<>(); this.immediatelyConnectedKeys = new HashSet<>(); this.closingChannels = new HashMap<>(); this.keysWithBufferedRead = new HashSet<>(); this.connected = new ArrayList<>(); this.disconnected = new HashMap<>(); this.failedSends = new ArrayList<>(); this.log = logContext.logger(Selector.class); this.sensors = new SelectorMetrics(metrics, metricGrpPrefix, metricTags, metricsPerConnection); this.channelBuilder = channelBuilder; this.recordTimePerConnection = recordTimePerConnection; this.idleExpiryManager = connectionMaxIdleMs < 0 ? null : new IdleExpiryManager(time, connectionMaxIdleMs); this.memoryPool = memoryPool; this.lowMemThreshold = (long) (0.1 * this.memoryPool.size()); this.failedAuthenticationDelayMs = failedAuthenticationDelayMs; this.delayedClosingChannels = (failedAuthenticationDelayMs > NO_FAILED_AUTHENTICATION_DELAY) ? new LinkedHashMap<String, DelayedAuthenticationFailureClose>() : null; } }
重要字段如下所示:
- nioSelector: 在 Java NIO 中用来监听网络I/O事件。
- channels: 用来进行管理客户端到各个Node节点的网络连接,Map 集合类型 <Node节点id, KafkaChannel>
- completedSends: 已经发送完成的请求对象 Send 集合,List 集合类型。
- completedReceives: 已经接收完毕的网络请求集合,LinkedHashMap 集合类型 <ChannelId, NetworkReceive>,其中 value 都是已经接收完毕的 NetworkReceive 类对象。
- immediatelyConnectedKeys: 立即连接key集合。
- closingChannels: 关闭连接的 channel 集合。
- disconnected: 断开连接的集合。Map 集合类型 <ChannelId, ChannelState>,value 是 KafkaChannel 的状态,可以在使用的时候可以通过这个 ChannelState 状态来判断处理逻辑。
- connected: 成功连接的集合,List 集合类型,存储成功请求的 ChannelId。
- failedSends: 发送失败的请求集合,List 集合类型, 存储失败请求的 ChannelId。
- channelBuilder: 用来构建 KafkaChannel 的工具类。
- maxReceiveSize: 最大可以接收的数据量大小。
- idleExpiryManager: 空闲超时到期连接管理器。
- memoryPool: 用来管理 ByteBuffer 的内存池,分配以及回收。
介绍完字段后,我们来看看该类的方法。方法比较多,这里深度剖析下其中几个重要方法,通过学习这些方法的不仅可以复习下 Java NIO 底层组件,另外还可以学到 Kafka 封装这些底层组件的实现思想。
NetworkClient 的请求一般都是交给 Kselector 去处理并完成的。而 Kselector 使用 NIO 异步非阻塞模式负责具体的连接、读写事件等操作。
我们先看下连接过程,客户端在和节点连接的时候,会创建和服务端的 SocketChannel 连接通道。Kselector 维护了每个目标节点对应的 KafkaChannel。
如下图所示:
1、connect()
@Override public void connect(String id, InetSocketAddress address, int sendBufferSize, int receiveBufferSize) throws IOException { // 1.先确认是否已经被连接过 ensureNotRegistered(id); // 2.打开一个 SocketChannel SocketChannel socketChannel = SocketChannel.open(); SelectionKey key = null; try { // 3.设置 socketChannel 信息 configureSocketChannel(socketChannel, sendBufferSize, receiveBufferSize); // 4.尝试发起连接 boolean connected = doConnect(socketChannel, address); // 5. 将该 socketChannel 注册到 nioSelector 上,并关注 OP_CONNECT 事件 key = registerChannel(id, socketChannel, SelectionKey.OP_CONNECT); // 6.如果立即连接成功了