ThreadDump分析實戰(性能瓶頸分析)

一、回顧

在前面我們瞭解了ThreadDump的查看方式,也大概瞭解了其能夠做些什麼,下面我們來繼續探討這個問題,不瞭解的同學回顧下以前的資料

ThreadDump分析筆記(一) 解讀堆棧

ThreadDump分析筆記(二) 分析堆棧

二、瓶頸在哪裏

改善資源也就是我們常說的性能優化,改善也就是需要在有限的資源內去做更多的事情。線程的運行因某個特定資源受阻時,我們稱之爲受限於該資源比如受限於數據庫,受限於對端的處理能力等。

其實利用併發來提高系統性能,就是意味着我們要使CPU儘可能的處於忙碌的狀態。如果程序受限於當前CPU的計算能力,那我們通過增加處理器或者集羣就可以解決問題了。但如果不能的利用CPU,使其處於忙碌狀態,那麼增加處理器也沒是無濟於事的。所以要充分的使用多線程,使空閒的處理器進行未完成的工作。

總體來說,性能提高,就是要解決受限資源,受限資源可能是:

CPU
如果當前CPU已經能夠接近100%的利用率,並且代碼業務邏輯無法再簡化,那麼說明 該系統的已經達到了性能最大化,如果再想提高性能,只能增加處理器或者集羣。

其他資源
如數據庫連接數量等,如果CPU利用率沒有接近100%,那麼通過修改代碼儘量提高CPU的使用率,那麼整體性能也會獲得極大提高。

下面我們來看一張圖,隨着系統壓力增大,但CPU使用率無法趨近於100%,如圖

性能好壞對比圖

如果在單CPU機器上無論多大壓力的情況下都無法令CPU使用率趨近於100%,那就說明這個程序還是有優化的空間的。一個系統性能瓶頸的分析過程大致如下:

  1. 先進行單流程的性能瓶頸分析,首先讓單流程的性能達到最優。(可以通過硬編碼增加時間戳來找出哪裏耗時最多,這種沒啥技巧,可具體問題具體分析)

  2. 進行整體性能瓶頸分析(這次的重點)

話說什麼是高性能?其實高性能在不同的場景下有不同的概念:

  1. 有的場合高性能意味着用戶速度的體驗,如界面操作,點擊一個菜單,響應很快我們就說性能很高

  2. 有的場合,高吞吐量意味着高性能,如短信,系統更看重吞吐量,而對每一個消息的處理時間不敏感

  3. 有的場合,是二者的結合,不但要求系統有很大的吞吐量,還要求每個消息在指定的時間內完成處理,不允許有延遲

性能調優的終極目標是:系統的CPU利用率接近100%.如果你的CPU沒有被充分利用,那 麼有如下幾個可能:

施加的壓力不足

可能是應用程序沒有足夠的負載,這時可以增加其壓力,觀察系統的響應時間,服務失敗率,和CPU的使用率情況。如果增加壓力,系統開始出現部分服務失敗,系統的響應時間變慢,或者CPU的使用率無法再上升,那麼此時的 壓力應該是系統的飽和壓力。即此時的能力是系統當前的最大能力。

系統存在瓶頸

當系統在飽和壓力下,如果CPU的使用率沒有接近100%,那麼說明這個系統的 性能還有提升的空間。

系統存在瓶頸的幾種表現:

  1. 持續運行緩慢。(時常發現應用程序運行緩慢,通過改變負載量、數據庫連接數等也無法有效提升整體響應時間)

  2. 系統性能隨時間的增加逐漸下降。(在負載穩定的情況下,系統運行時間越長速度越慢。可能是由於超出某個閾值範圍,系統運行頻繁出錯從而導致系統死鎖或崩潰)

  3. 系統性能隨負載的增加逐漸下降(隨着用戶數目的增多,運行越發緩慢。若干個用戶退出系統後,程序便能夠恢復正常運行狀態)

下面就來說說常見的集中性能瓶頸

三、幾種常見的性能瓶頸

不恰當的同步導致的資源爭用

  1. sychronized使用不當導致,不相關的方法用了同一鎖或者不同的共享變量用了同一把鎖,造成無謂的資源競爭。
class MyClass {

Object sharedObj; 
synchronized void fun1() {...} //訪問共享變量sharedObj 
synchronized void fun2() {...} //訪問共享變量sharedObj 
synchronized void fun3() {...} //不訪問共享變量sharedObj 
synchronized void fun4() {...} //不訪問共享變量sharedObj 
synchronized void fun5() {...} //不訪問共享變量sharedObj

}

