性能调优必备:NIO的优化实现原理

我们就从底层的网络 I/O 模型优化出发,再到内存拷贝优化和线程模型优化,深入分析下 Tomcat、Netty 等通信框架是如何通过优化 I/O 来提高系统性能的。

网络 I/O 模型优化

网络通信中,最底层的就是内核中的网络 I/O 模型了。 随着技术的发展,操作系统内核的网络模型衍生出了五种 I/O 模型,《UNIX 网络编程》一书将这五种 I/O 模型分为阻塞式 I/O、非阻塞式 I/O、I/O 复用、信号驱动式 I/O 和异步 I/O。每一种 I/O 模型的出现,都是基于前一种 I/O 模型的优化升级。

最开始的阻塞式 I/O,它在每一个连接创建时,都需要一个用户线程来处理, 并且在 I/O 操作没有就绪或结束时,线程会被挂起,进入阻塞等待状态,阻塞式 I/O 就成为了导致性能瓶颈的根本原因。

那阻塞到底发生在套接字(socket)通信的哪些环节呢?

在《Unix 网络编程》中,套接字通信可以分为流式套接字(TCP)和数据报套接字(UDP)。其中 TCP 连接是我们最常用的,一起来了解下 TCP 服务端的工作流程 (由于 TCP 数据传输比较复杂,存在拆包和装包的可能,这里我只假设一次最简单的 TCP 数据传输):

面试官:NIO的优化实现原理了解吗?图文结合教你如何正确避坑

首先, 应用程序通过系统调用 socket 创建一个套接字,它是系统分配给应用程序的一个文件描述符;

其次, 应用程序会通过系统调用 bind,绑定地址和端口号,给套接字命名一个名称;

然后, 系统会调用 listen 创建一个队列用于存放客户端进来的连接;

最后, 应用服务会通过系统调用 accept 来监听客户端的连接请求。

当有一个客户端连接到服务端之后,服务端就会调用 fork 创建一个子进程,通过系统调用 read 监听客户端发来的消息,再通过 write 向客户端返回信息。

1. 阻塞式 I/O

在整个 socket 通信工作流程中,socket 的默认状态是阻塞的。 也就是说,当发出一个不能立即完成的套接字调用时,其进程将被阻塞,被系统挂起,进入睡眠状态,一直等待相应的操作响应。从上图中,我们可以发现,可能存在的阻塞主要包括以下三种。

connect 阻塞:当客户端发起 TCP 连接请求,通过系统调用 connect 函数,TCP 连接的建立需要完成三次握手过程,客户端需要等待服务端发送回来的 ACK 以及 SYN 信号,同样服务端也需要阻塞等待客户端确认连接的 ACK 信号,这就意味着 TCP 的每个 connect 都会阻塞等待,直到确认连接。

面试官:NIO的优化实现原理了解吗?图文结合教你如何正确避坑

accept 阻塞:一个阻塞的 socket 通信的服务端接收外来连接,会调用 accept 函数,如果没有新的连接到达,调用进程将被挂起,进入阻塞状态。

面试官:NIO的优化实现原理了解吗?图文结合教你如何正确避坑

read、write 阻塞:当一个 socket 连接创建成功之后,服务端用 fork 函数创建一个子进程, 调用 read 函数等待客户端的数据写入,如果没有数据写入,调用子进程将被挂起,进入阻塞状态。

面试官:NIO的优化实现原理了解吗?图文结合教你如何正确避坑

2. 非阻塞式 I/O

使用 fcntl 可以把以上三种操作都设置为非阻塞操作。 如果没有数据返回,就会直接返回一个 EWOULDBLOCK 或 EAGAIN 错误,此时进程就不会一直被阻塞。

当我们把以上操作设置为了非阻塞状态,我们需要设置一个线程对该操作进行轮询检查,这也是最传统的非阻塞 I/O 模型。

面试官:NIO的优化实现原理了解吗?图文结合教你如何正确避坑

3. I/O 复用

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zzpxgg.html