0.UNXI IO模型简介
在 Unix 系统里,磁盘的文件读写、网络通讯等功能的核心部分都是 I/O, open, read , write ,connect 等系统调用都属于 I/O 操作。因为 I/O 操作都比较耗时,因此有了阻塞、非阻塞、同步、异步等不同 I/O 模型。
I/O 操作引发的阻塞是指进程中断执行,等待其它的工作(通常是比较耗时的)准备就绪再恢复。
阻塞可能出现在两个阶段:
- 阶段一:等待数据就绪(例如从硬盘读取数据完成)
- 阶段二:把数据从内核复制到用户空间,把数据从内核缓冲区复制到应用进程缓冲区
- 按照《Unix Network Programming》(下称 UNP)的分类,Unix 有 5 种 I/O 模型:
- 阻塞式 I/O ( Blocking I/O)
- 非阻塞式 I/O (NonBlocking I/O)
- I/O 多路复用 (I/O multiplexing)
- 信号驱动 I/O ( signal-driven I/O )
- 异步 I/O (asynchronous I/O )
1. 阻塞式 I/O ( Blocking I/O)
简单来说就是调用IO的应用进程被阻塞,直到数据复制到应用进程缓冲区中才返回。
常见的有C语言里的read()
和wirte()
函数
下面是阻塞式IO的一个Java实现
1 | public void start() { |
在阻塞式IO的基础上,我们可以使用线程池,让每一个线程承接一个socket连接,
主要原因在于socket.accept()
、socket.read()
、socket.write()
三个主要函数都是同步阻塞的,当一个连接在处理I/O的时候,系统是阻塞的,如果是单线程的话必然就挂死在那里;但CPU是被释放出来的,开启多线程,就可以让CPU去处理更多的事情。其实这也是所有使用多线程的本质: 1. 利用多核。 2. 当I/O阻塞系统,但CPU空闲的时候,可以利用多线程使用CPU资源。
1 | public class MTBIOServerThread implements Runnable{ |
接收到客户端的socket后,业务的处理过程可以交给一个线程来做,但还是改变不了socket被一个一个一个的做accept()和read()的情况。
总的来说,阻塞式 I/O 很难应对高并发的应用场景,因为每次阻塞,都有一个进程被搁置起来,直到完成工作,才会去处理下一个请求。
todo 压力测试
压力测试
环境:
网络:局域网
服务器:
1 | OS:Windows 10 |
测试机有八个子节点,共同运行在如下的计算机上
1 | OS:Ubuntu |
单线程BIO服务器能取得的最好成绩是在100User的条件下,平均每秒能得到3000RPS的成绩,如果提大幅度高User到300或者是500及以上,会导致几乎显著提高的的Failure,这是因为单线程服务器的限制,多个IO同时到来会收到阻塞
多线程服务器可以在近乎6000 Client下 0% Failure 的情况下,取得3000RPS的成绩
接下来让我们考虑一下C10K问题
这时候RPS率有所增加,但是Failure可以达到 30%,而且非常消耗系统资源
接下来终极测试 100000个User
RPS仍能保持3000-4000的水平,但是每个请求的处理延迟大大增加
2.非阻塞式 I/O (NonBlocking I/O)
在非阻塞式模型下,I/O 操作启动后,如果工作还未完成,内核会马上返回一个错误消息(EWOULDBLOCK或者EAGAIN 等) ,而不是像阻塞式一样等到完成工作再交出进程。
应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)。
由于 CPU 要处理更多的系统调用,因此这种模型是比较低效的。
3.I/O 多路复用
如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都需要创建一个线程去处理。如果同时有几万个连接,那么就需要创建相同数量的线程。并且相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小。
I/O 多路复用可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O,即事件驱动 I/O,multiplexing 使用 select、 poll 等函数实现,使阻塞发生在 select 等函数里,而不是阻塞进程。。
select 可以同时监听多个文件描述符,它执行后会处于等待状态,当有一个或者多个文件描述符变成“就绪”状态,就会继续向下执行,如此循环。
使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读,这一过程会被阻塞,当某一个套接字可读时返回。之后再使用 recvfrom 把数据从内核复制到进程中。
Java NIO 常见概念
NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector
通过轮询的方式去监听多个通道 Channel
上的事件,从而让一个线程就可以处理多个事件。
通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。
我们首先需要注册当这几个事件到来的时候所对应的处理器。然后在合适的时机告诉事件选择器:我对这个事件感兴趣。
对于写操作,就是写不出去的时候对写事件感兴趣;
对于读操作,就是完成连接和系统没有办法承载新读入的数据的时;
对于accept,一般是服务器刚启动的时候;
而对于connect,一般是connect失败需要重连或者直接异步调用connect的时候。
创建选择器并注册通道
1 |
|
通过选择器监听事件
1 |
|
获取到达的事件
1 | Set<SelectionKey> keys = selector.selectedKeys(); |
事件循环
因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。
注意,selector.select();
是阻塞的,无论是通过操作系统的通知(epoll)还是不停的轮询(select,poll),这个函数是阻塞的。所以你可以放心大胆地在一个while(true)里面调用这个函数而不用担心CPU空转。
1 | while (true) { |
一个简单的Java NIO 服务器实现:
1 | Selector selector = Selector.open(); |
压力测试
NIO由原来的阻塞读写(占用线程)变成了单线程轮询事件,找到可以进行读写的网络描述符进行读写。除了事件的轮询是阻塞的(没有可干的事情必须要阻塞),剩余的I/O操作都是纯CPU操作,没有必要开启多线程。
模拟的NIO服务器也是一个单线程服务器
在有1000-3000User时,NIO服务器可以保证1%上下的 Failure,但是在3000-10000User时,Failure率则能提高到10%上下,最终提高到100000User时候,单次请求的response时间也大幅度上升
让我们对照一下Spring框架下的Tomacat服务器
该服务器采用一个多线程的NIO模型
可以看出在1000-5000的低负载下,该服务器可以在0 Failure下在8000RPS的压力下运行,在主键提高到10000User后,Failue率逐渐上升,并最终到达100000User,Failure率在30%以上,但是tomcat服务器相比于前面的多线程BIO服务器,平均响应时间较低
PS
1.使用NIO != 高性能,当连接数<1000,并发程度不高或者局域网环境下NIO并没有显著的性能优势。
4. 信号驱动 I/O
应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。
相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。
5.异步 I/O
异步IO则是采用“订阅-通知”模式: 即应用程序向操作系统注册IO监听,然后继续做自己的事情。当操作系统发生IO事件,并且准备好数据后,在主动通知应用程序,触发相应的函数
异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。
注意在JAVA NIO框架中,我们说到了一个重要概念“selector”(选择器)。它负责代替应用查询中所有已注册的通道到操作系统中进行IO事件轮询、管理当前注册的通道集合,定位发生事件的通道等操操作;
但是在JAVA AIO框架中,由于应用程序不是“轮询”方式,而是订阅-通知方式,所以不再需要“selector”(选择器)了,改由channel通道直接到操作系统注册监听。
JAVA AIO框架中,只实现了两种网络IO通道“AsynchronousServerSocketChannel”(服务器监听通道)、“AsynchronousSocketChannel”(socket套接字通道)。但是无论哪种通道他们都有独立的fileDescriptor(文件标识符)、attachment(附件,附件可以使任意对象,类似“通道上下文”),并被独立的SocketChannelReadHandle类实例引用。
参考
转载无需注明来源,放弃所有权利