重大事故!IO问题引发线上20台机器同时崩溃( 三 )


当用户线程调用系统函数 read() , 内核开始准备数据(从网络接收数据) , 内核准备数据完成后 , 数据从内核拷贝到用户空间的应用程序缓冲区 , 数据拷贝完成后 , 请求才返回 。
从发起 Read 请求到最终完成内核到应用程序的拷贝 , 整个过程都是阻塞的 。为了提高性能 , 可以为每个连接都分配一个线程 。
因此 , 在大量连接的场景下就需要大量的线程 , 会造成巨大的性能损耗 , 这也是传统阻塞 IO 的最大缺陷 。

重大事故!IO问题引发线上20台机器同时崩溃

文章插图
 
同步非阻塞 IO
用户线程在发起 Read 请求后立即返回 , 不用等待内核准备数据的过程 。如果 Read 请求没读取到数据 , 用户线程会不断轮询发起 Read 请求 , 直到数据到达(内核准备好数据)后才停止轮询 。
非阻塞 IO 模型虽然避免了由于线程阻塞问题带来的大量线程消耗 , 但是频繁的重复轮询大大增加了请求次数 , 对 CPU 消耗也比较明显 。这种模型在实际应用中很少使用 。
重大事故!IO问题引发线上20台机器同时崩溃

文章插图
 
多路复用 IO 模型
多路复用 IO 模型 , 建立在多路事件分离函数 Select , Poll , Epoll 之上 。
在发起 Read 请求前 , 先更新 Select 的 Socket 监控列表 , 然后等待 Select 函数返回(此过程是阻塞的 , 所以说多路复用 IO 也是阻塞 IO 模型) 。
当某个 Socket 有数据到达时 , Select 函数返回 。此时用户线程才正式发起 Read 请求 , 读取并处理数据 。
这种模式用一个专门的监视线程去检查多个 Socket , 如果某个 Socket 有数据到达就交给工作线程处理 。
由于等待 Socket 数据到达过程非常耗时 , 所以这种方式解决了阻塞 IO 模型一个 Socket 连接就需要一个线程的问题 , 也不存在非阻塞 IO 模型忙轮询带来的 CPU 性能损耗的问题 。
多路复用 IO 模型的实际应用场景很多 , 比如大家耳熟能详的 Java NIO , redis 以及 Dubbo 采用的通信框架 Netty 都采用了这种模型 。
重大事故!IO问题引发线上20台机器同时崩溃

文章插图
 
下图是基于 Select 函数 Socket 编程的详细流程:
重大事故!IO问题引发线上20台机器同时崩溃

文章插图
 
信号驱动 IO 模型
信号驱动 IO 模型 , 应用进程使用 Sigaction 函数 , 内核会立即返回 , 也就是说内核准备数据的阶段应用进程是非阻塞的 。
内核准备好数据后向应用进程发送 SIGIO 信号 , 接到信号后数据被复制到应用程序进程 。
采用这种方式 , CPU 的利用率很高 。不过这种模式下 , 在大量 IO 操作的情况下可能造成信号队列溢出导致信号丢失 , 造成灾难性后果 。
异步 IO 模型
异步 IO 模型的基本机制是 , 应用进程告诉内核启动某个操作 , 内核操作完成后再通知应用进程 。
在多路复用 IO 模型中 , Socket 状态事件到达 , 得到通知后 , 应用进程才开始自行读取并处理数据 。
在异步 IO 模型中 , 应用进程得到通知时 , 内核已经读取完数据并把数据放到了应用进程的缓冲区中 , 此时应用进程直接使用数据即可 。
很明显 , 异步 IO 模型性能很高 。不过到目前为止 , 异步 IO 和信号驱动 IO 模型应用并不多见 , 传统阻塞 IO 和多路复用 IO 模型还是目前应用的主流 。
Linux 2.6 版本后才引入异步 IO 模型 , 目前很多系统对异步 IO 模型支持尚不成熟 。很多应用场景采用多路复用 IO 替代异步 IO 模型 。
如何避免 IO 问题带来的系统故障
对于磁盘文件访问的操作 , 可以采用线程池方式 , 并设置线程上线 , 从而避免整个 JVM 线程池污染 , 进而导致线程和 CPU 资源耗尽 。
对于网络间远程调用 。为了避免服务间调用的全链路故障 , 要设置合理的 TImeout 值 , 高并发场景下可以采用熔断机制 。
在同一 JVM 内部采用线程隔离机制 , 把线程分为若干组 , 不同的线程组分别服务于不同的类和方法 , 避免因为一个小功能点的故障 , 导致 JVM 内部所有线程受到影响 。


推荐阅读