重大事故!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🚀

 

 

本文做者:馮濤 來自公衆號:架構師進階之路

相關文章
相關標籤/搜索