Dubbo踩坑記:CPU突然飆升到300%,Dubbo活動線程數直接飆到1000

轉載:https://zhouwei.blog.csdn.net/article/details/127555819?spm=1001.2101.3001.6650.15&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-15-127555819-blog-115986471.235%5Ev27%5Epc_relevant_recovery_v2&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-15-127555819-blog-115986471.235%5Ev27%5Epc_relevant_recovery_v2&utm_relevant_index=16

背景:

新功能開發測試完成後,準備發佈上線,當發佈完第三臺機器時,監控顯示其中一臺機器CPU突然飆升到300%,Dubbo活動線程數直接飆到1000+,不得不停止發佈,立馬回滾出問題的機器

回滾之後恢復正常,繼續觀察另外兩臺已經發布的機器,最終,無一倖免,只能全部回滾了。


定位問題:

監控日誌分析

首先查看故障時間點的應用日誌,發現大量方法耗時較久,其中filterMission方法尤爲顯著,耗時長達30S+。

說明下,filterMission是當前服務中QPS較高的接口(日均調用量2個億),所以導致故障的可能性也較高。

於是重新review了一遍filterMission的實現,其中並無複雜的計算邏輯,沒有大量數據的處理邏輯,也不存在鎖的爭用,本次需求更是不涉及filterMission的改造,排除filterMission導致故障發生。

從日誌中也可以發現,大量請求發生超時,這些都只能說明系統負載過重,並不能定位問題的癥結所在。

Code Review
從應用日誌找不到原因所在,只能再做一次code review了。

首先檢查系統中是否存在同步代碼邏輯的使用,主要是爲了排除發生死鎖的可能;檢查具有複雜運算邏輯的代碼;同時,將本次修改的代碼和上一版本進行比對,也沒找出什麼問題。(事實證明,Review不夠仔細)

線程Dump分析
到此,從日誌中暫時也分析不出問題,盲目看代碼也無法具體定位問題了,現在只能重新發布一臺機器,出現問題時讓運維將應用程序的線程堆棧dump出來,分析jstack文件。開始分析dump文件前,先鞏固下基礎吧。

線程狀態

圖中各狀態說明:

New: 新建狀態,當線程對象創建時存在的狀態;

Runnable:ready-to-run,可運行狀態,調用thread.start()後,線程變成爲Runnable狀態,還需要獲得CPU才能運行;

Running:正在運行,當調用Thread.yield()或執行完時間片,CPU會重新調度;注意:Thread.yield()調用之後,線程會釋放CPU,但是CPU重新調度可能讓線程重新獲得時間片。

Waiting:調用thread.join()、object.wait()和LockSupport.park()線程都會進入該狀態,表明線程正處於等待某個資源或條件發生來喚醒自己;

thread.join()、object.wait()需要Object的notify()/notifyAll()或發生中斷來喚醒,LockSupport.park()需要LockSupport.unpark()來喚醒,這些方法使線程進入Runnable狀態,參與CPU調度。

thread.join():作用是等待線程thread終止,只有等thread執行完成後,主線程纔會繼續向下執行;

從join()實現可知,主線程調用thread.join()之後,只有thread.isAlive()返回true,纔會調用object.wait()使主線程進入等待狀態

也就是說,thread.start()未被調用,或thread已經結束,object.wait()都不會被調用。也就是說,必須先啓動線程thread,調用thread.join()纔會生效;

若主線程在waiting狀態被喚醒,會再次判斷thread.isAlive(),若爲true,繼續調用object.wait()使進入waiting狀態,直到thread終止,thread.isAlive()返回false。

object.wait():作用是使線程進入等待狀態,只有線程持有對象上的鎖,才能調用該對象的wait(),線程進入等待狀態後會釋放其持有的該對象上的鎖,但會仍然持有其它對象的鎖。

若其他線程想要調用notify()、notifyAll()喚醒該線程,也需要持有該對象的鎖。

LockSupport.park():掛起當前線程,不參與線程調度,除非調用LockSupport.unpark()重新參與調度。

Timed_Waiting:調用Thread.sleep(long)、LockSupport.parkNanos(long)、thread.join(long)或obj.wait(long)等都會使線程進入該狀態,與Waiting的區別在於Timed_Waiting的等待有時間限制;

Thread.sleep():讓當前線程停止運行指定的毫秒數,該線程不會釋放其持有的鎖等資源。

