写点什么

没搞清楚网络 I/O 模型?那怎么入门 Netty

用户头像
云流
关注
发布于: 2021 年 01 月 18 日

Netty 是网络应用框架,所以从最本质的角度来看,是对网络 I/O 模型的封装使用。

因此,要深刻理解 Netty 的高性能,也必须从网络 I/O 模型说起。

 

看完本文,可以回答这三个问题:

  • 五种 I/O 模型是什么?核心区别在哪里?

  • 同步=阻塞?异步=非阻塞?

  • Netty 的高性能,是采用了哪种 I/O 模型?


1.掌握五种 I/O 模型的关键钥匙

Unix 系统下的五种基本 I/O 模型大家应该都有所耳闻,分为:

  • blocking I/O(同步阻塞 IO,BIO)

  • nonblocking I/O(同步非阻塞 IO,NIO)

  • I/O multiplexing (I/O 多路复用)

  • signal driven I/O(信号驱动 I/O)

  • asynchronous I/O(异步 I/O,AIO)


每种 I/O 的特性如何,尤其是同步/非同步、阻塞/非阻塞的区别,其实很多人并不能准确地进行区分。

所以,我们先把最核心的“钥匙”告诉大家,带着这把“钥匙”再来看 I/O 模型的关键问题,就能手到擒来了。

当一次网络 IO 发生时,主要涉及到三个对象:

  • 发起此次 IO 操作的 Process 或者 Application

  • 系统内核 kernel。用户进程无法直接操作 I/O 设备,必须通过系统内核 kernel 与 I/O 设备交互。

  • I/O 设备,包括网络、磁盘等。本文主要针对网络。


 

真正的 I/O 过程,主要分为两个阶段:

  • 等待数据准备阶段。

  • 数据拷贝阶段。数据准备完毕,从内核 kernel 拷贝到进程 process 中


以一个 socket 上的输入操作为例。

第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。

第二步就是把数据从内核缓冲区复制到用户态缓冲区。

这里,我们先记住这 两个阶段,所有 I/O 模型的区别就在它们身上。

2.五种 I/O 模型详解

2.1 同步阻塞 I/O, BIO

我们一般使用最多的,最基础的 I/O 模型就是同步阻塞 I/O。

典型应用:

阻塞 socket、Java BIO

 

我们来解读一下 BIO 的过程:

  • 应用进程向内核发起 I/O 请求,发起调用的线程 一直阻塞,等待内核返回结果。

  • 数据准备完毕,从内核 kernel 拷贝到用户态内存(仍旧阻塞),然后 kernel 返回结果,用户进程 process 结束阻塞,重新运行。


“关键钥匙”分析:

BIO 的特点就是在 IO 执行的 两个阶段 都被 阻塞 了。

所以,我们日常使用 BIO 模型的时候,提高性能的方式,就是采用 多线程。

在一般的场景中,多线程模型下的 BIO 是成本较低、收益较高的方式。但是,如果在高并发的场景下,过多的创建线程,会严重占据系统资源,降低系统对外界响应效率。

那是不是可以考虑使用“线程池”或者“连接池”呢?

一定程度上可以。 “池化”的目的在于减少创建和销毁线程的频率,让空闲的线程重新承担新的执行任务,维持一个合理的线程数量,可以很好的降低系统开销。

但是,“池化”技术只能一定程度上缓解了频繁调用 IO 接口带来的资源占用。如果“池”上限 100,而我们需要 1000 的 IO,那并不能解决性能问题,这是由于 BIO 模型本身的限制决定的。

所以,需要非阻塞 I/O 来尝试解决这个问题。

2.2 同步非阻塞 I/O, NIO

BIO 的阻塞问题,让我们考虑使用非阻塞的 NIO 模型。

典型应用:

socket 的非阻塞模式

 

应用进程向内核发起 I/O 请求后,如果 kernel 中的数据还没有准备好,不再会“阻塞”等待结果,而是会立即返回。

从用户进程角度讲 ,它发起一个 IO 操作后,并不需要等待,而是马上就得到了一个结果。

用户进程判断结果是一个 error 时,它就知道数据还没有准备好,于是它开始发起轮训操作。

