網(wǎng)易二面:CPU狂飆900%,該怎么處理?
說(shuō)在前面
社群一位小伙伴面試了 網(wǎng)易,遇到了一個(gè) 性能類的面試題:
CPU飆升900%,該怎么處理?
可惜的是,以上的問(wèn)題,這個(gè)小伙沒(méi)有回答理想。
最終,導(dǎo)致他網(wǎng)易之路,終止在二面,非??上?/strong>
現(xiàn)在把這個(gè) 題目,以及參考答案,收入咱們的
《Java面試寶典 PDF》,供后面的小伙伴參考,前車之鑒啊
首先,說(shuō)明一下問(wèn)題:CPU飆升200% 以上是生產(chǎn)容易發(fā)生的場(chǎng)景
注:本文以 PDF 持續(xù)更新,最新Java 架構(gòu)筆記、面試題 的PDF文件,請(qǐng)后臺(tái)私信【筆記】獲取哦
場(chǎng)景:1:MySQL進(jìn)程飆升900%
大家在使用MySQL過(guò)程,想必都有遇到過(guò)CPU突然過(guò)高,或者達(dá)到200%以上的情況。
數(shù)據(jù)庫(kù)執(zhí)行查詢或數(shù)據(jù)修改操作時(shí),系統(tǒng)需要消耗大量的CPU資源維護(hù)從存儲(chǔ)系統(tǒng)、內(nèi)存數(shù)據(jù)中的一致性。
并發(fā)量大并且大量SQL性能低的情況下,比如字段是沒(méi)有建立索引,則會(huì)導(dǎo)致快速CPU飆升,如果還開啟了慢日志記錄,會(huì)導(dǎo)致性能更加惡化。生產(chǎn)上有MYSQL 飆升900% 的惡劣情況。
場(chǎng)景2:Java進(jìn)程飆升900%
一般來(lái)說(shuō)Java 進(jìn)程不做大量 CPU 運(yùn)算,正常情況下,CPU 應(yīng)該在 100~200% 之間,
但是,一旦高并發(fā)場(chǎng)景,要么走到了死循環(huán),要么就是在做大量的 GC, 容易出現(xiàn)這種 CPU 飆升的情況,CPU飆升900%,是完全有可能的。
其他場(chǎng)景:其他的類似進(jìn)程飆升900%的場(chǎng)景
比如Redis、Nginx等等。
尼恩提示:大家介紹場(chǎng)景的時(shí)候,就說(shuō)自己主要涉及了兩個(gè)場(chǎng)景, Java進(jìn)程飆升900%、MySQL進(jìn)程飆升900%兩種場(chǎng)景,其實(shí),這兩個(gè)場(chǎng)景就足夠講半天了, 其他的,使用規(guī)避技巧規(guī)避一下就行。
場(chǎng)景一:MySQL進(jìn)程CPU飆升到900%,怎么處理?
定位過(guò)程:
- 使用top 命令觀察,確定是mysqld導(dǎo)致還是其他原因。
- 如果是mysqld導(dǎo)致的,show processlist,查看session情況,確定是不是有消耗資源的sql在運(yùn)行。
- 找出消耗高的 sql,看看執(zhí)行計(jì)劃是否準(zhǔn)確, index 是否缺失,或者實(shí)在是數(shù)據(jù)量太大造成。
處理過(guò)程:
- kill 掉這些線程(同時(shí)觀察 cpu 使用率是否下降), 一般來(lái)說(shuō),肯定要 kill 掉這些線程(同時(shí)觀察 cpu 使用率是否下降),等進(jìn)行相應(yīng)的調(diào)整(比如說(shuō)加索引、改 sql、改內(nèi)存參數(shù))之后,再重新跑這些 SQL。
- 進(jìn)行相應(yīng)的調(diào)整(比如說(shuō)加索引、改 sql、改內(nèi)存參數(shù))
index 是否缺失,如果是,則 建立索引。也有可能是每個(gè) sql 消耗資源并不多,但是突然之間,有大量的 session 連進(jìn)來(lái)導(dǎo)致 cpu 飆升,這種情況就需要跟應(yīng)用一起來(lái)分析為何連接數(shù)會(huì)激增,再做出相應(yīng)的調(diào)整,比如說(shuō)限制連接數(shù)等 - 優(yōu)化的過(guò)程,往往不是一步完成的,而是一步一步,執(zhí)行一項(xiàng)優(yōu)化措辭,再觀察,再優(yōu)化。
場(chǎng)景1的真實(shí)案例:MySQL數(shù)據(jù)庫(kù)優(yōu)化的真實(shí)案例
尼恩提示:以下案例,來(lái)自互聯(lián)網(wǎng)。大家參考一下,準(zhǔn)備一個(gè)自己的案例。
本問(wèn)題親身經(jīng)歷過(guò)。
之前開發(fā)同事編寫的SQL語(yǔ)句,就導(dǎo)致過(guò)線上CPU過(guò)高,MySQL的CPU使用率達(dá)到900%+,通過(guò)優(yōu)化最后降低到70%~80%。下面說(shuō)說(shuō)個(gè)人在這個(gè)過(guò)程中的排查思路。
首先,我們要對(duì)問(wèn)題定位而不是盲目的開啟什么 慢日志,在并發(fā)量大并且大量SQL性能低的情況下,開啟慢日志無(wú)意是將MySQL推向崩潰的邊緣。
當(dāng)時(shí)遇到這個(gè)情況,分析了當(dāng)前的數(shù)據(jù)量、索引情況、緩存使用情況。目測(cè)數(shù)據(jù)量不大,也就幾百萬(wàn)條而已。接下來(lái)就去定位索引、緩存問(wèn)題。
- 經(jīng)過(guò)詢問(wèn),發(fā)現(xiàn)很多查詢都是走M(jìn)ySQL,沒(méi)有用到緩存。
- 既然沒(méi)有用到緩存,則是大量請(qǐng)求全部查詢MySQL導(dǎo)致。通過(guò)下面的命令查看:
發(fā)現(xiàn)類似很多相同的SQL語(yǔ)句,一直處于query狀態(tài)中。
初步分析可能是 user_code 字段沒(méi)有索引導(dǎo)致。接著查詢user表的索引情況:
發(fā)現(xiàn)這個(gè)字段是沒(méi)有建立索引。增加索引之后,該條SQL查詢能夠正常執(zhí)行。
3、沒(méi)隔一會(huì),又發(fā)生大量的請(qǐng)求超時(shí)問(wèn)題。接著進(jìn)行分析,發(fā)現(xiàn)是開啟了 慢日志查詢。大量的SQL查詢語(yǔ)句超過(guò)慢日志設(shè)置的閥值,于是將慢日志關(guān)閉之后,速度瞬間提升。CPU的使用率基本保持在300%左右。但還不是理想狀態(tài)。
4、緊接著將部分實(shí)時(shí)查詢數(shù)據(jù)的SQL語(yǔ)句,都通過(guò)緩存(redis)讀寫實(shí)現(xiàn)。觀察一段時(shí)間后,基本維持在了70%~80%。
總結(jié):其實(shí)本次事故的解決很簡(jiǎn)單,就是添加索引與緩存結(jié)合使用。
- 不推薦在這種CPU使用過(guò)高的情況下進(jìn)行慢日志的開啟。因?yàn)榇罅康恼?qǐng)求,如果真是慢日志問(wèn)題會(huì)發(fā)生日志磁盤寫入,性能賊低。
- 直接通過(guò)MySQL show processlist命令查看,基本能清晰的定位出部分查詢問(wèn)題嚴(yán)重的SQL語(yǔ)句,在針對(duì)該SQL語(yǔ)句進(jìn)行分析。一般可能就是索引、鎖、查詢大量字段、大表等問(wèn)題導(dǎo)致。
- 再則一定要使用緩存系統(tǒng),降低對(duì)MySQL的查詢頻次。
- 對(duì)于內(nèi)存調(diào)優(yōu),也是一種解決方案。
場(chǎng)景2展開:Java進(jìn)程CPU飆升到900%,怎么處理?
定位過(guò)程:
CPU飆升問(wèn)題定位的一般步驟是:
- 首先通過(guò)top指令查看當(dāng)前占用CPU較高的進(jìn)程PID;
- 查看當(dāng)前進(jìn)程消耗資源的線程PID:top -Hp PID
- 通過(guò)print命令將線程PID轉(zhuǎn)為16進(jìn)制,根據(jù)該16進(jìn)制值去打印的堆棧日志內(nèi)查詢,查看該線程所駐留的方法位置。
- 通過(guò)jstack命令,查看棧信息,定位到線程對(duì)應(yīng)的具體代碼。
- 分析代碼解決問(wèn)題。
處理過(guò)程:
- 如果是空循環(huán),或者空自旋。
- 處理方式:可以使用Thread.sleep或者加鎖,讓線程適當(dāng)?shù)淖枞?/li>
- 在循環(huán)的代碼邏輯中,創(chuàng)建大量的新對(duì)象導(dǎo)致頻繁GC。比如,從mysql查出了大量的數(shù)據(jù),比如100W以上等等。
- 處理方式:可以減少對(duì)象的創(chuàng)建數(shù)量,或者,可以考慮使用 對(duì)象池。
- 其他的一些造成CPU飆升的場(chǎng)景,比如 selector空輪訓(xùn)導(dǎo)致CPU飆升 。
- 處理方式:參考Netty源碼,無(wú)效的事件查詢到了一定的次數(shù),進(jìn)行 selector 重建。
Java的CPU 飆升700%優(yōu)化的真實(shí)案例
尼恩提示:以下案例,來(lái)自互聯(lián)網(wǎng)。大家參考一下,準(zhǔn)備一個(gè)自己的案例。
最近負(fù)責(zé)的一個(gè)項(xiàng)目上線,運(yùn)行一段時(shí)間后發(fā)現(xiàn)對(duì)應(yīng)的進(jìn)程竟然占用了700%的CPU,導(dǎo)致公司的物理服務(wù)器都不堪重負(fù),頻繁宕機(jī)。
那么,針對(duì)這類java進(jìn)程CPU飆升的問(wèn)題,我們一般要怎么去定位解決呢?、
采用top命令定位進(jìn)程
登錄服務(wù)器,執(zhí)行top命令,查看CPU占用情況,找到進(jìn)程的pid
很容易發(fā)現(xiàn),PID為29706的java進(jìn)程的CPU飆升到700%多,且一直降不下來(lái),很顯然出現(xiàn)了問(wèn)題。
使用top -Hp命令定位線程
使用 top -Hp <pid> 命令(為Java進(jìn)程的id號(hào))查看該Java進(jìn)程內(nèi)所有線程的資源占用情況(按shft+p按照cpu占用進(jìn)行排序,按shift+m按照內(nèi)存占用進(jìn)行排序)
此處按照cpu排序:
很容易發(fā)現(xiàn),多個(gè)線程的CPU占用達(dá)到了90%多。我們挑選線程號(hào)為30309的線程繼續(xù)分析。
使用jstack命令定位代碼
1.線程號(hào)轉(zhuǎn)換5為16進(jìn)制
printf “%x\n” 命令(tid指線程的id號(hào))將以上10進(jìn)制的線程號(hào)轉(zhuǎn)換為16進(jìn)制:
轉(zhuǎn)換后的結(jié)果分別為7665,由于導(dǎo)出的線程快照中線程的nid是16進(jìn)制的,而16進(jìn)制以0x開頭,所以對(duì)應(yīng)的16進(jìn)制的線程號(hào)nid為0x7665
2.采用jstack命令導(dǎo)出線程快照
通過(guò)使用dk自帶命令jstack獲取該java進(jìn)程的線程快照并輸入到文件中:
命令(為Java進(jìn)程的id號(hào))來(lái)獲取線程快照結(jié)果并輸入到指定文件。
3.根據(jù)線程號(hào)定位具體代碼
在jstack_result.txt 文件中根據(jù)線程好nid搜索對(duì)應(yīng)的線程描述
根據(jù)搜索結(jié)果,判斷應(yīng)該是ImageConverter.run()方法中的代碼出現(xiàn)問(wèn)題
當(dāng)然,這里也可以直接采用
來(lái)定位具體代碼
分析代碼解決問(wèn)題
下面是ImageConverter.run()方法中的部分核心代碼。
邏輯說(shuō)明:
在while循環(huán)中,不斷讀取堵塞隊(duì)列dataQueue中的數(shù)據(jù),如果數(shù)據(jù)為空,則執(zhí)行continue進(jìn)行下一次循環(huán)。
如果不為空,則通過(guò)poll()方法讀取數(shù)據(jù),做相關(guān)邏輯處理。
初看這段代碼好像每什么問(wèn)題,但是如果dataQueue對(duì)象長(zhǎng)期為空的話,這里就會(huì)一直空循環(huán),導(dǎo)致CPU飆升。
那么如果解決呢?
分析LinkedBlockingQueue阻塞隊(duì)列的API發(fā)現(xiàn):
這兩種取值的API,顯然take方法更時(shí)候這里的場(chǎng)景。
代碼修改為:
重啟項(xiàng)目后,測(cè)試發(fā)現(xiàn)項(xiàng)目運(yùn)行穩(wěn)定,對(duì)應(yīng)項(xiàng)目進(jìn)程的CPU消耗占比不到10%。