Java缺省提供了this鎖,多人喜歡直接在方法上使用synchronized加鎖,很多情況下這樣做是不恰當的,如果不考慮清楚就這樣做,很容易造成鎖粒度過大。要知道方法上的synchronized 是對象鎖,要是再加個static 就是class鎖,會鎖定所有創建的對象。所以在使用的時候一定要控制好邊界。

上面的代碼將sychronized加在類的每一個方法上面,違背了保護什麼鎖什麼的原則。 對於無共享資源的兩個方法,使用了同一個鎖,人爲造成了不必要的鎖等待。

  1. 鎖的粒度過大,對共享資源訪問完成後,沒有將後續的代碼放在synchronized同步代碼塊之外。從而導致資源佔用時間過長,其他爭用鎖的線程只能等待。如下:
void fun1() {
  synchronized(lock){
    
      //1 正在訪問共享資源 ... ...
      //2 做其它耗時操作,但這些耗時操作與共享資源無關... ...
  }
}

上面的代碼會導致線程長時間佔用鎖,從而導致其他線程只能等待。但這種寫法在不同的場合優化的方式也是不一樣的,需要注意下:

單CPU 將耗時操作拿到同步塊之外,有的情況下可以提升性能,有的場合則不能。

同步塊中的耗時代碼是CPU密集型代碼(如純CPU運算等),不存在磁盤IO/網 絡IO等低CPU消耗的代碼,這種情況下,由於CPU執行這段代碼是100%的使用率,因此縮小同步塊也不會帶來任何性能上的提升。但是,同時縮小同步塊也不會帶來性能上的下降。

同步塊中的耗時代碼屬於磁盤/網絡IO等低CPU消耗的代碼,噹噹前線程正在執 行不消耗CPU的代碼時,這時候CPU是空閒的,如果此時讓CPU忙起來,可以帶來整體性能上的提升,所在在這種場景下,將耗時操作的代碼放在同步塊中,肯定是可以提高整個性能的。

多CPU 將耗時操作拿到同步塊之外,總是可以提升性能

同步塊中的耗時代碼是純CPU運算,不存在磁盤IO/網絡IO等可能不消耗CPU的 代碼,這種情況下,由於是多CPU,其它CPU也許是空閒的,因此縮小同步塊可 以讓其它線程馬上得到執行這段代碼,可以帶來性能的提升。

同步塊中的耗時代碼存在磁盤/網絡IO等不消耗CPU的代碼,噹噹前線程正在執 行不消耗CPU的代碼時,這時候總有CPU是空閒的,如果此時讓CPU忙起來,可 以帶來整體性能上的提升,所在在這種場景下,將耗時操作的代碼放在同步塊 中,肯定是可以提高整個性能的。

所以,不管如何,縮小同步範圍只會帶來好處,那我們上面的代碼優化如下:

void fun1() {
  synchronized(lock){
    
      //1 正在訪問共享資源 ... ...
  }
      //2 做其它耗時操作,但這些耗時操作與共享資源無關... ...

}

sleep的濫用

sleep只適合用在等待固定時長的場合,如果輪詢代碼中夾雜着sleep()調用,這 種設計必然是一種糟糕的設計。

這種設計在某些場合下會導致嚴重的性能瓶頸,如果是用戶交互的系統,那麼用戶會必然會直接感覺系統變慢。

如果是後臺消息處理系統,那麼必然消息處 理會很慢。這種設計肯定可以使用notify()和wait()來完成同樣的功能

String +的濫用

String c = new String(“abc”) + new String(“efg”) + new String(“12345”);

每一次+操作都會產生一個臨時對象,並伴隨着數據拷貝,這個對性能是一個極大的消耗。這 個寫法常常成爲系統的瓶頸,如果這個地方恰好是一個性能瓶頸,修改成StringBuffer之後,性 能會有大幅的提升.

不恰當的線程模型

在多線程場合下, 如果線程模型不恰當, 也會使性能低下。 如在網 絡IO的場合,我們一定要使用消息發送隊列和消息接收隊列來進行異步IO. 這種修改之後, 性能可能會有幾十倍的上升。

線程數量不足

在使用線程池的場合,如果線程池的線程配置太少,也會導致性能低下

內存泄漏導致的頻繁GC

內存泄漏會導致GC越來越頻繁,而GC操作是CPU密集型操作,頻 繁GC會導致系統整體性能嚴重下降,這也是我們會經常遇到的問題。

四、分析的手段和工具

上面提到的所有這些原因形成的性能瓶頸,都可以通過線程堆棧分析,找到根本的原因,論ThreadDump的重要性,注意ThreadDump比較適合多線程場景下的問題分析。

怎麼模擬發現瓶頸

