對cpu與load的理解及線上問題處理思路

cpu如何計算java

當咱們執行top命令的時候,看到裏面的值(主要是cpu和load)值是一直在變的,所以有必要簡單瞭解一下Linux系統中cpu的計算方式。ios

cpu分爲系統cpu和進程、線程cpu,系統cpu的統計值位於/proc/stat下(如下的截圖未截全):數據庫

cpu、cpu0後面的這些數字都和前面的us、sy、ni這些對應,具體哪一個對應哪一個值不重要,感興趣的能夠網上查一下文檔。服務器

進程cpu的統計值位於/proc/{pid}/stat下:網絡

線程cpu的統計值位於/proc/{pid}/task/{threadId}/stat下: 多線程

 

這裏面的全部值都是從系統啓動時間到當前時間的一個值。所以,對於cpu的計算的作法是,採樣兩個足夠短的時間t一、t2:架構

  • 將t1的全部cpu使用狀況求和,獲得s1
  • 將t2的全部cpu使用狀況求和,獲得s2
  • s2 - s1獲得這個時間間隔內的全部時間totalCpuTime
  • 第一次的空閒idle1 - 第二次的空閒idle2,獲取採樣時間內的空閒時間
  • cpu使用率 = 100 * (totalCpuTime - idle) / totalCpuTime

其餘時間例如us、sy、ni都是相似的計算方式,總結起來講,cpu這個值反應的是某個採樣時間內的cpu使用狀況。所以有時候cpu很高,可是打印線程堆棧出來發現高cpu的線程在查詢數據庫等待中,不要以爲奇怪,由於cpu統計的是採樣時間內的數據。併發

假設top觀察某段時間用戶空間cpu一直很高,那麼意味着這段時間用戶的程序一直在佔據着cpu作事情。框架

 

對load的理解工具

關於load的含義,其實有些文章把它跟行車過橋聯繫在一塊兒是比較恰當和好理解的:

複製代碼
一個單核的處理器能夠形象得比喻成一條單車道,車輛依次行駛在這條單車道上,前車駛過以後後車才能夠行駛。
若是前面沒有車輛,那麼你順利經過;若是車輛衆多,那麼你須要等待前車經過以後才能夠經過。 所以,須要些特定的代號表示目前的車流狀況,例如: ·等於0.00,表示目前橋面上沒有任何的車流。實際上這種狀況0.00和1.00之間是相同的,總而言之很通暢,過往的車輛能夠絲絕不用等待的經過 ·等於1.00,表示恰好是在這座橋的承受範圍內。這種狀況不算糟糕,只是車流會有些堵,不過這種狀況可能會形成交通愈來愈慢 ·大於1.00,那麼說明這座橋已經超出負荷,交通嚴重的擁堵。那麼狀況有多糟糕? 例如2.00的狀況說明車流已經超出了橋所能承受的一倍,那麼將有多餘過橋一倍的車輛正在焦急的等待
複製代碼

可是比喻終歸是比喻,從比喻中咱們瞭解了,load表示的是系統的一個能力,可是咱們殊不知道什麼樣的任務會被歸到load的計算中。關於具體怎麼樣的任務會被歸到load的計算中,可使用man uptime命令看一下Linux對於load的解釋:

大體意思就是說,系統load是處於運行狀態或者不可中斷狀態的進程的平均數(標紅部分表示被算入load的內容)。一個處於運行狀態的進程表示正在使用cpu或者等待使用cpu,一個不可中斷狀態的進程表示正在等待IO,例如磁盤IO。load的平均值經過3個時間間隔來展現,就是咱們看到的1分鐘、5分鐘、15分鐘,load值和cpu核數有關,單核cpu的load=1表示系統一直處在負載狀態,可是4核cpu的load=1表示系統有75%的空閒。

特別注意,load指的是全部核的平均值,這和cpu的值是有區別的。

還有一個重要的點是,查了資料發現,雖然上面一直強調的是"進程",可是進程中的線程數也是會被看成不一樣的進程來計算的,假如一個進程產生1000個線程同時運行,那運行隊列的長度就是1000,load average就是1000。

 

