一次生產(chǎn) CPU 100% 排查優(yōu)化實(shí)踐
前言
到了年底果然都不太平,最近又收到了運(yùn)維報(bào)警:表示有些服務(wù)器負(fù)載非常高,讓我們定位問(wèn)題。
還真是想什么來(lái)什么,前些天還故意把某些服務(wù)器的負(fù)載提高(沒(méi)錯(cuò),老板讓我寫(xiě)個(gè) BUG!),不過(guò)還好是不同的環(huán)境互相沒(méi)有影響。
定位問(wèn)題
拿到問(wèn)題后首先去服務(wù)器上看了看,發(fā)現(xiàn)運(yùn)行的只有我們的 Java 應(yīng)用。于是先用 ps 命令拿到了應(yīng)用的 PID。
接著使用 ps-Hppid 將這個(gè)進(jìn)程的線程顯示出來(lái)。輸入大寫(xiě)的 P 可以將線程按照 CPU 使用比例排序,于是得到以下結(jié)果。
果然某些線程的 CPU 使用率非常高。
為了方便定位問(wèn)題我立馬使用 jstack pid>pid.log 將線程棧 dump 到日志文件中。
我在上面 100% 的線程中隨機(jī)選了一個(gè) pid=194283 轉(zhuǎn)換為 16 進(jìn)制(2f6eb)后在線程快照中查詢:
因?yàn)榫€程快照中線程 ID 都是16進(jìn)制存放。
發(fā)現(xiàn)這是 Disruptor 的一個(gè)堆棧,前段時(shí)間正好解決過(guò)一個(gè)由于 Disruptor 隊(duì)列引起的一次 OOM:強(qiáng)如 Disruptor 也發(fā)生內(nèi)存溢出?
沒(méi)想到又來(lái)一出。
為了更加直觀的查看線程的狀態(tài)信息,我將快照信息上傳到專門分析的平臺(tái)上。
http://fastthread.io/
其中有一項(xiàng)菜單展示了所有消耗 CPU 的線程,我仔細(xì)看了下發(fā)現(xiàn)幾乎都是和上面的堆棧一樣。
也就是說(shuō)都是 Disruptor 隊(duì)列的堆棧,同時(shí)都在執(zhí)行 java.lang.Thread.yield 函數(shù)。
眾所周知 yield 函數(shù)會(huì)讓當(dāng)前線程讓出 CPU 資源,再讓其他線程來(lái)競(jìng)爭(zhēng)。
根據(jù)剛才的線程快照發(fā)現(xiàn)處于 RUNNABLE 狀態(tài)并且都在執(zhí)行 yield 函數(shù)的線程大概有 30幾個(gè)。
因此初步判斷為大量線程執(zhí)行 yield 函數(shù)之后互相競(jìng)爭(zhēng)導(dǎo)致 CPU 使用率增高,而通過(guò)對(duì)堆棧發(fā)現(xiàn)是和使用 Disruptor 有關(guān)。
解決問(wèn)題
而后我查看了代碼,發(fā)現(xiàn)是根據(jù)每一個(gè)業(yè)務(wù)場(chǎng)景在內(nèi)部都會(huì)使用 2 個(gè) Disruptor 隊(duì)列來(lái)解耦。
假設(shè)現(xiàn)在有 7 個(gè)業(yè)務(wù)類型,那就等于是創(chuàng)建 2*7=14 個(gè) Disruptor 隊(duì)列,同時(shí)每個(gè)隊(duì)列有一個(gè)消費(fèi)者,也就是總共有 14 個(gè)消費(fèi)者(生產(chǎn)環(huán)境更多)。
同時(shí)發(fā)現(xiàn)配置的消費(fèi)等待策略為 YieldingWaitStrategy 這種等待策略確實(shí)會(huì)執(zhí)行 yield 來(lái)讓出 CPU。
代碼如下:
初步看來(lái)和這個(gè)等待策略有很大的關(guān)系。
本地模擬
為了驗(yàn)證,我在本地創(chuàng)建了 15 個(gè) Disruptor 隊(duì)列同時(shí)結(jié)合監(jiān)控觀察 CPU 的使用情況。
創(chuàng)建了 15 個(gè) Disruptor 隊(duì)列,同時(shí)每個(gè)隊(duì)列都用線程池來(lái)往 Disruptor隊(duì)列 里面發(fā)送 100W 條數(shù)據(jù)。
消費(fèi)程序僅僅只是打印一下。
跑了一段時(shí)間發(fā)現(xiàn) CPU 使用率確實(shí)很高。
同時(shí) dump 線程發(fā)現(xiàn)和生產(chǎn)的現(xiàn)象也是一致的:消費(fèi)線程都處于 RUNNABLE 狀態(tài),同時(shí)都在執(zhí)行 yield。
通過(guò)查詢 Disruptor 官方文檔發(fā)現(xiàn):
YieldingWaitStrategy 是一種充分壓榨 CPU 的策略,使用 自旋+yield的方式來(lái)提高性能。 當(dāng)消費(fèi)線程(Event Handler threads)的數(shù)量小于 CPU 核心數(shù)時(shí)推薦使用該策略。
同時(shí)查閱到其他的等待策略 BlockingWaitStrategy (也是默認(rèn)的策略),它使用的是鎖的機(jī)制,對(duì) CPU 的使用率不高。
于是在和之前同樣的條件下將等待策略換為 BlockingWaitStrategy。
和剛才的 CPU 對(duì)比會(huì)發(fā)現(xiàn)到后面使用率的會(huì)有明顯的降低;同時(shí) dump 線程后會(huì)發(fā)現(xiàn)大部分線程都處于 waiting 狀態(tài)。
優(yōu)化解決
看樣子將等待策略換為 BlockingWaitStrategy 可以減緩 CPU 的使用,
但留意到官方對(duì) YieldingWaitStrategy 的描述里談道: 當(dāng)消費(fèi)線程(Event Handler threads)的數(shù)量小于 CPU 核心數(shù)時(shí)推薦使用該策略。
而現(xiàn)有的使用場(chǎng)景很明顯消費(fèi)線程數(shù)已經(jīng)大大的超過(guò)了核心 CPU 數(shù)了,因?yàn)槲业氖褂梅绞绞且粋€(gè) Disruptor隊(duì)列一個(gè)消費(fèi)者,所以我將隊(duì)列調(diào)整為只有 1 個(gè)再試試(策略依然是 YieldingWaitStrategy)。
跑了一分鐘,發(fā)現(xiàn) CPU 的使用率一直都比較平穩(wěn)而且不高。
總結(jié)
所以排查到此可以有一個(gè)結(jié)論了,想要根本解決這個(gè)問(wèn)題需要將我們現(xiàn)有的業(yè)務(wù)拆分;現(xiàn)在是一個(gè)應(yīng)用里同時(shí)處理了 N 個(gè)業(yè)務(wù),每個(gè)業(yè)務(wù)都會(huì)使用好幾個(gè) Disruptor 隊(duì)列。
由于是在一臺(tái)服務(wù)器上運(yùn)行,所以 CPU 資源都是共享的,這就會(huì)導(dǎo)致 CPU 的使用率居高不下。
所以我們的調(diào)整方式如下:
- 為了快速緩解這個(gè)問(wèn)題,先將等待策略換為 BlockingWaitStrategy,可以有效降低 CPU 的使用率(業(yè)務(wù)上也還能接受)。
- 第二步就需要將應(yīng)用拆分(上文模擬的一個(gè) Disruptor 隊(duì)列),一個(gè)應(yīng)用處理一種業(yè)務(wù)類型;然后分別單獨(dú)部署,這樣也可以互相隔離互不影響。
當(dāng)然還有其他的一些優(yōu)化,因?yàn)檫@也是一個(gè)老系統(tǒng)了,這次 dump 線程居然發(fā)現(xiàn)創(chuàng)建了 800+ 的線程。
創(chuàng)建線程池的方式也是核心線程數(shù)、***線程數(shù)是一樣的,導(dǎo)致一些空閑的線程也得不到回收;這樣會(huì)有很多無(wú)意義的資源消耗。
所以也會(huì)結(jié)合業(yè)務(wù)將創(chuàng)建線程池的方式調(diào)整一下,將線程數(shù)降下來(lái),盡量的物盡其用。