Blocked:指線程正在等待獲取鎖,當線程進入synchronized保護的代碼塊或方法時,沒有獲取到鎖,則會進入該狀態;或者線程正在等待I/O,也會進入該狀態。注意,java中Lock對象不會使線程進入該狀態。

Dead:線程執行完畢,或者拋出了未捕獲的異常之後,會進入dead狀態,表示該線程結束。

上圖中某些狀態只是爲了方便說明,實際並不存在,如running/sleeping,java中明確定義的線程狀態值有如下幾個:

NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED
  • 1

分析jstack日誌
大量dubbo線程處於WAITING狀態,看日誌:

"DubboServerHandler-172.24.16.78:33180-thread-1220" #1700 daemon prio=5 os_prio=0 tid=0x00007f3394988800 nid=0x4aae waiting on condition [0x00007f32d75c0000]
   java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x00000000866090c0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
    at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1081)
    at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:809)
    at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)

由日誌可知道,線程“DubboServerHandler-172.24.16.78:33180-thread-1220”處於WAITING狀態

主要原因是線程從線程池隊列中取任務來執行,但線程池爲空,最終調用了LockSupport.park使線程進入等待狀態,需要等待隊列非空的通知。

設想一下,什麼時候會新創建新線程來處理請求?結合jdk線程池實現可知,當新請求到來時,若池中線程數量未達到corePoolSize,線程池就會直接新建線程來處理請求。

根據jstack日誌,有195個dubbo線程從ScheduledThreadPoolExecutor中取任務導致處於WAITING狀態,按理這些dubbo線程只負責處理客戶端請求,不會處理調度任務,爲什麼會去調度任務線程中取任務呢?這裏暫時拋出這個問題吧,我也不知道答案,希望有大神幫忙解答。

還有另外一部分WAITING狀態的線程,看日誌:

"DubboServerHandler-172.24.16.78:33180-thread-1489" #1636 daemon prio=5 os_prio=0 tid=0x00007f33b0122800 nid=0x48ec waiting on condition [0x00007f32db600000]
   java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x0000000089d717a8> (a java.util.concurrent.SynchronousQueue$TransferStack)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
    at java.util.concurrent.SynchronousQueue$TransferStack.awaitFulfill(SynchronousQueue.java:458)
    at java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:362)
    at java.util.concurrent.SynchronousQueue.take(SynchronousQueue.java:924)
    at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)

這部分dubbo線程主要是因爲從ThreadPoolExecutor取任務來執行時被掛起(309個線程),這些線程正常處理完第一個請求後,就會回到線程池等待新的請求。

由於這裏使用newFixedThreadPool作爲dubbo請求處理池,因此每個新請求默認都會創建新線程來處理,除非達到池的限定值。只有達到線程池最大線程數量,新的請求來臨纔會被加入任務隊列,哪些阻塞在getTask()的線程纔會得到複用。

此外,還有大量dubbo線程處於BLOCKED狀態,看日誌:

"DubboServerHandler-172.24.16.78:33180-thread-236" #1727 daemon prio=5 os_prio=0 tid=0x00007f336403b000 nid=0x4b8b waiting for monitor entry [0x00007f32d58a4000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at org.apache.logging.log4j.core.appender.rolling.RollingFileManager.checkRollover(RollingFileManager.java:149)
    - waiting to lock <0x0000000085057998> (a org.apache.logging.log4j.core.appender.rolling.RollingRandomAccessFileManager)
    at org.apache.logging.log4j.core.appender.RollingRandomAccessFileAppender.append(RollingRandomAccessFileAppender.java:88)
    at org.apache.logging.log4j.core.config.AppenderControl.tryCallAppender(AppenderControl.java:155)
    at org.apache.logging.log4j.core.config.AppenderControl.callAppender0(AppenderControl.java:128)
    at org.apache.logging.log4j.core.config.AppenderControl.callAppenderPreventRecursion(AppenderControl.java:119)
    at org.apache.logging.log4j.core.config.AppenderControl.callAppender(AppenderControl.java:84)
    at org.apache.logging.log4j.core.config.LoggerConfig.callAppenders(LoggerConfig.java:390)
    at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:375)
    at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:359)
    at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:349)
    at org.apache.logging.log4j.core.config.AwaitCompletionReliabilityStrategy.log(AwaitCompletionReliabilityStrategy.java:63)
    at org.apache.logging.log4j.core.Logger.logMessage(Logger.java:146)
    at org.apache.logging.log4j.spi.AbstractLogger.logMessage(AbstractLogger.java:
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章