更多请关注 >>http://java.jr-jr.com

  1. 1. 概念
    1. 1.1. Reactor
    2. 1.2. NIO和BIO的区别
  2. 2. Linux IO模型
    1. 2.1. 阻塞IO模型
    2. 2.2. 非阻塞IO模型
    3. 2.3. 多路复用
    4. 2.4. 信号驱动IO模型
    5. 2.5. 异步IO模型
  3. 3. epoll
  4. 4. Zero Copy
  5. 5. C10K 问题

NIO,New IO,又叫做Non-blocking IO,Netty线程里用的New I/O worker #*

概念

IO 性能对于一个系统的影响是至关重要的。一个系统经过多项优化以后,瓶颈往往落在数据库;而数据库经过多种优化以后,瓶颈最终会落到 IO 。而IO 性能的发展,明显落后于CPU的发展。Redis也好,NoSql也好,这些流行技术的背后都在直接或者间接地回避IO瓶颈,从而提高系统性能。

我们说阻塞和非阻塞时,要区分场合范围,比如 Linux中说的非阻塞I/O和Java的NIO1.0中的非阻塞I/O不是相同的概念. 从最根本来说, 阻塞就是进程”被”休息,CPU处理其它进程去了.非阻塞可以理解成: 将大的整片时间的阻塞分成N多的小的阻塞,所以进程不断地有机会 “被” CPU光顾, 理论上可以做点其它事.看上去 Linux非阻塞I/O要比阻塞好,但CPU会很大机率因socket没数据而空转.虽然这个进程是爽了, 但是从整个机器的效率来说, 浪费更大了!Java NIO1.0中的非阻塞I/O中的 Selector.select()函数还是阻塞的, 所以不会有无谓的CPU浪费.

Reactor

IO分为两个阶段:

  • 资源等待阶段
  • 资源使用阶段

Java NIO解决的是资源等待阶段阶段的问题。资源使用阶段,依然是在用户线程中去完成的。Java NIO使用的是Reactor模型:

Reactor有很多种变种,深入了解

Reactor多线程模型的特点:

  • 有专门一个NIO线程Acceptor线程用于监听服务端,接收客户端的TCP连接请求;
  • 网络IO操作-读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送;
  • 1个NIO线程可以同时处理N条链路,但是1个链路只对应1个NIO线程,防止发生并发操作问题。

NIO和BIO的区别

  • 阻塞IO:使用简单,但随之而来的问题就是会形成阻塞,需要独立线程配合,而这些线程在大多数时候都是没有进行运算的。Java的BIO使用这种方式,问题带来的问题很明显,一个Socket需要一个独立的线程,因此,会造成线程膨胀。
  • 非阻塞IO:采用轮询方式,不会形成线程的阻塞。Java的NIO使用这种方式,对比BIO的优势很明显,可以使用一个线程进行所有Socket的监听(select)。大大减少了线程数。

下面我们看一下Linux 5种经典的IO模型。然后我们会发现,Java的NIO,其实只是多路复用而已。

Linux IO模型

linux下有五种常见的IO模型,其中只有一种异步模型,其余皆为同步模型。如图:

阻塞IO模型

阻塞IO模型是最常见的IO模型了,对于所有的“慢速设备”(socketpipefifoterminal)的IO默认的方式都是阻塞的方式。阻塞就是进程放弃cpu,让给其他进程使用cpu。进程阻塞最显著的表现就是进程睡眠了。阻塞的时间通常取决于数据是否到来。
这种方式使用简单,但随之而来的问题就是会形成阻塞,需要独立线程配合,而这些线程在大多数时候都是没有进行运算的。Java的BIO使用这种方式,问题带来的问题很明显,一个Socket需要一个独立的线程,因此,会造成线程膨胀

非阻塞IO模型

非阻塞IO就是设置IO相关的系统调用为non-blocking,随后进行的IO操作无论有没有可用数据都会立即返回,并设置errno为EWOULDBLOCK或者EAGAIN。我们可以通过主动check的方式(polling,轮询)确保IO有效时,随之进行相关的IO操作。当然这种方式看起来就似乎不太靠谱,浪费了太多的CPU时间,用宝贵的CPU时间做轮询太不靠谱儿了。图示:

多路复用

为了解决阻塞I/O的问题,就有了I/O多路复用模型,多路复用就是用单独的线程(是内核级的, 可以认为是高效的优化的) 来统一等待所有的socket上的数据, 一当某个socket上有数据后, 就启用用户线程(可能是从线程池中取出, 而不是重新生成), copy socket data, 并且处理message.因为网络延迟的原因, 同时在处理socket data的用户线程往往比实际的socket数量要少很多. 所以实际应用中, 大部分是用线程池, 池中thread数量可随socket的高峰和低谷 而动态调整.

