前序
- 前面两篇文章,基于元数据获取以及发送消息总体做了分析,本篇博文基于其中Kafka对于JAVA NIO的应用进行分析
- Java NIO理论知识是参考美团公众号的文章:https://zhuanlan.zhihu.com/p/23488863
- 对理论知识有一定了解后,抽取了Kafka生产者对NIO的应用代码并进行了上传
JAVA NIO
- NIO(Non-blocking I/O),是一种同步非阻塞的I/O模型,也是I/O多路复用的基础
- 传统BIO(Blocking I/O)模型分析(一个连接一个线程)
2.1 Socket.accept()、Socket.read()、Socket.write()都是同步阻塞的,单线程会挂死在那里,但CPU是被释放出来
2.2 使用多线程的本质:利用多核、可以利用多线程使用CPU资源
NIO是怎么工作的
- 所有的系统I/O都分为两个阶段:等待就绪和操作
- 几种常见I/O模型的对比
2.1 对于NIO,如果没有数据,则直接返回0,永远不会阻塞
2.2 NIO一个重要的特点是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)
如何结合事件模型使用NIO同步非阻塞特性(重点)
- 利用NIO的读写函数可以立刻返回,如果一个连接不能读写,将这件事记录下来
- 记录方式:在Selector上注册标记位,然后切换到其他就绪的连接(channel)继续读写
- 具体利用事件模型单线程处理所有I/O请求的步骤:
3.1 注册事件所对应的处理器
3.2 在事件选择器上注册事件
3.3 用一个死循环选择就绪的事件
(1)select是阻塞的,while(true)调用不会CPU空转
优化线程模型(重点)
- 现在服务器一般都是多核处理器,如果能够利用多核心进行I/O,无疑对效率会有更大的提高
- 仔细分析一下我们需要的线程,其实主要包括以下几种:
2.1 事件分发器,单线程选择就绪的事件
2.2 I/O处理器,包括connect、read、write等,这种纯CPU操作,一般开启CPU核心个线程就可以
2.3 业务线程,在处理完I/O后,业务一般还会有自己的业务逻辑,有的还会有其他的阻塞I/O,如DB操作,RPC等。只要有阻塞,就需要单独的线程 - Java的Selector对于Linux系统来说,有一个致命限制:同一个channel的select不能被并发的调用
Selector.wakeup()
- 主要作用:解除阻塞在Selector.select()/select(long)上的线程,立即返回
- 两次成功的select之间多次调用wakeup等价于一次调用
- 如果当前没有阻塞在select上,则本次wakeup调用将作用于下一次select——“记忆”作用
- 优先级更高的事件触发,希望及时处理
抽取Kafka Producer的NIO应用代码
- 演示代码入口:NetworkSendTest
- 抽取的代码主要是发送消息到指定分区节点
- 代码总结
3.1 连接指定分区节点:Selector#connect,将节点ID与Channel建立绑定关系channels(Map<String, KafkaChannel>)
3.2 写入消息到ByteBuffer中,并封装在KafkaChannel的send属性中(NetworkSend初始化)
3.3 将写事件添加到KafkaChannel的selectionKey中
3.4 Selector轮询到写就绪事件后触发对应处理器,将ByteBuffer中的数据写入SocketChannel中(KafkaChannel#send)
3.5 将写事件从KafkaChannel的selectionKey中移除
正如上面所说,从通道读取数据到缓冲区,从缓冲区写入数据到通道。如下图所示:
NIO处理方式,一个线程可以管理过个网络连接
抽取Kafka Producer的Future模式
- 在发送消息时,将消息添加累计器重后返回一个FutureRecordMetadata(future的实现)
1.1 在其get方法获取结果时,用latch.await()阻塞
- 在正式NIO发送请求前,定义一个回调RequestCompletionHandler,并封装请求体中InFlightRequest$ClientRequest
2.1 在InFlightRequests中属性requests保存节点ID与请求列表的映射
- 处理NIO的响应,调用对应节点的回调(inFlightRequests)