一次CPU佔用過高事件的胡亂解決

1.起因

之前組裏做了一個基於drools的業務邏輯校驗程序,使用的是java語言。因爲工作後一直就用C#,但是學校裏教的都是java,用上了很久沒用的java還是挺高興的。做完,測試完,放到線上,回家。

我們的服務設定是每天早上5點定時跑的,到了第二天早上,手機就接到Zabbix的報警短信,我們的那臺跑檢驗程序的機器CPU佔用率超過80%,雖然我們之前Drools規則系統已經加入了很多業務,但是CPU佔用過高這事從來沒發生過,這鍋看來必須自己背了。
這裏寫圖片描述

2.定位問題

到了公司,趕緊看看出了什麼問題。重新啓動程序,發現CPU佔用率在10-70之間波動。查看日誌可以發現CPU佔用率高的時候代碼正在查詢Sql Server數據庫。

我們數據庫訪問用到了iBatis,第一感覺是iBatis開了多線程消耗CPU,但是觀察程序運行時的線程數並沒有增多太多。想着用最新版本是不是可以解決,於是更新了iBatis庫的版本。。。然而CPU佔用率還是很高。

爲了確定是不是Sql語句本身導致的CPU佔用,我在Sql Server客戶端裏面試着運行了下程序的Sql語句,發現CPU佔用就沒超過20%過。。。

然後用JDBC執行了下訪問數據庫的語句,發現CPU佔用根本不高,而且程序速度也變快了很多。。。
似乎結論很明顯了:辣雞iBatis,速度慢還佔CPU。。

於是我花了一早上時間把原來的iBatis查詢數據庫的語句全替換成了JDBC的方式,然後放上了服務器,運行。結果非常讓人失望。。CPU依然佔用很高。。。

下午做別的事情了,這件事就沒管。第二天早上一來,發現程序報錯了。gc overhead limit exceeded。 百度之,內存不足?按網上的方法,修改了JVM的啓動參數,設置了JVM的最大內存,重新運行。這下不報錯了,但是CPU佔用高的問題還是存在。

覺得要好好查查哪裏出問題了。就從網上下載了Process Explorer這麼一個工具,用來分析每個線程的CPU佔用。
Process Explorer是Windows下的一個類似任務管理器的工具,但是除了能查詢更多參數外,Process Explorer在這裏最大的用處是能定位到每一個線程的CPU佔用。簡單介紹下用法:
這裏寫圖片描述

選擇需要查看的進程,右鍵,選中Properties。然後選中Threads選項卡,就能看到當前進程的所有線程TID,以及佔用CPU和CPU Cycles。這裏有Start Address,不過這個地址基本不是人能看懂的【皺眉】。

這裏寫圖片描述

運行程序,打開Process Explorer,終於發現了CPU佔用率波動的原因。從Process Explorer的畫面上能看到每隔一段時間,就有10多個線程的CPU佔用率突然變高,到3%左右,這十幾個線程同時佔用CPU又同時消失,而且這幾個線程的出現時各自的CPU佔用率幾乎一樣。想到昨天的gc overhead limit exceeded報錯,百度的結果大部分說的是內存不足,但是還發現有一種說法是GC時間過長而且回收內存過低,導致JVM認爲你存在內存溢出。。。於是提前報Out of Memory異常。可以給JVM加上啓動項取消這個提前退出機制,但是這麼做治標不治本,所以沒用。但是這幾個線程讓我懷疑到了GC上來。
這裏寫圖片描述
佔用CPU的線程現在用Process Explorer可以看到了,距離真相就差找到線程對應的代碼了。要怎麼看線程對應的代碼呢?Jdk有個自帶工具叫JStack,使用命令 JStack -l [pid] > C:\1.txt把分析結果輸入C:\1.txt。打開文件,搜索之前Process Explorer中看到的TID(JStack中tid是16進制記錄的,記得用計算機轉化爲16進制):
這裏寫圖片描述

具體用法還可以參考JStack和Process Explorer。查了幾個佔用CPU很高的線程,最終發現了罪魁禍首果然是Java的GC線程

如何查看GC情況呢?在JVM虛擬機後加上參數-XX:+PrintGCDateStamps -XX:+PrintGCDetails,這樣Java程序就會在控制檯中打印出GC詳情。發現部分GC時間很長,甚至達到9秒,沒截圖,從網上找了一段日誌,看起來是這樣的:
2015-12-06T12:32:02.890+0800:
[GC [PSYoungGen: 142833K->10728K(142848K)] 166113K->59145K(317952K), 0.0792023 secs]
[Times: user=0.22 sys=0.00, real=0.08 secs] 。
real=0.08 secs表示真實GC時間,PSYoungGen: 142833K->10728K(142848K) 中PSYoungGen表示這次回收是針對Young Generation對象的垃圾回收,142833K->10728K分別代表回收前和回收後的大小。

3.解決

看來是錯怪了iBatis了。於是從減少GC入手,一開始嘗試將程序中佔用內存較多的對象轉化爲Static對象存儲,企圖阻止GC收集,結果並沒有什麼用。。。。又嘗試設置iBatis的Ferch Size屬性,控制每次獲取數據庫的行數,減少內存增長速度,CPU佔用率稍有降低,但是還是不夠。百度了很久,發現了一篇文章:成爲Java GC專家(5)—Java性能調優原則 按照文章的說法,在JVM啓動時加上參數
-XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75,網上有提到JVM提供了3種垃圾收集器,如果是長生命週期(老年代)對象較多時可以使用CMS收集器。
這裏寫圖片描述

實驗證明主要是參數-XX:+UseConcMarkSweepGC起作用,添加上以後,CPU佔用率終於保持在20%左右,更神奇的是內存佔用都變小了。當然,垃圾回收時間減少了,程序速度都變快了不少。

這裏寫圖片描述

要理解其中具體原因,看來還得了解下JAVA GC的工作原理了。。。

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