多路复用I/O中内核中统一的wait socket data那部分可以理解成是非阻塞, 也可以理解成阻塞. 可以理解成非阻塞 是因为它不是等到socket数据全部到达再处理, 而是有了一部分数据就会调用用户线程来处理, 理解成阻塞, 是因为它和用户空间(Appliction)层的非阻塞socket的不同是: socket中没有数据时, 内核还是wait(阻塞)的, 而用户空间的非阻塞socket没有数据也会返回, 会造成CPU的浪费.

Linux下的select和poll 就是多路复用模式,poll相对select,没有了句柄数的限制,但他们都是在内核层通过轮询socket句柄的方式来实现的, 没有利用更底层的notify机制. 但就算是这样,相对阻塞socket也已经进步了很多很多了! 毕竟用一个内核线程就解决了,阻塞socket中N多线程都在无谓地wait的局面.


多路复用I/O 还是让用户层来copy socket data. 这个过程是将内核中的socket buffer copy到用户空间的 buffer. 这有两个问题: 一是多了一次内核空间switch到用户空间的过程, 二是用户空间层不便暴露很低层但很高效的copy方式(比如DMA), 所以如果由内核层来做这个动作, 可以更好地提高效率!

信号驱动IO模型

所谓信号驱动,就是利用信号机制,安装信号SIGIO的处理函数(进行IO相关操作),通过监控文件描述符,当其就绪时,通知目标进程进行IO操作(signal handler)。

异步IO模型

由于异步IO请求只是写入了缓存,从缓存到硬盘是否成功不可知,因此异步IO相当于把一个IO拆成了两部分,一是发起请求,二是获取处理结果。因此,对应用来说增加了复杂性。但是异步IO的性能是所有很好的,而且异步的思想贯穿了IT系统放放面面。

epoll

epoll是Java NIO在linux上的默认实现。相关的工具可以关注select,poll,关于三者间的区别,参见这里。在Mac上类似的实现是kqueue,Solaris上是/dev/poll
epoll的优点

  • 支持一个进程打开大数目的socket描述符(FD)
  • IO效率不随FD数目增加而线性下降

    传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是”活跃”的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对”活跃”的socket进行操作—这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有”活跃”的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个”伪”AIO,因为这时候推动力在os内核。

  • 使用mmap加速内核与用户空间的消息传递。

  • 内核微调

epoll有2种工作方式:LT和ET:

  • LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.
  • ET (edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。

Zero Copy

上面多次提到内核空间和用户空间的switch, 在socket read/write这么小的粒度频繁调用, 代价肯定是很大的.
所以可以在网上看到Zero Copy的技术, 说到底Zero Copy的思路就是: 分析你的业务, 看看是否能避免不必要的跨空间copy,比如可以用 sendfile()函数充分利用内核可以调用DMA的优势, 直接在内核空间将文件的内容通过socket发送出去,而不必经过用户空间.显然,sendfile是有很多的前提条件的, 如果你想让文件内容作一些变换再发出去,就必须要经过用户空间的Appliation logic, 也是无法使用sendfile了.还有一种方式就是象epoll所做的,用内存映射. 据我所知,kafka速度快的一个原因就是使用了零拷贝
关于零拷贝,可以看这篇文章

C10K 问题

网络服务在处理数以万计的客户端连接时,往往出现效率低下甚至完全瘫痪,这被 称为C10K问题。随着互联网的迅速发展,越来越多的网络服务开始面临 C10K 问题, 作为大型网站的开发人员有必要对C10K问题有一定的了解。
C10K问题的最大特点是:设计不够良好的程序,其性能和连接数及机器性能的关系往往是非线性的。举个例子:如果没有考虑过C10K问题,一个经典的基于select的程序能在旧服务器上很好处理1000并发的吞吐量,它在2倍性能新服务器上往往处理不了并发2000的吞吐量。
这是因为在策略不当时,大量操作的消耗和当前连接数n成线性相关。会导致单个任务的资源消耗和当前连接数的关系会是O(n)。而服务程序需要同时对数以万计的socket进行I/O处理,积累下来的资源消耗会相当可观,这显然会导致系统吞吐量不能 和机器性能匹配。为解决这个问题,必须改变对连接提供服务的策略。
更详细的资料参考:The C10K problem