請求數和load的關係

以前我本身一直有個誤區:當成千上萬的請求過來,且在排隊的時候,後面的請求得不處處理,load值必然會升高。認真思考以後,這個觀點可真是大錯特錯,所以特別做爲一段寫一下和你們分享。

以Redis爲例,咱們都知道Redis是單線程模型的,這意味着同一時間能夠有無數個請求過來,可是同一時間只有一個命令會被處理(圖片來源https://www.processon.com/view/5c2ddab0e4b0fa03ce89d14f):

單獨的一條線程接到就緒的命令以後,會將命令轉給事件分發器,事件分發器根據命令的類型執行對應的命令處理邏輯。因爲只有一條線程,只要後面排隊的命令足夠多到讓這條線程一個接一個不停地處理命令,那麼load表現就等於1。

整個過程當中,回看load這個值,它和請求數沒有任何關係,真正和load相關的是工做線程數量,main線程是工做線程、Timer是工做線程、GC線程也是工做線程,load是以線程/進程做爲統計指標,不管請求數是多少,最終都須要線程去處理,而工做線程的處理性能直接決定了最終的load值。

舉個例子,假設一個服務中有一個線程池,線程池中線程數量固定爲64:

  • 正常來講一個任務執行時間爲10ms,線程拿到任務10ms處理完,很快回歸線程池等待下一個任務到來,天然不多有處於運行狀態或者等待IO的線程,從一個統計週期來看load表現爲很低
  • 某段時間因爲系統問題,一個任務10s都處理不完,至關於線程一直在處理任務,在load的統計週期裏面就體現出的值=64(不考慮這64條線程外的場景)

所以,總而言之,搞清楚load值和請求數、線程數的關係很是重要,想清楚這些才能正確地進行下一步的工做。

 

load高、cpu高的問題排查思路

首先拋出一個觀點:cpu高不是問題,由cpu高引發的load高才是問題,load是判斷系統能力指標的依據

爲何這麼說呢,以單核cpu爲例,當咱們平常cpu在20%、30%的時候其實對cpu資源是浪費的,這意味着絕大多數時候cpu並無在作事,理論上來講一個系統極限cpu利用率能夠達到100%,這意味着cpu徹底被利用起來了處理計算密集型任務,例如for循環、md5加密、new對象等等。可是實際不可能出現這種狀況,由於應用程序中不消耗cpu的IO不存在是幾乎不可能的,例如讀取數據庫或者讀取文件,所以cpu不是越高越好,一般75%是一個須要引發警惕的經驗值。

注意前面提到的是"引發警惕",意味着cpu高不必定是問題,可是須要去看一下,尤爲是平常的時候,由於一般平常流量不大,cpu是不可能打到這麼高的。若是隻是普通的代碼中確實在處理正常業務那沒問題,若是代碼裏面出現了死循環(例如JDK1.7中經典的HashMap擴容引起的死循環問題),那麼幾條線程一直佔着cpu,最後就會形成load的增高。

在一個Java應用中,排查cpu高的思路一般比較簡單,有比較固定的作法:

  • ps -ef | grep java,查詢Java應用的進程pid
  • top -H -p pid,查詢佔用cpu最高的線程pid
  • 將10進制的線程pid轉成16進制的線程pid,例如2000=0x7d0
  • jstack 進程pid | grep -A 20 '0x7d0',查找nid匹配的線程,查看堆棧,定位引發高cpu的緣由

網上有不少文章寫到這裏就停了,實踐過程當中並非這樣。由於cpu是時間段內的統計值、jstack是一個瞬時堆棧只記錄瞬時狀態,兩個根本不是一個維度的事,所以徹底有可能從打印出來的堆棧行號中看到代碼停留在如下地方:

  • 不消耗cpu的網絡IO
  • for (int i = 0, size = list.size(); i < size; i++) {...}
  • 調用native方法

若是徹底按照上面那一套步驟作的話碰到這種狀況就傻眼了,左思右想半天卻不得其解,根本不明白爲何這種代碼會致使高cpu。針對可能出現的這種狀況,實際排查問題的時候jstack建議打印5次至少3次,根據屢次的堆棧內容,再結合相關代碼段進行分析,定位高cpu出現的緣由,高cpu多是代碼段中某個bug致使的而不是堆棧打印出來的那幾行致使的

另外,cpu高的狀況還有一種可能的緣由,假如一個4核cpu的服務器咱們看到總的cpu達到了100%+,按1以後觀察每一個cpu的us,只有一個達到了90%+,其餘都在1%左右(下圖只是演示top按1以後的效果並不是真實場景):

這種狀況下能夠重點考慮是否是頻繁FullGC引發的。由於咱們知道FullGC的時候會有Stop The World這個動做,多核cpu的服務器,除了GC線程外,在Stop The World的時候都是會掛起的,直到Stop The World結束。以幾種老年代垃圾收集器爲例:

  • Serial Old收集器,全程Stop The World
  • Parallel Old收集器,全程Stop The World
  • CMS收集器,它在初始標記與併發標記兩個過程當中,爲了準確標記出須要回收的對象,都會Stop The World,可是相比前兩種大大減小了系統停頓時間

不管如何,當真正發生Stop The World的時候,就會出現GC線程在佔用cpu工做而其餘線程掛起的狀況,天然表現也就爲某個cpu的us很高並且他cpu的us很低。

針對FullGC的問題,排查思路一般爲:

  • ps -ef | grep java,查詢Java應用的進程pid
  • jstat -gcutil pid 1000 1000,每隔1秒打印一次內存狀況共打印1000次,觀察老年代(O)、MetaSpace(MU)的內存使用率與FullGC次數
  • 確認有頻繁的FullGC的發生,查看GC日誌,每一個應用GC日誌配置的路徑不一樣
  • jmap -dump:format=b,file=filename pid,保留現場
  • 重啓應用,迅速止血,避免引發更大的線上問題
  • dump出來的內容,結合MAT分析工具分析內存狀況,排查FullGC出現的緣由

若是FullGC只是發生在老年代區,比較有經驗的開發人員仍是容易發現問題的,通常都是一些代碼bug引發的。MetaSpace發生的FullGC常常會是一些詭異、隱晦的問題,不少和引入的第三方框架使用不當有關或者就是第三方框架有bug致使的,排查起來就很費時間。

那麼頻繁FullGC以後最終會致使load如何變化呢?這個我沒有驗證過和看過具體數據,只是經過理論分析,若是全部線程都是空閒的,只有GC線程在一直作FullGC,那麼load最後會趨近於1。可是實際不可能,由於若是沒有其餘線程在運行,怎麼可能致使頻繁FullGC呢。因此,在其餘線程處理任務的狀況下Stop The World以後,cpu掛起,任務得不處處理,更大可能的是load會一直升高。

最後順便提一句,前面一直在講FullGC,頻繁的YoungGC也是會致使load升高的,以前看到過的一個案例是,Object轉xml,xml轉Object,代碼中每處都new XStream()去進行xml序列化與反序列化,回收速度跟不上new的速度,YoungGC次數陡增。

 

load高、cpu低的問題排查思路

關於load的部分,咱們能夠看到會致使load高的幾個因素:

  • 線程正在使用cpu
  • 線程正在等待使用cpu
  • 線程在執行不可被打斷的IO操做

既然cpu不高,load高,那麼線程要麼在進行io要麼在等待使用cpu。不過對於後者"等待使用cpu"我這裏存疑,好比線程池裏面10個線程,任務來的很慢,每次只會用到1個線程,那麼9個線程都是在等待使用cpu,可是這9個線程明顯是不會佔據系統資源的,所以我認爲天然也不會消耗cpu,因此這個點不考慮。

所以,在cpu不高的狀況下假如load高,大機率io高才是罪魁禍首,它致使的是任務一直在跑,遲遲處理不完,線程沒法迴歸線程池中。首先簡單講講磁盤io,既然wa表示的是磁盤io等待cpu的百分比,那麼咱們能夠看下wa確認下是否是磁盤io致使的:

若是是,那麼按照cpu高一樣的方式打印一下堆棧,查看文件io的部分進行分析,排查緣由,例如是否是多線程都在讀取本地一個超大的文件到內存。

磁盤io致使的load高,我相信這畢竟是少數,由於Java語言的特色,應用程序更多的高io應當是在處理網絡請求,例如:

  • 從數據庫中獲取數據
  • 從Redis中獲取數據
  • 調用Http接口從支付寶獲取數據
  • 經過dubbo獲取某服務中的數據

針對這種狀況,我以爲首先咱們應該對整個系統架構的依賴比較熟悉,例如我畫一個草圖:

對依賴方的調用任何一個出現比較高的耗時都會增長自身系統的load,出現load高的建議排查方式爲:

  • 查日誌,不管是HBase、MySql、Redis調用仍是經過http、dubbo調用接口,調用超時,拿鏈接池中的鏈接超時,一般都會有錯誤日誌拋出來,只要系統裏面沒有捕獲異常以後不打日誌直接吞掉通常都能查到相關的異常
  • 對於dubbo、http的調用,建議作好監控埋點,輸出接口名、方法入參(控制大小)、是否成功、調用時長等必要參數,有些時候可能沒有超時,可是調用2秒、3秒同樣會致使load升高,因此這種時候須要查看方法調用時長進行下一步動做

若是上面的步驟仍是沒用或者沒有對接口調用作埋點,那麼仍是萬能的打印堆棧吧,連續打印五次十次,看一下每次的堆棧是否大多都指向同一個接口的調用,網絡io的話,堆棧的最後幾行通常都有at java.net.SocketInputStream.read(SocketInputStream.java:129)

 

Java應用load高的幾種緣由總結

前面說了這麼多,這裏總結一下load高常見的、可能的一些緣由:

  • 死循環或者不合理的大量循環操做,若是不是循環操做,按照現代cpu的處理速度來講處理一大段代碼也就一會會兒的事,基本對能力無消耗
  • 頻繁的YoungGC
  • 頻繁的FullGC
  • 高磁盤IO
  • 高網絡IO

線上遇到問題的時候首先不要慌,由於大部分load高的問題都集中在以上幾個點裏面,如下分析問題的步驟或許能幫你整理思路:

  • top先查看用戶us與空閒us(id)的cpu佔比,目的是確認load高是否高cpu起的
  • 若是是高cpu引發的,那麼確認一下是否gc引發的,jstat命令 + gc日誌基本就能確認
  • gc引發的高cpu直接dump,非gc引發的分析線程堆棧
  • 若是不是高cpu引發的,查看磁盤io佔比(wa),若是是,那麼打線程堆棧分析是否有大量的文件io
  • 若是不是高cpu引發的,且不是磁盤io致使的,檢查各依賴子系統的調用耗時,高耗時的網絡調用極可能是罪魁禍首

最後仍是不行,當一籌莫展時,jstack打印堆棧多分析分析吧,或許能靈光一現能找到錯誤緣由。

 

結語

先有理論,把理論想透了,實戰碰到問題的時候才能頭腦清楚。

坦白講,cpu和load高排查是一個很偏實戰的事情,這方面我還也有很長一條路須要走,身邊在這塊經驗比我豐富的同事多得很。不少人有問過我,項目比較簡單,根本沒有這種線上問題須要我去排查怎麼辦?這個問題只能說,平時多積累、多實戰是惟一途徑,假如沒有實戰機會,那麼推薦三種方式:

  • 本身經過代碼模擬各類異常,例如FullGC、死鎖、死循環,而後利用工具去查,可能比較簡單,可是萬丈高樓平地起,再複雜的東西都是由簡單的變化過來的
  • 多上服務器上敲敲top、sar、iostat這些命令,熟記每一個命令的做用及輸出參數的含義
  • 去網上找一下其餘人處理FullGC、cpu高方法的文章,站在巨人的肩膀上,看看前人走過的路,總結記錄一些實用的點

當真的有實戰機會來的時候把握住,即便是同事排查的問題,也能夠在過後搞清楚問題的前因後果,長此以往天然這方面的能力就會提升上去。

相關文章
相關標籤/搜索