Categories
程式開發

重大事故! IO問題引發線上20台機器同時崩潰


幾年前的一個下午,公司裡碼農們正在安靜地敲著代碼,突然很多人的手機同時“嗶嗶”地響了起來。本來以為發工資了,都挺高興!打開一看,原來是告警短信

故障回顧

告警提示“線程數過多,超出閾值”,“CPU空閒率太低”。打開監控系統一看,訂單服務所有20個服務節點都不行了,服務沒響應。

每個springboot節點線程數全都達到了最大值。但是JVM堆內存和GC沒有明顯異常。 CPU 空閒率基本都是0%,但是CPU使用率並不高,反而IO等待卻非常高。下面是執行top命令查看CPU狀況的截圖:

重大事故! IO問題引發線上20台機器同時崩潰 1

從上圖,我們可以看到:

CPU空閒率是0%(上圖中紅框id)

CPU使用率是22%(上圖中紅框us 13% 加上sy 9%,us可以理解成用戶進程佔用的CPU,sy可以理解成系統進程佔用的CPU)

CPU 在等待磁盤IO操作上花費的時間佔比是76.6% (上圖中紅框wa)

到現在可以確定,問題肯定發生在IO等待上。利用監控系統和jstack命令,最終定位問題發生在文件寫入上。大量的磁盤讀寫導致了JVM線程資源耗盡(注意,不代表系統CPU耗盡)。最終導致訂單服務無法響應上游服務的請求。

IO,你不知道的那些事兒

既然IO對系統性能和穩定性影響這麼大,我們就來深入探究一下。

所謂的I/O(Input/Output)操作實際上就是輸入輸出的數據傳輸行為。程序員最關注的主要是磁盤IO和網絡IO,因為這兩個IO操作和應用程序的關係最直接最緊密。

磁盤IO:磁盤的輸入輸出,比如磁盤和內存之間的數據傳輸。

網絡IO:不同系統間跨網絡的數據傳輸,比如兩個系統間的遠程接口調用。

下面這張圖展示了應用程序中發生IO的具體場景:

重大事故! IO問題引發線上20台機器同時崩潰 2

通過上圖,我們可以了解到IO操作發生的具體場景。一個請求過程可能會發生很多次的IO操作:

頁面請求到服務器會發生網絡IO服務之間遠程調用會發生網絡IO應用程序訪問數據庫會發生網絡IO數據庫查詢或者寫入數據會發生磁盤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問題引發線上20台機器同時崩潰 3

通過上面內容我們了解到,IO數據傳輸時,是不佔用CPU的。當應用進程或線程發生IO等待時,CPU會及時釋放相應的時間片資源並把時間片分配給其他進程或線程使用,從而使CPU資源得到充分利用。所以,假如CPU大部分消耗在IO等待(wa)上時,即便CPU空閒率(id)是0%,也並不意味著CPU資源完全耗盡了,如果有新的任務來了,CPU仍然有精力執行任務。如下圖:

重大事故! IO問題引發線上20台機器同時崩潰 4

在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等狀態。

重大事故! IO問題引發線上20台機器同時崩潰 5

如上圖,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問題引發線上20台機器同時崩潰 6

同步非阻塞IO

用戶線程在發起Read請求後立即返回,不用等待內核准備數據的過程。如果Read請求沒讀取到數據,用戶線程會不斷輪詢發起Read請求,直到數據到達(內核准備好數據)後才停止輪詢。非阻塞IO模型雖然避免了由於線程阻塞問題帶來的大量線程消耗,但是頻繁的重複輪詢大大增加了請求次數,對CPU消耗也比較明顯。這種模型在實際應用中很少使用。

重大事故! IO問題引發線上20台機器同時崩潰 7

多路復用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台機器同時崩潰 8

下圖是基於select函數Socket編程的詳細流程。

重大事故! IO問題引發線上20台機器同時崩潰 9

信號驅動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(全鏈路性能監控)也非常重要,能及時預警,防患於未然,在故障發生時也能幫助我們快速定位問題。

看完三件事❤️

如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:

點贊,轉發,有你們的『點贊和評論』,才是我創造的動力。關注公眾號『 java爛豬皮』,不定期分享原創知識。同時可以期待後續文章ing🚀

重大事故! IO問題引發線上20台機器同時崩潰 10

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