現網問題排查手冊

現網問題排查手冊

1. 服務CPU佔用高

​ CPU 使用率是衡量系統繁忙程度的重要指標,一般情況下單純的 CPU 高並沒有問題,它代表系統正在不斷的處理我們的任務,但是如果 CPU 過高,導致任務處理不過來,從而引起 load 高,這個是非常危險需要關注的。 CPU 使用率的安全值沒有一個標準值,取決於你的系統是計算密集型還是 IO 密集型,一般計算密集型應用 CPU 使用率偏高 load 偏低,IO 密集型相反。

當出現了服務CPU使用率高,並且任務處理不過來時,我們需要快速定位服務具體在處理什麼任務導致CPU使用率很高,具體的操作步驟如下:

  • ps -ef | grep 服務名: 找到 Java 進程 id

  • top -Hp pid: 找到使用 CPU 最高的線程

  • printf '0x%x\n' tid: 線程 id 轉化 16 進制

  • jstack pid | grep tid 找到線程堆棧

這裏我們以網關爲作爲事例舉例:

  • 查找網關的進程id,執行: ps -ef|grep GatewayService

    其中13905爲網關的進程id。

  • 找到使用 CPU 最高的線程,執行:top -Hp 13905

    由於只是演示,這裏的CPU使用率並不高,實際場景可能是90以上。上圖昨天我們能看到PID列,就是線程的Id,我們拿第一個13935爲例。

  • 將線程號轉換爲16進制:

    轉換成16進制後的線程號爲0x374d

  • 查詢線程的線程棧:

    我們可以看到線程名稱爲RxComputationScheduler-1,通過這個名稱我們可以推測出是哪個邏輯導致的CPU佔用高,假如不知道這個線程的具體邏輯,可以把完整的線程堆棧打印出來,一般爲代碼業務邏輯線程,或者是GC線程導致。

    完整的線程棧信息通過jstack -l pid,一般保存到文件,由運維發送給開發查看,此處我們可以看到上述線程的線程棧:

2. 服務GC頻繁/內存泄漏

​ 一般情況下,服務GC頻繁,也可以通過經驗1中,服務CPU佔用高排查得出,由於GC頻繁已經影響到服務的正常使用,此時GC線程的CPU佔用率一般都會很高,所以通過方式1,一般可以定位到CPU佔用高的線程爲如下所示:

除了上述方法還可以通過如下方式定位GC是否頻繁: 使用jstat命令查看GC統計情況:jstat -gcutil pid

各個字段的說明如下:

  • **S0:**倖存1區當前使用比例
  • **S1:**倖存2區當前使用比例
  • **E:**伊甸園區使用比例
  • **O:**老年代使用比例
  • **M:**元數據區使用比例
  • **CCS:**壓縮使用比例
  • **YGC:**年輕代垃圾回收次數
  • **FGC:**老年代垃圾回收次數
  • **FGCT:**老年代垃圾回收消耗時間
  • **GCT:**垃圾回收消耗總時間

這裏我們主要關注FGC相關的信息,一般情況下web應用FGC都應該爲0,這個數值比較大的話,需要重點關注。

FGC頻繁,一般由兩種原因導致:

  1. 內存分配不足,這種情況需要調整服務的內存大小
  2. 服務存在內存泄漏,或代碼邏輯問題緩存過多數據在內存中

我們需要通過dump出堆信息來分析具體佔用過多的對象是什麼,這時命令爲:

jmap -dump:format=b,file=heap.bin pid 此命令是把指定pid服務的堆導出來,這是一個二進制的文件,需要使用專門的工具Eclipse Memory Analyzer。工具的詳細使用這裏不做過多贅述,有需要的可以到網上搜一下,通過內存泄漏檢查,可以看到如下信息:

上圖可以看到有三個嫌疑問題,這裏我們隨機截取一個:

從上面的截圖可以看到,線程http-nio-8601-exec-100佔用了整個堆內存的48.41%,大小爲2個G左右byte[],這顯然是不正常的。我們點擊See stacktrace,可以看到當前線程的調用棧信息(截圖中爲一部分棧信息):

根據線程棧信息可以看到該線程正在進行數據流的讀操作,並且此線程是tomcat的io線程,可以推測是在處理上傳/下載請求。帶着這些信息針對性的走查代碼,查找問題的根源。

3. 服務請求無響應