直到 kernel 中的数据准备好了,一旦用户再轮训过来,就马上将数据拷贝到了用户内存,然后返回。

所以,在非阻塞式 IO 中,用户进程其实是需要不断地主动询问 kernel 数据准备好了没有。

“关键钥匙”分析:

非阻塞 NIO 模型相比于 BIO 的显著差异在于,在“数据等待”阶段,不再“阻塞”,立即返回。

但是在“数据拷贝”阶段,仍然是“阻塞”的。

虽然非阻塞模型避免了“数据等待”阶段的阻塞,但是,采用轮询方式,会导致系统上下文切换开销很大,会大幅度推高 CPU 占用率。

因此,单独使用非阻塞 I/O 模型的效率并不高。而且随着并发量的提升,非阻塞 I/O 会存在严重的性能浪费。

我们可以看到,轮训的目的只是检测“数据是否已经就绪”,而操作系统提供了更为高效的检测接口,

例如 select()多路复用模式,可以一次检测多个连接是否活跃。

2.3 多路复用 IO

多路复用实现了一个线程处理多个 I/O 句柄的操作,有些地方也称这种 IO 方式为事件驱动 IO(event driven IO)。

  • 多路 指的是多个数据通道

  • 复用 指的是使用一个或多个固定线程来处理每一个 Socket。


典型应用:

select、poll、epoll 三种方案

Java NIO

 

多个的进程的 IO 可以注册到一个复用器(selector)上,然后用一个进程调用 select,select 会监听所有注册进来的 IO。

如果 selector 所有监听的 IO 在内核缓冲区都没有可读数据,select 调用进程会被阻塞;同时,kernel 会“监视”所有 select 负责的 socket,如果任何一个 socket 中的数据准备好了,select 就会返回;

然后 select 调用进程可以自己或通知另外的进程(注册进程)来再次发起读取 IO,然后 process 将数据从 kernel 拷贝到用户进程,读取内核中准备好的数据。

可以看到,多个进程注册 IO 后,只有一个 select 调用进程被阻塞。

多路复用解决了同步阻塞 I/O 和同步非阻塞 I/O 的问题,是一种非常高效的 I/O 模型。我们可以直观看到,这个模型的好处在于单个 process 就可以同时处理多个网络连接的 IO。

“关键钥匙”分析:

多路复用 I/O,select 阶段,对于多路 socket 的“数据等待”阶段而言,是“非阻塞”。

对单个 socket 的“数据拷贝”阶段,也是“阻塞”。

这里需要特别注意!!!!

其实如果处理的 IO 数不多的情况下,使用多路复用 IO 的 web server 不一定比使用 池化+BIO 的 web server 性能更好,可能延迟还更大。

考虑极端情况下,只有一个 IO,多路复用需要 2 次系统调用(select + recvfrom),而 BIO 只需要 1 次系统调用(recvfrom)。

所以,多路复用 IO 的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

2.4 信号驱动 I/O

在使用信号驱动 I/O 时,当数据准备就绪后,内核通过发送一个 SIGIO 信号通知应用进程,应用进程就可以开始读取数据了。

 

信号驱动 I/O 模型的最大特点,就是不需要 process 进程不断轮训内核是否已经准备就绪。

“关键钥匙”分析:

信号驱动 I/O 在"数据等待"阶段“非阻塞”。

当数据准备完成后,信号通知 process,process 开始“数据拷贝”阶段,这里仍然是“阻塞”的。

信号驱动 I/O 有几个缺陷:

1)在大量 IO 操作时可能会因为信号队列溢出导致没法通知。

2)信号驱动 I/O 尽管对于处理 UDP 套接字来说有用,信号通知意味着到达一个数据报,或者返回一个异步错误。

但是,对于 TCP 而言,信号驱动的 I/O 方式不太好用。因为导致信号通知的情况有非常多种,每一个来进行判别会消耗很大资源。

所以信号驱动 I/O 模式用得非常少。

而且尤其需要注意,在“数据拷贝”阶段,它仍然是“阻塞”的。

2.5 异步 I/O,AIO

真正的异步 I/O,就是 AIO。

典型应用:

JAVA7 AIO、高性能服务器

 

根据前面四个模型的分析,相信大家已经能明显看懂这个模型的运行方式了。

