現網問題排查手冊
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頻繁,一般由兩種原因導致:
- 內存分配不足,這種情況需要調整服務的內存大小
- 服務存在內存泄漏,或代碼邏輯問題緩存過多數據在內存中
我們需要通過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線程全部卡在了HttpClient
的leaseConnection()
這裏,也就是所有的線程都在等待獲取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鎖時失敗
死鎖產生
解決方案:
- 將
Map<Long, ConcurrentHashMap<Long, MeetingInfo>> MEETING_INFO_MAP
的values改爲有序的容器,如ConcurrentLinkedQueue。 - 將processEndSession發送消息異步處理,同一場會議使用同一個線程處理。
- 全部使用同一個線程處理
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進行優化