​ 服務請求無響應,首先查看服務日誌,有沒有什麼異常日誌,但是一般遇到的情況都是線程卡死導致,線程卡死服務是沒有日誌的。導致的原因是服務在運行的過程中,某些異常場景導致資源未釋放,從而導致tomcat的io線程一直被佔用,新的請求進來時,沒有可用的工作線程處理,導致服務假死的情況發生,這種情況一般可以通過重啓服務,立刻就可以恢復服務,但是過一段時間肯定還會復現,所以找到最終原因才能保證後續不會發生類似問題。我們第一步要做的是打印服務的線程棧信息:jstack -l pid > dump.log,我們可以分析所有的tomcat io線程的線程棧,看一下線程都卡在哪個地方。下圖線程棧是網關上次故障線程卡死的信息:

從線程棧可以看到Tomcat的IO線程全部卡在了HttpClientleaseConnection()這裏,也就是所有的線程都在等待獲取HttpClient的連接,但是始終獲取不到。分析Zuul網關HttpClient的源碼得知,是在異常場景,連接沒有釋放到HttpClient的連接池導致。

4. 網絡連接數/線程數飆升

​ 一般一個服務的網絡連接/線程數飆升是由於當前服務或當前服務的上游服務有問題導致,經驗3服務請求無響應也會導致線程數飆升,一直達到配置的最大值,網絡連接會隨着線程數的增加而增加,一直達到配置的最大值,當前使用的SpringBoot版本默認是10000個連接。所以排查思路類似於經驗3,但是引起這個問題的原因不一定是當前服務有問題,有可能是上游服務異常導致,所以當查尋當前服務線程棧無異常時,需要同步查一下上游服務的線程棧。如何快速確定是哪個服務有問題呢?我們可以通過Grafana,看下接口性能,假如問題出現在網關和賬戶兩個服務,我們可以先看一下賬戶服務的接口響應是否異常,假如賬戶服務的接口響應都很快,那麼基本可以斷定是網關出了問題。

5. 一次請求調用,代碼邏輯走了一半沒了蹤跡,後續日誌也沒打

這種問題引起的原因有多種,下面列舉一下可能引起的原因:

  • **原因一:**日誌打印有問題,如包或類庫引用錯誤導致代碼運行了但是日誌沒有正常打印

  • **原因二:**代碼邏輯缺陷

  • **原因三:**系統異常如爆內存導致線程異常退出

  • **原因四:**存在三方調用,但是調用未設置超時時間

問題出現時,前三條原因都可以很容易的確認,原因一和原因二一般可以通過分析代碼邏輯確定,原因三,也可以通過日誌中搜索OutOfMemory關鍵字來確定是否發生了內存溢出的異常。原因四排查相對較爲繁瑣,下面具體講一下原因四的排查流程:

找到當次調用方法入口的任何一條日誌,提取線程名稱,假如線程名爲:http-nio-8659-exec-9,線程名拿到以後,在kibana上,指定服務、線程名、機器搜索關鍵詞爲線程名,看一下異常發生後,此線程是否還在處理其他請求,結果搜索的結果如下圖,發現從4月29號開始,就沒有線程http-nio-8659-exec-9的蹤跡了。

這是有點可疑的,但是仍然不能確定,假如這個線程因爲空閒被回收了呢?所以此時我們導出服務的線程棧,查看是否還有該線程,結果如下(這個線程棧導出日期是在5月9號):

從線程棧中發現這個線程還在進行流的讀操作,到5月9號已經讀了10天了,還在讀。最終發現是HttpClient調用三方服務未設置超時時間導致,線程一直卡死,不重啓服務這個線程將永遠卡在那。

6. 服務CLOSE_WAIT連接數過多排查思路

​ CLOSE_WAIT狀態出現在被動關閉方,收到關閉信號後調用close方法前。是一種「等待關閉」的狀態。如下圖所示。主動關閉的一方發出 FIN 包,被動關閉的一方響應 ACK 包,此時,被動關閉的一方就進入了 CLOSE_WAIT 狀態。

​ 通常出現CLOSE_WAIT分爲幾種可能:

  • 程序問題:如果代碼層面忘記了 close 相應的 socket 連接,那麼自然不會發出 FIN 包,從而導致 CLOSE_WAIT 累積;或者代碼不嚴謹,出現死循環之類的問題,導致即便後面寫了 close 也永遠執行不到。
  • 響應太慢或者超時設置過小:如果連接雙方不和諧,一方不耐煩直接 timeout,另一方卻還在忙於耗時邏輯,就會導致 close 被延後。響應太慢是首要問題,不過換個角度看,也可能是 timeout 設置過小。

通常排查路徑爲

1、netstat -alpn | grep {port}查詢連接狀態,是否存在過多CLOSE_WAIT

