基本的 IO(包括网络和文件) 编程过程

  • 打开文件描述符(windows 是 handler,java 是 stream 或 channel)
  • 多路捕获(Multiplexe,即 select/poll/epoll)IO 可读写的状态
  • 对可以读写的文件描述符进行 IO 读写

IO 设备速度比CPU、内存慢,开多线程更好利用 CPU和内存,每个线程读写一个fd

C10K

海量网络连接下,瓶颈不在机器设备和网络速度,在于os和 IO 应用程序的沟通协作的方式


一万个 socket 连接过来,传统 IO 编程模型要开一万个线程来应对

socket 会关闭打开,一万个线程要不断的关闭线程重建线程,资源都浪费在这上面了

一个线程耗 1M 内存,1 万个线程至少10G ,IA-32 机器架构下基本是不可能的(要开 PAE),x64 架构才有可能舒服点,这仅仅是粗略算的内存消耗,还有别的资源

高性能网络编程(即 IO 编程)

一,需要松绑 IO 连接和应用程序线程的对应关系,这就是非阻塞(nonblocking)、 异步(asynchronous)要求的由来

(构造一个线程池,epoll 监控到有数据的 fd,把 fd 传入线程池,由这些 worker thread 来读写 io)

二,需要高性能的 OS 对 IO 设备可读写(数据来了)的通知方式:

从 level-triggerednotification 到 edge-triggered notification


异步,不等于 AIO(asynchronousIO),linux 的 AIO 和 java 的 AIO 都是实现异步的一种方式,都是渣

针对前面说的这两点,看看 select 和 poll 的问题

这两个函数都在每次调用的时候要求把需要监控(看看有没有数据)的文件描述符,通过数组传递进入内核,内核每次都要扫描这些文件描述符,去理解它们,建立一个文件描述符和 IO 对应的数组(实际内核工作会有好点的实现方式,但可以这么理解先),以便 IO 来的时候,通知这些文件描述符,进而通知到进程里等待的这些 select、poll

一万个fd(网络连接)要监控时工作效率很低,资源要求却很高

epoll

epoll 分为三个函数

  • 创建一个 session 类似的东西
  • 告诉内核维持这个 session,并把属于 session 内的 fd 传给内核
  • epoll_wait 是真正的监控多个文件描述符函数

只需要告诉内核,在等待哪个 session,而 session 内的 fd,内核早就分析过了,不再在每次 epoll 调用的时候分析,这就节省了内核大部分工作

每次调用 epoll,内核不再重新扫描 fd 数组,因为我们维持了 session

epoll 效率在内核通知方式上,也改进了

select/poll 的通知方式是 level-triggerednotification,内核被 DMA 中断,捕获到 IO 设备来数据后,本来只需要查找这个数据属于哪个文件描述符,进而通知线程里等待的函数即可

但select/poll 要求内核在通知阶段还要继续再扫描一次刚才所建立的内核 fd 和 io 对应的那个数组,因为应用程序可能没有真正去读上次通知有数据后的那些 fd,应用程序上次没读,内核在这次 select 和 poll 调用的时候就得继续通知,这个 os 和应用程序的沟通方式效率是低下的

只是方便编程而已(可以不去读那个网络 io,反正下次会继续通知)

epoll 设计了另外一种通知方式:edge-triggerednotification

此模式下,io 设备来了数据,就只通知这些 io 设备对应的 fd,上次通知过的 fd 不再通知,内核不再扫描一大堆 fd 了

epoll 是专门针对大网络并发连接下的 os 和应用沟通协作上的一个设计

linux 下编网络服务器必然要采用这个(nginx、php 异步框架 swool、varnish)

java 的 NIO/NIO.2 都只是用了 epoll,没有打开 edge-triggerednotification,所以不如Netty


AIO 的问题

select/poll/epoll 都需要用一个函数去监控一大堆 fd,但AIO 不需要

把 fd 告诉内核,应用程序无需等待,内核会通过信号等软中断告诉应用程序,数据来了,程序直接读,所以,用了 AIO 可以废弃 select/poll/epoll

但 linux 的 AIO 的实现方式是内核和应用共享一片内存区域,应用通过检测这个内存区域(避免调用 nonblocking 的 read、write 函数来测试是否来数据,因为即便调用 nonblocking 的 read 和 write 由于进程要切换用户态和内核态,仍旧效率不高)来得知 fd 是否有数据

可是检测内存区域毕竟不是实时的,你需要在线程里构造一个监控内存的循环,设置 sleep,总的效率不如 epoll 这样的实时通知

所以,AIO 是渣,适合低并发的 IO 操作

所以 java7 NIO.2 引入的 AIO 对高并发的网络 IO 设计程序来说,也是渣,只有 Netty 的epoll+edge-triggerednotification 最牛,能在 linux 让应用和 OS 取得最高效率的沟通

注:非阻塞 IO 和异步 IO 基本是一个意思,只是描述同一个东西的不同方面


netty拒绝AIO主要是因为netty整体架构是reactor模型, 而AIO是proactor模型, 混合在一起会非常混乱, 要么把AIO也改造成reactor模型看起来是把epoll绕个弯又绕回来

netty基本不看重windows, AIO在linux也是用reactor模型的epoll实现的, 而且被JDK封装了一层不容易深度优化, 所以才放弃的

另外AIO还有个缺点是接收数据需要预先分配缓存, 而不是NIO那种需要接收时才需要分配缓存, 所以对连接数量非常大但流量小的情况, 内存浪费很多

libuv统一了reactor和proactor(epoll和iocp)接口, iocp的实现, 可以传空缓冲区去异步接收, 回调时再用缓冲区去实际接收, 感觉是把iocp当reactor去用了, 然而AIO并不支持用空缓冲区异步接收