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

几年前的一个下午,公司里码农们正在安静地敲着代码,忽然不少人的手机同时“哔哔”地响了起来。原本觉得发工资了,都挺高兴!打开一看,原来是告警短信java

故障回顾

告警提示“线程数过多,超出阈值”,“CPU空闲率过低”。打开监控系统一看,订单服务全部20个服务节点都不行了,服务没响应。程序员

每一个springboot节点线程数全都达到了最大值。可是JVM堆内存和GC没有明显异常。CPU 空闲率基本都是0%,可是CPU使用率并不高,反而IO等待却很是高。下面是执行top命令查看CPU情况的截图:spring

 

 

从上图,咱们能够看到:数据库

CPU空闲率是0%(上图中红框id)编程

CPU使用率是22%(上图中红框 us 13% 加上 sy 9%,us能够理解成用户进程占用的CPU,sy能够理解成系统进程占用的CPU)springboot

CPU 在等待磁盘IO操做上花费的时间占比是76.6% (上图中红框 wa)服务器

到如今能够肯定,问题确定发生在IO等待上。利用监控系统和jstack命令,最终定位问题发生在文件写入上。大量的磁盘读写致使了JVM线程资源耗尽(注意,不表明系统CPU耗尽)。最终致使订单服务没法响应上游服务的请求。网络

IO,你不知道的那些事儿

既然IO对系统性能和稳定性影响这么大,咱们就来深刻探究一下。架构

所谓的I/O(Input/Output)操做实际上就是输入输出的数据传输行为。程序员最关注的主要是磁盘IO和网络IO,由于这两个IO操做和应用程序的关系最直接最紧密。并发

磁盘IO:磁盘的输入输出,好比磁盘和内存之间的数据传输。

网络IO:不一样系统间跨网络的数据传输,好比两个系统间的远程接口调用。

下面这张图展现了应用程序中发生IO的具体场景:

 

 

经过上图,咱们能够了解到IO操做发生的具体场景。一个请求过程可能会发生不少次的IO操做:

  1. 页面请求到服务器会发生网络IO

  2. 服务之间远程调用会发生网络IO

  3. 应用程序访问数据库会发生网络IO

  4. 数据库查询或者写入数据会发生磁盘IO

IO和CPU的关系

很多攻城狮会这样理解,若是CPU空闲率是0%,就表明CPU已经在满负荷工做,没精力再处理其余任务了。真是这样的吗?

咱们先看一下计算机是怎么管理磁盘IO操做的。计算机发展早期,磁盘和内存的数据传输是由CPU控制的,也就是说从磁盘读取数据到内存中,是须要CPU存储和转发的,期间CPU一直会被占用。咱们知道磁盘的读写速度远远比不上CPU的运转速度。这样在传输数据时就会占用大量CPU资源,形成CPU资源严重浪费。

后来有人设计了一个IO控制器,专门控制磁盘IO。当发生磁盘和内存间的数据传输前,CPU会给IO控制器发送指令,让IO控制器负责数据传输操做,数据传输完IO控制器再通知CPU。所以,从磁盘读取数据到内存的过程就再也不须要CPU参与了,CPU能够空出来处理其余事情,大大提升了CPU利用率。这个IO控制器就是“DMA”,即直接内存访问,Direct Memory Access。如今的计算机基本都采用这种DMA模式进行数据传输。

 

 

经过上面内容咱们了解到,IO数据传输时,是不占用CPU的。当应用进程或线程发生IO等待时,CPU会及时释放相应的时间片资源并把时间片分配给其余进程或线程使用,从而使CPU资源获得充分利用。因此,假如CPU大部分消耗在IO等待(wa)上时,即使CPU空闲率(id)是0%,也并不意味着CPU资源彻底耗尽了,若是有新的任务来了,CPU仍然有精力执行任务。以下图:

 

 

在DMA模式下执行IO操做是不占用CPU的,因此CPU IO等待(上图的wa)实际上属于CPU空闲率的一部分。因此咱们执行top命令时,除了要关注CPU空闲率,CPU使用率(us,sy),还要关注IO Wait(wa)。注意,wa只表明磁盘IO Wait,不包括网络IO Wait。

Java中线程状态和IO的关系

当咱们用jstack查看Java线程状态时,会看到各类线程状态。当发生IO等待时(好比远程调用时),线程是什么状态呢,Blocked仍是Waiting?

答案是Runnable状态,是否是有些出乎意料!实际上,在操做系统层面Java的Runnable状态除了包括Running状态,还包括Ready(就绪状态,等待CPU调度)和IO Wait等状态。

 

 

如上图,Runnable状态的注解明确说明了,在JVM层面执行的线程,在操做系统层面可能在等待其余资源。若是等待的资源是CPU,在操做系统层面线程就是等待被CPU调度的Ready状态;若是等待的资源是磁盘网卡等IO资源,在操做系统层面线程就是等待IO操做完成的IO Wait状态。

有人可能会问,为何Java线程没有专门的Running状态呢?

目前绝大部分主流操做系统都是以时间分片的方式对任务进行轮询调度,时间片一般很短,大概几十毫秒,也就是说一个线程每次在cpu上只能执行几十毫秒,而后就会被CPU调度出来变成Ready状态,等待再一次被CPU执行,线程在Ready和Running两个状态间快速切换。一般状况,JVM线程状态主要为了监控使用,是给人看的。当你看到线程状态是Running的一瞬间,线程状态早已经切换N次了。因此,再给线程专门加一个Running状态也就没什么意义了。