性能瓶頸的幾個特徵:

  1. 當前的性能瓶頸只有一處,只有當解決的這一處,才知道下一處。沒有解決當前的性能瓶 頸,下一處性能瓶頸是不會出現的。在公路上,最窄的一處決定了該道路的通車能力。只有拓寬了最窄的地方,整個的交通的通車能力才能上去,而如果直接拓寬次窄(即第二 窄)的路段,整個路段的通車能力不會有任何的提升,如圖:

最差那一段代表了整體性能

  1. 性能瓶頸是動態的,低負載下不是瓶頸的地方,在高負載下可能成爲瓶頸。在高壓力下才 能出現的瓶頸,由於JProfiler等性能剖析工具依附在JVM上帶來的開銷,使系統根本就無 法達到該瓶頸出現時需要的性能。因此這種類型的性能瓶頸在JProfiler 或者OptimizeIt等 性能剖析工具下壓根無法出現,也就無法找到這個性能瓶頸。在這種場合下,進行線程堆 棧分析纔是一個真正有效的辦法。

鑑於性能瓶頸的以上特點,進行性能模擬的時候,一定要使用比系統當前稍高的壓力下 進行模擬,否則性能瓶頸不會現形。具體的步驟如下:
調優過程

如何通過線程堆棧找到性能瓶頸?

一般一個系統一旦出現性能瓶頸,從堆棧 上分析,有如下三種最爲典型的堆棧特徵:

  1. 絕大多數線程的堆棧都表現爲在同一個調用上下文上,且只剩下非常少的空閒線程。可能的原因如下:

    (a) 線程的數量過少

    (b) 鎖的粒度過大導致的鎖競爭

    © 資源競爭(如數據庫連接池中連接不足,導致有些獲取連接的線程被阻塞)

    (d) 鎖範圍內有大量耗時操作(如大量的磁盤IO),導致鎖爭用。

    (e) 遠程通信的對方處理緩慢(dubbo 提供者變慢),如數據庫側的SQL代碼性能低下。

  2. 絕大多數線程處於等待狀態,只有幾個工作的線程,總體性能上不去。可能的原因是,系統存在關鍵路徑,在該關鍵路徑上沒有足夠的能力給下個階段輸送大量的任務,導致其它地方空閒。如在消息分發系統,消息分發一般是一個線程,而消息處理是多個線程,這 時候消息分發是瓶頸的話,那麼從線程堆棧就會觀察到上面提到的現象:即該關鍵路 沒有足夠的能力給下個階段輸送大量的任務,導致其它地方空閒。

  3. 線程總的數量很少。導致性能瓶頸的原因與上面的類似。這裏線程很少,是由於某些線程池實現使用另一種設計思路,當任務來了之後才new出線程來,這種實現方式下,線程的數量上不去,就意味有在某處關鍵路徑上沒有足夠的能力給下個階段輸送大量的任務, 從而不需要更多的線程來處理。

下面是一個出現了性能瓶頸的堆棧的例子:

"Thread-243" prio=1 tid=0xa58f2048 nid=0x7ac2 runnable [0xaeedb000..0xaeedc480]
at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.read(SocketInputStream.java:129) at oracle.net.ns.Packet.receive(Unknown Source) ... ...
at oracle.jdbc.driver.LongRawAccessor.getBytes() at oracle.jdbc.driver.OracleResultSetImpl.getBytes()
- locked <0x9350b0d8> (a oracle.jdbc.driver.OracleResultSetImpl) at oracle.jdbc.driver.OracleResultSet.getBytes(O) ... ...
at org.hibernate.loader.hql.QueryLoader.list() at org.hibernate.hql.ast.QueryTranslatorImpl.list() ... ...
at com.wes.NodeTimerOut.execute(NodeTimerOut.java:175) at com.wes.timer.TimerTaskImpl.executeAll(TimerTaskImpl.java:707) at com.wes.timer.TimerTaskImpl.execute(TimerTaskImpl.java:627)
- locked <0x80df8ce8> (a com.wes.timer.TimerTaskImpl) at com.wes.threadpool.RunnableWrapper.run(RunnableWrapper.java:209) at com.wes.threadpool.PooledExecutorEx$Worker.run() at java.lang.Thread.run(Thread.java:595)

  
"Thread-248" prio=1 tid=0xa58f2048 nid=0x7ac2 runnable [0xaeedb000..0xaeedc480]
at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.read(SocketInputStream.java:129) at oracle.net.ns.Packet.receive(Unknown Source) ... ...
at oracle.jdbc.driver.LongRawAccessor.getBytes() at oracle.jdbc.driver.OracleResultSetImpl.getBytes() - locked <0x9350b0d8> (a oracle.jdbc.driver.OracleResultSetImpl) 2
at oracle.jdbc.driver.OracleResultSet.getBytes(O) ... ...
at org.hibernate.loader.hql.QueryLoader.list() at org.hibernate.hql.ast.QueryTranslatorImpl.list() ... ...
a com.wes.NodeTimerOut.execute(NodeTimerOut.java:175) at com.wes.timer.TimerTaskImpl.executeAll(TimerTaskImpl.java:707) at com.wes.timer.TimerTaskImpl.execute(TimerTaskImpl.java:627) - locked <0x80df8ce8> (a com.wes.timer.TimerTaskImpl) at com.wes.threadpool.RunnableWrapper.run(RunnableWrapper.java:209) at com.wes.threadpool.PooledExecutorEx$Worker.run() at java.lang.Thread.run(Thread.java:595)
... ...

  
  
