I/O 模型浅析
BIO
BIO 是数据准备阶段和数据拷贝阶段两阶段都是阻塞的。导致 CPU 资源得不到很好的利用,因为大多数时候是处于阻塞状态下的。虽然可以利用线程池实现一请求一线程以利用 CPU。但严重依赖于线程,并发量上不去,因为:
- 线程的切换成本高
- 线程的创建和销毁成本很高
- 线程本身占用较大内存
非阻塞IO
当用户线程发起IO请求时,不用等到内核准备好数据才能返回,而是可以立刻返回,没有被阻塞住。但是用户线程始终还是得获取到数据,所以各个线程只能不断的自己轮询,检查内核空间的数据是否准备好,这样则很耗CPU。
IO多路复用
IO多路复用,其实是利用select函数阻塞多个IO操作,并对这些IO操作进行轮询检测,一旦数据准备好后,就可以通知线程进行真正的IO操作(此时数据已经在内核空间准备好了,此时用户线程直接进行拷贝的动作即可)。期间如果数据未准备好,用户线程可以可以干其他事情的。
JAVA NIO
Java 的 NIO 在非阻塞 IO 的基础上实现了多路复用 IO ,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动线程进行处理,这样就使得每个连接不会阻塞,提高了性能。
在事件模型上利用 NIO 非阻塞特性
NIO的主要事件有几个:读就绪、写就绪、有新连接到来。
我们首先需要注册当这几个事件到来的时候所对应的处理器。然后在合适的时机告诉事件选择器:我对这个事件感兴趣。对于写操作,就是写不出去的时候对写事件感兴趣;对于读操作,就是完成连接和系统没有办法承载新读入的数据的时;对于accept,一般是服务器刚启动的时候;而对于connect,一般是connect失败需要重连或者直接异步调用connect的时候。
优化线程模型
NIO 的 Reactor 和 Proactor 模型
Reactor 体现的是我可以读了。在Reactor模式中,事件分发器等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分发器就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。
Proactor 体现的是我读完了。事件处理者(或者代由事件分发器发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。事件分发器等IO Complete事件完成。这种异步模式的典型实现是基于操作系统底层异步API的,所以我们可称之为“系统级别”的或者“真正意义上”的异步,因为具体的读写是由操作系统代劳的。
NIO 给我们带来什么
- 事件驱动模型
- 单线程处理多任务
- 非阻塞I/O,I/O读写不再阻塞,而是返回0
AIO
AIO 模式为一个有效请求一个线程,客户端的I/O请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理。AIO 依靠操作系统实现。
参考:
美团点评:
NIO:
NIO 模型的利用
Netty
一个异步、事件驱动的NIO框架。通过NIO异步非阻塞通信、加上对Java序列化的优化等等使得Netty成为一个高性能的通信框架。
高性能的序列化
JDk 序列化弊端
- 序列化后的码流大小(网络带宽的占用)
- 序列化 & 反序列化的性能(CPU 资源占用)
- 无法跨语言
Netty 提供的序列化
- Google Protobuf
- JBoss marshalling
Netty 简化 NIO 繁琐的操作
高效的线程模型
- Reactor 单线程模型
通过 Acceptor 接收客户端的 TCP 连接请求消息,链路建立成功之后,通过 Dispatch 将对应的 ByteBuffer 派发到指定的 Handler 上进行消息解码。用户 Handler 可以通过 NIO 线程将消息发送给客户端。
缺点:一个 NIO 线程同时处理成百上千的链路,性能上无法支撑;一旦 NIO 线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。、
- Reactor 多线程模型
a. 有专门一个 NIO 线程 -Acceptor 线程用于监听服务端,接收客户端的 TCP 连接请求;
b. 网络 IO 操作 - 读、写等由一个 NIO 线程池负责,线程池可以采用标准的 JDK 线程池实现,它包含一个任务队列和 N 个可用的线程,由这些 NIO 线程负责消息的读取、解码、编码和发送;
缺点:一个 NIO 线程负责监听和处理所有的客户端连接可能会存在性能问题
- 主从 Reactor 多线程模型
利用主从 NIO 线程模型,可以解决 1 个服务端监听线程无法有效处理所有客户端连接的性能不足问题。因此,在 Netty 的官方 demo 中,推荐使用该线程模型。
无锁化串行设计
Netty 采用了串行无锁化设计,在 IO 线程内部进行串行操作,避免多线程竞争导致的性能下降。表面上看,串行化设计似乎 CPU 利用率不高,并发程度不够。但是,通过调整 NIO 线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列 - 多个工作线程模型性能更优
参考:
Netty 高性能之道: NIO