深刻理解网络IO模型

5种Linux网络IO模型包括:同步阻塞IO、同步非阻塞IO、多路复用IO、信号驱动IO和异步IO。

写在前面

为了更好地理解网络IO模型,咱们先了解几个基本概念。

Socket(套接字):Socket能够理解成,在两个应用程序进行网络通讯时,分别在两个应用程序中的通讯端点。通讯时,一个应用程序将数据写入Socket,而后经过网卡把数据发送到另一个应用程序的Socket中。咱们日常所说的HTTP和TCP协议的远程通讯,底层都是基于Socket实现的。5种网络IO模型也都要基于Socket实现网络通讯。

阻塞与非阻塞:所谓阻塞,就是发出一个请求不能马上返回响应,要等全部的逻辑全处理完才能返回响应。非阻塞反之,发出一个请求马上返回应答,不用等处理完全部逻辑。

内核空间与用户空间:在Linux中,应用程序稳定性远远比不上操做系统程序,为了保证操做系统的稳定性,Linux区分了内核空间和用户空间。能够这样理解,内核空间运行操做系统程序和驱动程序,用户空间运行应用程序。Linux以这种方式隔离了操做系统程序和应用程序,避免了应用程序影响到操做系统自身的稳定性。这也是Linux系统超级稳定的主要缘由。全部的系统资源操做都在内核空间进行,好比读写磁盘文件,内存分配和回收,网络接口调用等。因此在一次网络IO读取过程当中,数据并非直接从网卡读取到用户空间中的应用程序缓冲区,而是先从网卡拷贝到内核空间缓冲区,而后再从内核拷贝到用户空间中的应用程序缓冲区。对于网络IO写入过程,过程则相反,先将数据从用户空间中的应用程序缓冲区拷贝到内核缓冲区,再从内核缓冲区把数据经过网卡发送出去。

同步阻塞IO

咱们先看一下传统阻塞IO。在Linux中,默认状况下全部socket都是阻塞模式的。当用户线程调用系统函数read(),内核开始准备数据(从网络接收数据),内核准备数据完成后,数据从内核拷贝到用户空间的应用程序缓冲区,数据拷贝完成后,请求才返回。从发起read请求到最终完成内核到应用程序的拷贝,整个过程都是阻塞的。为了提升性能,能够为每一个链接都分配一个线程。所以,在大量链接的场景下就须要大量的线程,会形成巨大的性能损耗,这也是传统阻塞IO的最大缺陷。

 

 

同步非阻塞IO

用户线程在发起Read请求后当即返回,不用等待内核准备数据的过程。若是Read请求没读取到数据,用户线程会不断轮询发起Read请求,直到数据到达(内核准备好数据)后才中止轮询。非阻塞IO模型虽然避免了因为线程阻塞问题带来的大量线程消耗,可是频繁的重复轮询大大增长了请求次数,对CPU消耗也比较明显。这种模型在实际应用中不多使用。

 

 

多路复用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都采用了这种模型。

 

 

下图是基于select函数Socket编程的详细流程。

 

 

信号驱动IO模型

信号驱动IO模型,应用进程使用sigaction函数,内核会当即返回,也就是说内核准备数据的阶段应用进程是非阻塞的。内核准备好数据后向应用进程发送SIGIO信号,接到信号后数据被复制到应用程序进程。

采用这种方式,CPU的利用率很高。不过这种模式下,在大量IO操做的状况下可能形成信号队列溢出致使信号丢失,形成灾难性后果。

异步IO模型

异步IO模型的基本机制是,应用进程告诉内核启动某个操做,内核操做完成后再通知应用进程。在多路复用IO模型中,socket状态事件到达,获得通知后,应用进程才开始自行读取并处理数据。在异步IO模型中,应用进程获得通知时,内核已经读取完数据并把数据放到了应用进程的缓冲区中,此时应用进程直接使用数据便可。

很明显,异步IO模型性能很高。不过到目前为止,异步IO和信号驱动IO模型应用并很少见,传统阻塞IO和多路复用IO模型仍是目前应用的主流。Linux2.6版本后才引入异步IO模型,目前不少系统对异步IO模型支持尚不成熟。不少应用场景采用多路复用IO替代异步IO模型。

如何避免IO问题带来的系统故障

对于磁盘文件访问的操做,能够采用线程池方式,并设置线程上线,从而避免整个JVM线程池污染,进而致使线程和CPU资源耗尽。

对于网络间远程调用。为了不服务间调用的全链路故障,要设置合理的TImeout值,高并发场景下能够采用熔断机制。在同一JVM内部采用线程隔离机制,把线程分为若干组,不一样的线程组分别服务于不一样的类和方法,避免由于一个小功能点的故障,致使JVM内部全部线程受到影响。

此外,完善的运维监控(磁盘IO,网络IO)和APM(全链路性能监控)也很是重要,能及时预警,防患于未然,在故障发生时也能帮助咱们快速定位问题。

看完三件事❤️

若是你以为这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发,有大家的 『点赞和评论』,才是我创造的动力。

  2. 关注公众号 『 java烂猪皮 』,不按期分享原创知识。

  3. 同时能够期待后续文章ing🚀

 

 

本文做者:冯涛 来自公众号:架构师进阶之路