用户进程发起 I/O 请求后,立刻就可以开始去做其它的事。而另一方面,从 kernel 的角度,当它收到一个请求之后,首先它会立刻返回,所以不会对用户进程产生任何 block。然后,kernel 会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel 会给用户进程发送一个 signal,告诉它 I/O 操作完成了。

AIO 最重要的一点是 从内核缓冲区拷贝数据到用户态缓冲区的过程也是由系统异步完成,应用进程只需要在指定的数组中引用数据即可。

AIO 与信号驱动 I/O 的主要区别:

信号驱动 I/O 由内核通知何时可以开始一个 I/O 操作,而异步 I/O 由内核通知 I/O 操作何时已经完成。

“关键钥匙”分析:

"数据等待"阶段,非阻塞

"数据拷贝”阶段,非阻塞

AIO 是真正的异步模型,它不会对请求进程产生任何的阻塞。

3. 同步=阻塞?异步=非阻塞?

日常使用过程中,我们往往把 同步 I/O 等同于 阻塞 I/O,异步 I/O 等同于 非阻塞 I/O。

实际上,严格意义来说,这两组概念还是有很大的区别的。

3.1 阻塞 I/O 与 非阻塞 I/O

阻塞与非阻塞的区别比较明显,也很好理解。


结合 I/O 模型来说,阻塞 I/O 会一直 block 对应的进程直到操作完成,而非阻塞 IO 在 kernel 在"等待数据准备"阶段会立刻返回。


所以我们一般认为,阻塞 I/O 只有 BIO,另外四个模型都是属于非阻塞 I/O。

3.2 同步 I/O 与 异步 I/O

先来看看 同步 I/O 和 异步 I/O 的定义是什么,根据 POSIX 的定义:

  • 同步 I/O : A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;

  • 异步 I/O : An asynchronous I/O operation does not cause the requesting process to be blocked;


两者的区别就在于同步 I/O 做 "IO operation”的时候会将 process 阻塞。

那么按照这个定义,我们看看前面每个模型的“关键钥匙”分析部分,可以明显看到,BIO,NIO,IO 多路复用、信号驱动 IO 四种模型都属于 同步 IO。

因为它们在 IO 的第二阶段,真正执行“数据拷贝”的阶段,都是“阻塞”的。以 NIO 为例,在执行 recvfrom 这个系统调用的时候,如果 kernel 的数据没有准备好,这时候不会 block 进程。但是当 kernel 中数据准备好的时候,recvfrom 会将数据从 kernel 拷贝到用户内存中,这个时候进程是被 block 了。

同理,信号驱动 IO,当内核中 IO 数据就绪时以 SIGIO 信号通知请求进程,请求进程再把数据从内核读入到用户空间,这一步也是阻塞的。

所以,真正的异步 I/O 只有一个,就是 AIO。当进程发起 IO 操作之后,就直接返回再也不管了,直到 kernel 发送一个信号,告诉进程说 IO 完成。在这整个过程中,进程完全没有被阻塞。如定义所说,不会因为 IO 操作阻塞。

4. Netty 采用了哪种 I/O 模型呢?

Netty 的 I/O 模型是基于非阻塞 I/O 实现的,底层依赖的是 JDK NIO 框架的多路复用器 Selector。

一个多路复用器 Selector 可以同时轮询多个 Channel,采用 epoll 模式后,只需要一个线程负责 Selector 的轮询,就可以接入成千上万的客户端。

更具体的实现方式和模型,我们下一期再展开说明。

对了,一定有同学想问,Netty 为什么不采用 AIO 呢?

因为 AIO 的目的是希望 I/O 线程不阻塞主线程,属于异步 I/O,由内核通知 I/O 操作何时完成。AIO 适用于连接数多的且需要长时间连接的场景。

对于 AIO 来说,目前操作系统支持程度有限且实现起来复杂。

Netty 也尝试过 AIO,但是效果不是很理想,最终废弃了。


原文链接:https://www.cnblogs.com/awan-note/p/14292721.html

用户头像

云流

关注

还未添加个人签名 2020.09.02 加入

还未添加个人简介

评论

发布
暂无评论
没搞清楚网络I/O模型?那怎么入门Netty