"Thread-238" prio=1 tid=0xa4a84a58 nid=0x7abd in Object.wait() [0xaec56000..0xaec57700]
at java.lang.Object.wait(Native Method) at com.wes.collection.SimpleLinkedList.poll(SimpleLinkedList.java:104)
- locked <0x6ae67be0> (a com.wes.collection.SimpleLinkedList) at com.wes.XADataSourceImpl.getConnection_internal(XADataSourceImpl.java:1642) ... ...
at org.hibernate.impl.SessionImpl.list() at org.hibernate.impl.SessionImpl.find() at com.wes.DBSessionMediatorImpl.find() at com.wes.ResourceDBInteractorImpl.getCallBackObj() at com.wes.NodeTimerOut.execute(NodeTimerOut.java:152) at com.wes.timer.TimerTaskImpl.executeAll() at com.wes.timer.TimerTaskImpl.execute(TimerTaskImpl.java:627)
- locked <0x80e08c00> (a com.facilities.timer.TimerTaskImpl) at com.wes.threadpool.RunnableWrapper.run(RunnableWrapper.java:209) at com.wes.threadpool.PooledExecutorEx$Worker.run() at java.lang.Thread.run(Thread.java:595)

  
  
"Thread-233" prio=1 tid=0xa4a84a58 nid=0x7abd in Object.wait() [0xaec56000..0xaec57700]
at java.lang.Object.wait(Native Method) 
at com.wes.collection.SimpleLinkedList.poll(SimpleLinkedList.java:104) - locked <0x6ae67be0> (a com.wes.collection.SimpleLinkedList) at com.wes.XADataSourceImpl.getConnection_internal(XADataSourceImpl.java:1642) ... ...
at org.hibernate.impl.SessionImpl.list() at org.hibernate.impl.SessionImpl.find() at com.wes.DBSessionMediatorImpl.find() 48
at com.wes.ResourceDBInteractorImpl.getCallBackObj() at com.wes.NodeTimerOut.execute(NodeTimerOut.java:152) at com.wes.timer.TimerTaskImpl.executeAll() at com.wes.timer.TimerTaskImpl.execute(TimerTaskImpl.java:627) - locked <0x80e08c00> (a com.facilities.timer.TimerTaskImpl) at com.wes.threadpool.RunnableWrapper.run(RunnableWrapper.java:209) at com.wes.threadpool.PooledExecutorEx$Worker.run() at java.lang.Thread.run(Thread.java:595) ... ...

從堆棧中看,其中有N多個是JDBC數據庫訪問佔用的。這說明有可能把鏈接已經耗盡,其它所有http請求由於獲取不到鏈接,而被阻塞在java.lang.Object.wait()方法 上. 從這個堆棧中看,性能瓶頸出現在數據庫訪問上,數據庫訪問耗盡了所有的連接。找到瓶 頸後,下一步結合源代碼分析,具體是什麼原因導致了數據庫的訪問需要過長的時間? 沒有創建索引,還是使用了效率過低的SQL語句?

性能調優的終結條件

性能調優的過程總是有一個止點,那麼滿足什麼條件,就說明已經沒有優化的空間?總結下就有如下倆個:

  1. 算法足夠優化,代碼的優化已到極致了
  2. 線程充分的使用了cpu

如果達到上面的條件,性能仍然無法滿足應用的要求,只能通過考慮購買更好的機器,或 者集羣來實現更大的容量支持

性能調優工具

耳熟能詳的幾個 JProfiler VisualVM和JDK自帶的一些工具。但這些分析工具一旦掛到系統上之後,會導致整體性能的大幅下降,在多線程場合下,由於整體的壓力無法上去,導致性能瓶頸根本就不會出現,因此這種場合下進行性能 分析,這些工具基本上是沒有幫助的。這些性能剖析工具比較適合於單線程下的代碼段分析, 找到比較耗時的算法和代碼,但對於多線程場合下鎖使用不當的分析,往往無能爲力。

角兒

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章