關于CPU使用率飆升,我們需要了解什么?
CPU% = (1 - idleTime / sysTime) * 100
idleTime:CPU處于空閑狀態(tài)的時間
sysTime:CPU處于用戶態(tài)和內核臺的時間總和
2、CPU 使用率跟啥有關系?
常聽說計算密集型的程序是比較耗 CPU 使用率的。
3、CPU 與進程、線程有關系么?
現在分時操作系統(tǒng)是通過循輪方式分配時間片進行進程調度的,如果進程在等待或阻塞,不會造成 CPU 資源使用。線程稱為輕進程,共享進程資源,關于線程的調度,CPU 對于線程也是分時調度。而在 Java 中,線程的調用由 JVM 負責,線程的調度一般有兩種模式,分時調度和搶占式調度。
4、一個 while 死循環(huán),會不會引起 CPU 使用率飚升?
會的。
先不說別的,死循環(huán)會調用 CPU 寄存器進行計數,這個操作就會占用 CPU。其次,如果線程一直處于死循環(huán)狀態(tài),CPU 調用會進行線程切換么?
死循環(huán)不會讓出 CPU,除非操作系統(tǒng)時間片到期,但死循環(huán)會不斷向系統(tǒng)申請時間片,直到系統(tǒng)沒有空閑時間做別的事情。
這個問題在 stackoverflow 也有人提問:why does an infinite loop of the unintended kind increase the CPU use?
地址:https://stackoverflow.com/questions/2846165/why-does-an-infinite-loop-of-the-unintended-kind-increase-the-cpu-use
5、頻繁 Young GC 會不會引起 CPU 使用率飚升?
會的。
Young GC 本身是 JVM 進行垃圾回收的操作,會計算內存和調用寄存器,頻繁 Young GC 一定是會占用 CPU。
之前有個一個案例,for 循環(huán)從數據庫查詢數據集合,二次封裝新的數據集合,這時如果量比較大時,內存沒有足夠的空間存儲,那么 JVM 就會 GC 回收那些不再使用的數據,因此量大的時候,就會收到 CPU 使用率報警。
6、線程數很高的應用,CPU 使用率一定高么?
不會。
通過 jstack 查看系統(tǒng)線程狀態(tài),查看整個線程數很多,但 Runable 和 Running 狀態(tài)的線程不多,這時 CPU 使用率不一定會高。
之前有過一個案例,查看系統(tǒng)線程數 1000+,jstack 分析 900多個線程是 BLOCKED 和 WAITING 狀態(tài)的,這種線程是不會占用 CPU 的。
如果線程數很高,其實大多數原因是死鎖,大量線程處于 BLOCKED 和 WAITING 狀態(tài)。
7、CPU 使用率高的應用,線程數一定高么?
不會。
同上,CPU 使用率高的關鍵因素還是計算密集型操作,一個線程如果有大量計算,也會造成 CPU 使用率高,也是現在為什么一個大數據腳本任務,要大規(guī)模集群共同運算才能運行的原因。
8、BLOCKED 狀態(tài)的線程會不會引起 CPU 使用率飚升?
不一定。
CPU使用率的飆升,更多是因為上下文的切換或者runnable狀態(tài)線程過多導致。Blocked狀態(tài),未必會引起CPU上升。
9、分時操作系統(tǒng) CPU us高或者sy高是什么意思?
通過top命令,可以觀察到CPU的us,sy值,示例如下:
us 用戶空間占用CPU百分比,簡單來說,us高是因為程序導致的,通過分析線程堆棧,可以很容易的定位到問題線程。
sy 內核空間占用CPU百分比,sy高的時候,如果是程序問題導致,基本是因為線程上下文切換造成的。
CPU飆升線程定位示例:
public class cpuTest {
public static void main(String args[]){
for(int i=0;i<10;i++){
new Thread(){
public void run(){
try{
Thread.sleep(100000);
}catch(Exception e){}
}
}.start();
}
Thread t=new Thread(){
public void run(){
int i=0;
while(true){
i=(i++)/100;
}
}
};
t.setName("Busiest Thread");
t.start();
}
}
步驟1: 執(zhí)行top -c
,顯示進程運行信息列表,鍵入P (大寫p),進程按照CPU使用率排序,最耗CPU的進程PID為18207
步驟2:首先我們可以通過top -Hp <pid>
來看這個進程里所有線程的cpu消耗情況
$ top -Hp 18207top - 19:11:43 up 573 days, 2:43, 2 users, load average: 3.03, 3.03, 3.02Tasks: 44 total, 1 running, 43 sleeping, 0 stopped, 0 zombieCpu(s): 18.8%us, 0.0%sy, 0.0%ni, 81.1%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%stMem: 99191752k total, 98683576k used, 508176k free, 128248k buffersSwap: 1999864k total, 191064k used, 1808800k free, 17413760k cached PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND18250 admin 20 0 26.1g 28m 10m R 99.9 0.0 0:19.50 java Test18207 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test18208 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.09 java Test18209 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test18210 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test18211 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java
cpu最高的線程是pid為18250的線程,占了99.9%
步驟3:printf “%x\n” 18250
將線程PID轉化為16進制
$ printf “%x\n” 18250
0X47A
...
步驟4:jstack 18207|grep'0X47A'-C5 --color
找出進程中消耗CPU最多的線程棧
$ jstack 18207|grep'0X47A'-C5 --colorFull thread dump OpenJDK 64-Bit Server VM (25.66-b60 mixed mode):"Attach Listener" #30 daemon prio=9 os_prio=0 tid=0x00007fb90be13000 nid=0x47d7 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE"DestroyJavaVM" #29 prio=5 os_prio=0 tid=0x00007fb96245b800 nid=0x4720 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE"Busiest Thread" #28 prio=5 os_prio=0 tid=0x00007fb91498d000 nid=0x474a runnable [0x00007fb9065fe000] java.lang.Thread.State: RUNNABLE at Test$2.run(Test.java:18)"Thread-9" #27 prio=5 os_prio=0 tid=0x00007fb91498c800 nid=0x4749 waiting on condition [0x00007fb906bfe000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at Test$1.run(Test.java:9)...
因此,最耗cpu的線程是Busiest Thread
日常程序中常見的耗CPU的操作:
1、頻繁GC,訪問量高時,有可能造成頻繁的GC、甚至FGC。當調用量大時,內存分配過快,就會造成GC線程不停的執(zhí)行,導致CPU飆高
2、序列化與反序列化,后文中舉了一個真實的案例,程序執(zhí)行xml解析的時,調用量增大的情況下,導致了CPU被打滿
3、加密解密
4、正則表達式校驗,曾經線上發(fā)生一次血案,正則校驗將CPU打滿。大概原因是:Java 正則表達式使用的引擎實現是 NFA 自動機,這種引擎在進行字符匹配會發(fā)生回溯(backtracking)
5、線程上下文切換、當啟動了很多線程,而這些線程都處于不斷的阻塞狀態(tài)(鎖等待、IO等待等)和執(zhí)行狀態(tài)的變化過程中。當鎖競爭激烈時,很容易出現這種情況
6、某些線程在做無阻塞的運算,簡單的例子while(true)中不停的做運算,沒有任何阻塞。寫程序時,如果需要做很久的計算,可以適當將程序sleep下
7、Excel 導出事件
頻繁GC案例
案例背景:網關服務進行控制單個url訪問次數限流,CPU過若干天后飆升到80%,重啟服務過若干天后又再次飆升到80%
分析過程:通過上述方法進行線程棧定位,并進行內存堆分析,發(fā)現pathRaterLimiterConcurrentHashMap對象占用大量堆空間,找到程序中對應的filter過濾器代碼如下
public HttpRequestMessage apply(HttpRequestMessage request) {
String requestPath = request.getPath();
if (pathRaterLimiterConcurrentHashMap.containsKey(requestPath)) {
if (!pathRaterLimiterConcurrentHashMap.get(requestPath).tryAcquire()) {
log.warn("too many request:"+requestPath+",time:"+System.currentTimeMillis());
SessionContext context = request.getContext();
if(requestPath!=null && requestPath.equals("/voting/selection")){
context.setEndpoint(VoteTimeOutEndpoint.class.getCanonicalName());
}
//context.setEndpoint(ManyRequestsEndpoint.class.getCanonicalName());
request.setPath("/404.html");
context.setRouteVIP("limit-api");
}
} else {
pathRaterLimiterConcurrentHashMap.put(requestPath, RateLimiter.create(limit));
pathRaterLimiterConcurrentHashMap.get(requestPath).tryAcquire();
}
return request;
}
分析發(fā)現當url不斷增加時,map的k-v會不斷增加,以至于最后頻繁觸發(fā)fullgc,導致cpu飆升。
解決方案:跑一個定時任務線程定期清理map中的對象,后采用LRU算法重寫一個maputil,定時清理過期的key值。
正則表達式案例
案例背景:前幾天線上一個項目監(jiān)控信息突然報告異常,上到機器上后查看相關資源的使用情況,發(fā)現 CPU 利用率將近 100%。
分析過程:
通過 Java 自帶的線程 Dump 工具,我們導出了出問題的堆棧信息。我們可以看到所有的堆棧都指向了一個名為 validateUrl 的方法,這樣的報錯信息在堆棧中一共超過 100 處。通過排查代碼,我們知道這個方法的主要功能是校驗 URL 是否合法。
很奇怪,一個正則表達式怎么會導致 CPU 利用率居高不下。為了弄清楚復現問題,我們將其中的關鍵代碼摘抄出來,做了個簡單的單元測試。
public static void main(String[] args) {
String badRegex = "^([hH][tT]{2}[pP]://|[hH][tT]{2}[pP][sS]://)" +
"(([A-Za-z0-9-~]+).)+([A-Za-z0-9-~\\\\/])+$";
String bugUrl = "http://www.fapiao.com/dddp-web/pdf/download?" +
"request=6e7JGxxxxx4ILd-kExxxxxxxqJ4-CHLmqVnenXC692m7" +
"4H38sdfdsazxcUmfcOH2fAfY1Vw__%5EDadIfJgiEf";
if (bugUrl.matches(badRegex)) {
System.out.println("match!!");
} else {
System.out.println("no match!!");
}
}
正則表達式分為三部分:
第一部分匹配 http 和 https 協(xié)議,第二部分匹配 www. 字符,第三部分匹配許多字符。我看著這個表達式發(fā)呆了許久,也沒發(fā)現沒有什么大的問題。
其實這里導致 CPU 使用率高的關鍵原因就是:Java 正則表達式使用的引擎實現是 NFA 自動機,這種正則表達式引擎在進行字符匹配時會發(fā)生回溯(backtracking)。而一旦發(fā)生回溯,那其消耗的時間就會變得很長,有可能是幾分鐘,也有可能是幾個小時,時間長短取決于回溯的次數和復雜度。
正則表達式是一個很方便的匹配符號,但要實現這么復雜,功能如此強大的匹配語法,就必須要有一套算法來實現,而實現這套算法的東西就叫做正則表達式引擎。簡單地說,實現正則表達式引擎的有兩種方式:DFA 自動機(Deterministic Final Automata 確定型有窮自動機)和 NFA 自動機(Non deterministic Finite Automaton 不確定型有窮自動機)。
對于這兩種自動機,他們有各自的區(qū)別,這里并不打算深入將它們的原理。簡單地說,DFA 自動機的時間復雜度是線性的,更加穩(wěn)定,但是功能有限。而 NFA 的時間復雜度比較不穩(wěn)定,有時候很好,有時候不怎么好,好不好取決于你寫的正則表達式。
解決方案:
其實在正則表達式中有這么三種模式:貪婪模式、懶惰模式、獨占模式。在關于數量的匹配中,有 + ? * {min,max}
四種,如果只是單獨使用,那么它們就是貪婪模式。
如果在他們之后加多一個 ? 符號,那么原先的貪婪模式就會變成懶惰模式,即盡可能少地匹配。但是懶惰模式還是會發(fā)生回溯現象的。
如果在他們之后加多一個 + 符號,那么原先的貪婪模式就會變成獨占模式,即盡可能多地匹配,但是不回溯。
String goodRegex = "^([hH][tT]{2}[pP]://|[hH][tT]{2}[pP][sS]://)" +
"(([A-Za-z0-9-~]+).)++([A-Za-z0-9-~\\\\/])+$";
Excel導出案例
案例背景:京東某系統(tǒng)使用了poi-ooxml-3.5-final做excel導出功能。起初使用該版本的poi的HSSF配合多線程生成excel,沒有任何問題,后來改成了XSSF生成后上線,導出3w條數據時,cpu使用率達到了100%,內存達到了100%
由于cpu使用率打爆,內存打爆,整個服務器處于拒絕服務狀態(tài),而呈現到前端則是應用系統(tǒng)大部分卡死。于是業(yè)務方不斷反復點擊導出按鈕,狀況不斷擴大到集群內其他機器上,導致集群出現雪崩現象。
分析過程
由于服務器已經被打死,內存那么高,根本無法dump線上堆內存,甚至連jstack查看線程棧都無法使用。因此嘗試在測試機復盤
可見eden空間的s0和s1已經無法交換了,eden空間已經完全打滿,old空間也一樣打滿,yong gc和full gc都非常頻繁,cpu自然使用率高了,不過不足以打滿整個cpu!現在目前定位到了fullgc沒有回收垃圾,那么需要找到內存打滿和為啥沒回收的原因。要想找到內存打滿的原因肯定需要分析heap空間對象。
由于問題出現在導出報表,并且已知升級了版本并且改成了單線程導出就解決了,同時之前使用HSSF的時候并沒有出現問題,也證明了業(yè)務代碼沒有問題,問題出現在XSSF的版本和多線程上。所以本地可以模擬poi-ooxml-3.5-FINAL的XSSF進行大量數據的導出實驗,同時需要進行多線程導出。
在本地mock數據可以使用簡單的大量對象構成的結構進行導出,線上30個列導出,本地測試5個列,線上是本地的6倍,線上的每一行的數據量必然要比本地的數據量大很多。同時懷疑是poi-ooxml-3.5-FINAL內存泄露或內存管理出現的問題,那么其實不需要4g內存,在2g的內存下壓榨到死看看heap中大量的對象是不是poi相關的就可以了。然后再升級下版本,繼續(xù)壓榨一下看看會不會壓死即可。
public static void main(String[] args) {
int size = 500000;
List<User> users = new ArrayList<>(size);
User user;
for (int i = 0; i < size; i++) {
user = new User();
user.setId(Integer.toUnsignedLong(i));
user.setAge(i + 10);
user.setName("user" + i);
user.setRemark(System.currentTimeMillis() + "");
user.setSex("男");
users.add(user);
}
new Thread(() -{
String[] columnName = {"用戶id", "姓名", "年齡", "性別", "備注"};
Object[][] data = new Object[size][5];
int index = 0;
for (User u : users) {
data[index][0] = u.getId();
data[index][1] = u.getName();
data[index][2] = u.getAge();
data[index][3] = u.getSex();
data[index][4] = u.getRemark();
index++;
}
XSSFWorkbook xssfWorkbook = generateExcel("test", "test", columnName, data);
}).start();
try {
Thread.currentThread().join();//等待子線程結束
} catch (InterruptedException e) {
e.printStackTrace();
}
}
模擬現象與線上情況類似,大量cpu占用在XSSFCell.setCellValue中,生成excel generateExcel就占據了所有的cpu,堆信息全是POI對象
這里還需要注意的是,需要驗證poi-ooxml-3.5-FINAL在多線程情況下是否會出現這個問題,驗證很簡單,把new Thread去掉,直接在主線程導出。這里直接說明實驗結果,new Thread去了依然內存爆滿!
解決方案
查看poi官網的change log http://poi.apache.org/changes.html ,既然3.5-FINAL的XSSF有問題,向上查找3.5-FINAL之后的XSSF相關字樣的信息,會發(fā)現在3.6中memory usage optimization in xssf - avoid creating parentless xml beans,xxsf進行中做了內存優(yōu)化 - 避免了創(chuàng)建無父類的xml bean對象
所以得出結論,升級poi-oxxml版本到3.6或者更高版本!
來源:技術讓夢想更偉大
免責聲明:本文內容由21ic獲得授權后發(fā)布,版權歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯系我們,謝謝!