NIO

NIO

摘自 美团点评技术团队-知乎

服务端同步阻塞I/O的问题:

  1. 线程的创建和销毁成本很高, 在Linux这样的操作系统中, 线程本质上就是一个进程. 创建和销毁都是重量级的系统函数.
  2. 线程本身占用较大内存, 像Java的线程栈, 一般至少分配512K~1M的空间, 如果系统中的线程数过千, 恐怕整个JVM的内存都会被吃掉一半.
  3. 线程的切换成本是很高. 操作系统发生线程切换的时候, 需要保留线程的上下文, 然后执行系统调用. 如果线程数过高, 可能执行线程切换的时间甚至会大于线程执行的时间, 这时候带来的表现往往是系统load偏高, CPU sy使用率特别高(超过20%以上), 导致系统几乎陷入不可用的状态.
  4. 容易造成锯齿状的系统负载. 因为系统负载是用活动线程数或CPU核心数, 一旦线程数量高但外部网络环境不是很稳定, 就很容易造成大量请求的结果同时返回, 激活大量阻塞线程从而使系统负载压力过大.

所以, 当面对十万甚至百万级连接的时候, 传统的BIO模型是无能为力的. 随着移动端应用的兴起和各种网络游戏的盛行, 百万级长连接日趋普遍, 此时, 必然需要一种更高效的I/O处理模型.

常见I/O模型对比:

I/O模型图

以socket.read()为例子:

  • 传统的BIO里面socket.read(), 如果TCP RecvBuffer里没有数据, 函数会一直阻塞, 直到收到数据, 返回读到的数据.
  • 对于NIO, 如果TCP RecvBuffer有数据, 就把数据从网卡读到内存, 并且返回给用户. 反之则直接返回0, 永远不会阻塞.
  • 最新的AIO(Async I/O)里面会更进一步: 不但等待就绪是非阻塞的, 就连数据从网卡到内存的过程也是异步的.

换句话说, BIO里用户最关心”我要读”, NIO里用户最关心”我可以读了”, 在AIO模型里用户更需要关注的是”读完了”.

NIO一个重要的特点是: socket主要的读, 写, 注册和接收函数, 在等待就绪阶段都是非阻塞的, 真正的I/O操作是同步阻塞的(消耗CPU但性能非常高).

结合事件模型使用NIO同步非阻塞特性:

NIO的读写函数可以立刻返回, 这就给了我们不开线程利用CPU的最好机会: 如果一个连接不能读写(socket.read()返回0或者socket.write()返回0), 我们可以把这件事记下来, 记录的方式通常是在Selector上注册标记位, 然后切换到其它就绪的连接(channel)继续进行读写.

单线程事件模型伪代码实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
interface ChannelHandler{
void channelReadable(Channel channel);
void channelWritable(Channel channel);
}

class Channel{
Socket socket;
Event event;//读,写或者连接
}

//IO线程主循环:
class IoThread extends Thread{
public void run(){
Channel channel;
while(channel=Selector.select()){//阻塞选择就绪的事件和对应的连接
if(channel.event==accept){
registerNewChannelHandler(channel);//如果是新连接,则注册一个新的读写处理器
}
if(channel.event==write){
getChannelHandler(channel).channelWritable(channel);//如果可以写,则执行写事件
}
if(channel.event==read){
getChannelHandler(channel).channelReadable(channel);//如果可以读,则执行读事件
}
}
}
Map<Channel,ChannelHandler> handlerMap;//所有channel的对应事件处理器
}

NIO由原来的阻塞读写变成了单线程轮询事件, 找到可以进行读写的网络描述符进行读写. 除了事件的轮询是阻塞的, 剩余的I/O操作都是纯CPU操作.

需要的线程主要包括以下几种:

  1. 事件分发器. 单线程选择就绪的事件.
  2. I/O处理器, 包括connect, read, write等, 这种纯CPU操作, 一般开启CPU核心个线程就可以.
  3. 业务线程, 在处理完I/O后, 业务一般还会有自己的业务逻辑, 有的还会有其他的阻塞I/O, 如DB操作, RPC等. 只要有阻塞, 就需要单独的线程.