2、根據CLOSE_WAIT的Address地址,推測問題出現方

3、jstack -l pid > dump.log 查詢異常

4、dump.log一般主要排查三種狀態 1、BLOCKED 2、WAITING 3、RUNNABLE

5、BLOCKED表示一個阻塞線程在等待monitor鎖,可能由以下幾種原因引起

​ 1>線程通過調用sleep方法進入睡眠狀態; 2>線程調用一個在I/O上被阻塞的操作,即該操作在輸入輸出操作完成之前不會返回到它的調用者; ​ 3>線程試圖得到一個鎖,而該鎖正被其他線程持有; ​ 4>線程在等待某個觸發條件;

6、WAITING表示一個線程在等待另一個線程執行一個動作時在這個狀態,可能由以下幾種原因引起

​ 1>Object#wait()而且不加超時參數

​ 2>Thread#join() 而且不加超時參數

​ 3>LockSupport#park()

7、 RUNNABLE表示線程獲得了cpu 時間片(timeslice)

8、先後深入代碼邏輯查詢該三種狀態是否存在問題

以ZuulGateway爲例

​ 某次生產事故中,ZuulGateway請求無法處理,導致應用不正常。查看進程,Zuul進程仍在,且仍會打印Eureka更新的日誌,但請求的日誌不再打印了。查詢連接數後發現存在很多CLOSE_WAIT,同時根據堆棧信息判斷出問題出現在服務轉發的連接上。查詢源碼邏輯後發現,如果在轉發的過程中,代碼拋出異常,則httpClient不會主動將連接釋放或關閉,導致httpClient的連接被佔用。一旦連接被佔用完,則httpClient不再能夠向下遊發送請求。然而,此時用戶仍能夠向Zuul發送請求。請求數逐漸增加,最終,用戶最大請求數也被佔滿,Zuul服務僵死,無法再處理任何用戶請求。

最終解決方法

增加ReleaseConnOnErrorFilter,用於異常時的連接關閉。

7. 線程死鎖問題排查思路

​ 死鎖是指兩個或兩個以上的進程在執行過程中,由於競爭資源或者由於彼此通信而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱爲死鎖進程。

  • jstack -l {pid} 首先找到線程死鎖位置

  • 深入代碼邏輯,找到死鎖產生原因

下例爲L1(Spring WebSocket)的一次線程死鎖經歷

  • 根據jstack線程棧後發現

  • 找到代碼位置

  • 分析原因後得知:

    代碼中消息發送(需要獲取鎖)失敗會觸發關閉session,同時觸發消息發送(需要獲取session鎖)

    由於ConcurrentHashMap無序,[A] -> [A、B] 或 [A] -> [B、A]

    [A] -> [A、B]

    線程1獲取sessionA的鎖,發送消息失敗後關閉session觸發消息發送,A成功,獲取sessionB鎖

    線程2獲取sessionA的鎖,等待直到線程1釋放

    死鎖產生

    [A] -> [B、A]

    線程1獲取sessionA的鎖,發送消息失敗後關閉session觸發消息發送,獲取sessionB鎖時失敗

    線程2獲取sessionB的鎖,發送消息失敗後關閉session觸發消息發送,sessionB成功獲取,獲取sessionA鎖時失敗

    死鎖產生

解決方案:

  1. Map<Long, ConcurrentHashMap<Long, MeetingInfo>> MEETING_INFO_MAP的values改爲有序的容器,如ConcurrentLinkedQueue。
  2. 將processEndSession發送消息異步處理,同一場會議使用同一個線程處理。
  3. 全部使用同一個線程處理

8. 鎖重入導致的併發修改異常

9. 一次請求,Tomcat和Nginx都沒有accessLog就是客戶端網絡問題嗎?

10.數據庫死鎖問題排查

11. mysql長事務問題排查

  • mysql中查詢連接的客戶端端口: show PROCESSLIST; Host:該進程程序連接mysql的ip:port
  • 服務器上查詢該連接端口的進程:netstat -anp | grep 36884 查詢到該進程
  • ps -ef | grep 查詢該進程詳情屬於哪個服務
  • jstack -l pid > dump.log 查詢異常

以管理系統問題爲例

DBA告警,查詢到備庫有一些長事務

運維根據端口號查詢到服務屬於哪個數據庫

jstack後發現99個BLOCKED,並且都是數據庫查詢相關,都在等待獲取數據庫連接

同時發現某條SQL在執行時查詢不到結果

KILL該進程,臨時性關掉該查詢頁面

對SQL進行優化

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