CPU和磁碟IO實戰筆記總結
前言
前面分析了linux的平均負載,在分析的時候自己也順便實際測試了一番,測試例子記錄如下,防止遺忘。
先說測試結論:load average平均負載和cpu使用率以及磁碟IO有關,和網路IO無關。
1.cpu使用率高問題分析
首先先確定什麼操作會導致cpu使用率飈高,在我們java應用中,通常會有下面一些程式碼,下面那些情況會對cpu的開銷大呢?
1.多層的 if switch。對cpu開銷很小。
2.網路傳輸(接入請求和響應請求要經過網路)。網路傳輸是瓶頸在IO上,對cpu開銷小。
3.執行緒阻塞(同步鎖、wait)。執行緒既然阻塞了,就暫時不再使用cpu資源,因此對cpu開銷也小。執行緒阻塞會對效能造成影響,直接感覺就是系統執行緩慢。
4.多層迴圈巢狀或者遞迴。這種可能會對cpu的開銷不小,如果在迴圈內又做了比如排序等操作。
5.字串處理。常見的消耗cpu的操作是json解析、正則表示式
6.日期格式化。比如使用jdk的SimpleDateFormat.format耗費cpu,建議使用apache的FaseDateFormat。
7.大量的sql資料處理。一次查詢多大量資料,需要對資料進行處理返回,對cpu開銷也大,這個情況經常有。
8.執行緒上下文頻繁切換。對cpu的開銷也大。
9.死迴圈或者while(true){}情況無限執行
10.頻繁full gc導致cpu 100%
1.1.實測正則表示式開銷cpu100%
測試程式碼如下
//controller
@RequestMapping(value = "/test/cpu/regex", method = RequestMethod.GET)
public CommonResult<String> cpuRegex() {
allocateService.cpuRegex();
return CommonResult.success(null);
}
//service方法
/**
* 使用正則表示式讓cpu達到100%,這個結果計算要15s
*/
@Override
public void cpuRegex() {
String regex = "(\\w+,?)+";
String val = "abcdefghijklmno,abcdefghijklmno+";
log.info("正則結果->{}", val.matches(regex));//val.matches耗費cpu
}
使用jmeter併發2,壓測5min。(因為這個正則表示式執行特殊,直接會把cpu打滿,我測試伺服器是2C,因此開2個執行緒就可以把cpu打滿)
壓測一段時間後通過top命令檢視,發現cpu被打滿了,平均負載已經超過了2*0.7
1.1.1.使用傳統方式top -Hp PID 和jstack PID確定開銷cpu最高的執行緒
step1:間隔幾秒多次執行jstack PID >> regex ,多次執行是為了查出共性
step2:top -Hp PID 檢視並記錄幾個執行緒ID,如下圖
上圖中的1443,1458,20023就是執行緒ID,轉換為16進位制數
printf "%x\n" 20023結果為4e37,在jstack結果檔案regex1問題內進行搜尋,比如執行grep -A 50 4e37 regex1 查詢4e37後面50行
在確定cpu消耗上,只檢視java.lang.Thread.State: RUNNABLE的堆疊,這個符合,但是這個堆疊是阻塞在socket讀取上,並不會消耗CPU的,因此這個忽略(這也是為什麼要多次執行jstack的原因,多次執行找能找出共性)
printf "%x\n" 1443結果為5a3,在jstack結果檔案regex1問題內進行搜尋,比如執行grep -A 100 5a3 regex1 查詢4e37後面50行
發現業務方法內的org.allocate.memory.allocate.service.impl.AllocateServiceImpl.cpuRegex()對cpu的開銷大,比較可疑,再同檢視其它佔用cpu資源高的執行緒,進行查詢進行驗證。最終確定是這個程式碼問題導致cpu100%。
1.1.2.使用show-busy-java-threads指令碼確定cpu開銷大問題
github有個大神專門寫了個指令碼show-busy-java-threads (地址https://github.com/oldratlee/useful-scripts/blob/master/docs/java.md#-show-busy-java-threads)分析cpu100%問題,超級好用,在這裡我直接執行 show-busy-java-threads 2 5 > regex2 列印最消耗cpu的執行緒堆疊,直接檢視結果檔案regex2,內容和分析結果如下:
================================================================================
2020-12-03 14:21:00.086099451 [1/5]: /home/baseuser/bin/show-busy-java-threads 2 5
================================================================================
[1] Busy(99.9%) thread(32256/0x7e00) stack of java process(32178) under user(baseuser)://確定檢視到
"http-nio-8111-exec-10" #38 daemon prio=5 os_prio=0 tid=0x00007f6eb9a31000 nid=0x7e00 runnable [0x00007f6e802bd000]
java.lang.Thread.State: RUNNABLE
at java.util.regex.Pattern$GroupTail.match(Pattern.java:4717)
at java.util.regex.Pattern$Ques.match(Pattern.java:4182)
//省略其它堆疊
at java.lang.String.matches(String.java:2121)
at org.allocate.memory.allocate.service.impl.AllocateServiceImpl.cpuRegex(AllocateServiceImpl.java:114)
at org.allocate.memory.allocate.controller.AllocateController.cpuRegex(AllocateController.java:34)
//省略其它堆疊
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1415)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
- locked <0x00000000a242f908> (a org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
[2] Busy(98.0%) thread(1399/0x577) stack of java process(32178) under user(baseuser):
"http-nio-8111-exec-12" #45 daemon prio=5 os_prio=0 tid=0x00007f6e90001800 nid=0x577 runnable [0x00007f6e63af8000]
java.lang.Thread.State: RUNNABLE
at java.util.regex.Pattern$Curly.match(Pattern.java:4226)
at java.util.regex.Pattern$GroupHead.match(Pattern.java:4658)
at java.util.regex.Pattern$Loop.match(Pattern.java:4785)
//省略其它堆疊
at java.lang.String.matches(String.java:2121)
at org.allocate.memory.allocate.service.impl.AllocateServiceImpl.cpuRegex(AllocateServiceImpl.java:114)
at org.allocate.memory.allocate.controller.AllocateController.cpuRegex(AllocateController.java:34)
//省略其它堆疊
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1415)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
- locked <0x00000000a242fce0> (a org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
[3] Busy(0.0%) thread(1415/0x587) stack of java process(32178) under user(baseuser):
"http-nio-8111-exec-29" #61 daemon prio=5 os_prio=0 tid=0x00007f6e98015800 nid=0x587 waiting on condition [0x00007f6e62aeb000]
java.lang.Thread.State: WAITING (parking)//非RUNNABLE狀態不消耗cpu,忽略
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000d00215f8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:107)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:33)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
[4] Busy(0.0%) thread(1420/0x58c) stack of java process(32178) under user(baseuser):
"http-nio-8111-exec-34" #66 daemon prio=5 os_prio=0 tid=0x00007f6e9801d000 nid=0x58c waiting on condition [0x00007f6e625e6000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000d00215f8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:107)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:33)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
[5] Busy(0.0%) thread(1428/0x594) stack of java process(32178) under user(baseuser):
"http-nio-8111-exec-41" #73 daemon prio=5 os_prio=0 tid=0x00007f6e90018000 nid=0x594 waiting on condition [0x00007f6e61edf000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000d00215f8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:107)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:33)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
直接檢視busy最大的即可,這個很神器。
1.2.實測日期格式化對cpu資源的開銷
SimpleDateFormat.format操作比較耗費cpu,測試程式碼如下
/**
* 使用jdk的SimpleDateFormat.format耗費cpu,建議使用apache的FaseDateFormat
*/
@Override
public void cpuDateFormat() {
for (int i=0;i<10;i++) {
String current = format.format(new Date());
String date = format.format(System.currentTimeMillis()+1000);
}
}
併發200,壓測10min情況下top如圖
top -Hp PID結果如下圖
發現執行緒對cpu的開銷比較均衡,再通過檢視show-busy-java-threads 2 10 > format.log檢視結果,在format.log檔案內搜尋Busy(
,截圖如下:
每個執行緒對cpu的消耗是差不多的,和通過top -Hp PID結果是相同的。接著搜尋farmat.log檔案內的java.lang.Thread.State:
檢視執行緒整體狀態,發現基本處於TIMED_WAITING (parking)
,截圖如下,這個狀態是由於執行了有超時時間限制的LockSupport.parkXXX操作
具體的堆疊如下
這個堆疊說明tomcat執行緒阻塞在從佇列獲取執行任務。繼而說明大多數執行緒都是處於阻塞狀態,cpu頻繁處於執行緒切換狀態(執行緒在cpu執行,只有在阻塞情況下,cpu才會切換到其它執行緒執行),通過vmstat 2 10000檢視cs每秒上下文切換數指標如下:
每秒執行緒要切換5000次左右,我跟生產伺服器的vmstat cs指標對比了下,生產是2000~3000,cpu使用率是個位數,看來這個5000次切換並不大。
和別人說的日期格式開銷cpu大並不是很契合呀。
1.3.執行緒上下文大量切換導致cpu使用率高測試
再做測試,測試下執行緒上下文大量切換對cpu使用率的影響。測試程式碼如下,每次請求讓阻塞兩次,這樣cpu切換至少2次
@Override
public void cpuSwitch() {
try {
Thread.sleep(1);//執行緒切換,釋放cpu資源,休眠1ms再去搶佔cpu資源
} catch (InterruptedException e) {
//ignore
}
int i = 1+1;
Thread.yield();//執行緒切換,釋放cpu使用權接著又去搶佔cpu資源
int j = 1+1;
}
使用vmstat 1 1000 檢視結果截圖如下
這次程式碼裡面沒有耗費cpu的操作,但是執行緒阻塞兩次,檢視到每秒上下文切換是3w多次,cpu使用者使用率是70%左右,系統使用率是30%左右,發現系統使用cpu也高了,通過top命令檢視cpu使用率基本在200%(我測試機是2C,打滿就是200%),截圖如下
top -Hp PID截圖如下圖,看到每個執行緒使用cpu資源很均衡
通過show-busy-java-thread 2 10 檢視堆疊,發現cpu使用很均衡,截圖如下
檢視棧的dump結果,基本都是TIMED_WAITING (parking)
這些TIMED_WAITING (parking)狀態對應的棧都是
[3] Busy(3.1%) thread(21241/0x52f9) stack of java process(18059) under user(baseuser):
"http-nio-8111-exec-404" #445 daemon prio=5 os_prio=0 tid=0x00007f4e48012800 nid=0x52f9 waiting on condition [0x00007f4e0ffc0000]
java.lang.Thread.State: TIMED_WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000d0177bd0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2078)
at java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:467)
at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:89)
at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:33)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1066)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
這個堆疊說明tomcat執行緒阻塞在從佇列獲取執行任務,又通過執行緒切換上下文那麼多,說明這個場景cpu高是消耗線上程上下文切換中。
總結:通過測試發現,執行緒上下文大量切換的時候,核心使用cpu也會變高,因為此時需要核心進行協調,進而核心消耗cpu資源也會高些(上面測試例子中核心使用cpu資源是20%~30%)。
但是有個疑問:執行緒上下文切換到底多少是多?認為需要引起注意呢?
1.4.實測fastjson解析字串對cpu資源開銷
找了個字串較大,使用fastjson進行解析
@Override
public void cpuJson() {
JSONArray jsonArray = JSON.parseArray(testJsonStr);//testJsonStr是應用啟動載入的一個較大的json字串
List<DetailVO> listVO = jsonArray.toJavaList(DetailVO.class);
}
jmeter併發100,持續10min
壓測期間top的兩張截圖,發現平均負載上升很快
vmstat結果如下圖,可以看出等待cpu執行的執行緒比較多,瓶頸在cpu上。cs表示執行緒上下文,數量並不大,2000左右。 us使用率較高。
使用show-busy-java-threads 2 5檢視結果,篩選Busy結果如下圖,說明每個執行緒使用的cpu資源均衡。
接著篩選執行緒狀態java.lang.Thread.State,大部分處於TIMED_WAITING狀態。
選取其中一個TIMED_WAITING狀態如下
[5] Busy(3.2%) thread(15329/0x3be1) stack of java process(15276) under user(baseuser):
"http-nio-8111-exec-7" #35 daemon prio=5 os_prio=0 tid=0x00007f6d7dadb800 nid=0x3be1 waiting on condition [0x00007f6d27bfa000]
java.lang.Thread.State: TIMED_WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000d015b238> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2078)
at java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:467)
at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:89)
at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:33)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1066)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
阻塞在網路IO上,感覺json解析字串並不會很消耗cpu。
通過jemeter檢視tps,結果是2778
感覺自己這個測試翻車了,莫非自己這個測試fastjson很快?但是自己通常debug到json解析方法時候執行是較慢的。
1.5.大量的sql資料處理(待測)
一次查詢大量的sql,對sql資料進行處理,這個處理過程通常不會有阻塞操作的,因此cpu一直在計算過程中,對cpu的開銷也大。模擬大量資料並處理不是很方便,留待後面測試。
1.6.死迴圈導致cpu使用率飈高
自己以前測試過對於while(tue)無限迴圈,直接會導致一顆cpu打滿(有4C,這樣情況是實際cpu使用率是25%),但是現實是我們程式碼是不會這樣寫的,只是會有些異常情況導致進入while(true)死迴圈,這樣情況在寫程式碼過程中很難發現的。
比如下面程式碼
public byte[] readBytes(int length) throws IOException {
if ((this.socket == null) || (!this.socket.isConnected())) {
throw new IOException("++++ attempting to read from closed socket");
}
byte[] result = null;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
if (this.recIndex >= length) {
bos.write(this.recBuf, 0, length);
byte[] newBuf = new byte[this.recBufSize];
if (this.recIndex > length) {
System.arraycopy(this.recBuf, length, newBuf, 0, this.recIndex - length);
}
this.recBuf = newBuf;
this.recIndex -= length;
} else {
int totalread = length;
if (this.recIndex > 0) {
totalread -= this.recIndex;
bos.write(this.recBuf, 0, this.recIndex);
this.recBuf = new byte[this.recBufSize];
this.recIndex = 0;
}
}
int readCount = 0;
while (totalread > 0) {
if ((readCount = this.in.read(this.recBuf)) > 0) {//如果結果為0,則進入while死迴圈
if (totalread > readCount) {
bos.write(this.recBuf, 0, readCount);
this.recBuf = new byte[this.recBufSize];
this.recIndex = 0;
} else {
bos.write(this.recBuf, 0, totalread);
byte[] newBuf = new byte[this.recBufSize];
System.arraycopy(this.recBuf, totalread, newBuf, 0, readCount - totalread);
this.recBuf = newBuf;
this.recIndex = (readCount - totalread);
}
totalread -= readCount;
}
}
}
對於this.in.read(this.recBuf))來說,如果連線狀態是CLOSED_WAIT狀態,讀取返回就是0,這樣就演變成了while(true)死迴圈,直接導致一顆cpu就一直是100%。這樣情況在網路不好的情況下可能出現,但是自己暫時沒發模擬到。以後看如何模擬到再寫下這個例子。
1.7.full gc導致cpu 100%(待測)
1.8.cpu使用率高的總結
通過前面測試,只是測試了正則表示式導致cpu100%,大量執行緒上下文切換導致cpu使用率高。日期格式化和json解析並沒有達到預期效果。本次測試更加熟悉了一些監控命令和stack,也並不是沒有成果。
cpu使用率高的根本原因是cpu一直在工作,如何判斷cpu實際是真正在幹活還是在頻繁切換呢?通過vmstat的cs指標確認。通常生產上給的硬體資源都是完全足夠的,遇到資源不夠了通常都是擴容,每臺伺服器節點的資源消耗通常是較小的。cpu使用率,排查了上下文頻繁切換,那說明程式中有對cpu資源開銷大的處理程式碼,這種情況通常是擴容解決,畢竟加機器是最快速能解決資源問題的。
寫這個記錄只是為了分析下load average而做。
2.磁碟IO分析
檢視磁碟通常是先檢視top的wa指標,如果較高(通常高與30就要注意了),接著檢視vmstat 的procs的b指標,接著具體用iostat和iotop來確定具體問題。
下面以一個每次請求都讀取檔案內容和寫入檔案內容來測試。該程式碼每次先從檔案內讀取,然後再寫入到一個新檔案到磁碟上。讀取和寫入都是阻塞動作。
private final AtomicInteger number = new AtomicInteger(1);
@Override
public void io() {
try {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
URL url = (classLoader != null ?
classLoader.getResource("test-service.2020-01-09.log") :
ClassLoader.getSystemResource("test-service.2020-01-09.log"));//test-service.2020-01-09.log有200k
String logText = IOUtils.toString(url, Charset.forName("utf8"));//讀取resources下的test-service.2020-01-09.log檔案內
File targetFile = null;
String os = System.getProperty("os.name");
if (StrUtil.containsIgnoreCase(os, "linux")) {
targetFile = new File("/data/log/allocate/test-service-"+number.getAndIncrement()+".log");
} else {
targetFile = new File("D:\\data\\allocate\\test-service-"+number.getAndIncrement()+".log");
}
boolean exists = targetFile.exists();
if (!exists) {
targetFile.createNewFile();
}
FileOutputStream out = new FileOutputStream(targetFile);
out.write(logText.getBytes("utf8"));//阻塞io操作
out.close();
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
"test-service.2020-01-09.log" + "]", ex);
}
}
在2C2G虛擬機器上測試,jmeter併發100,壓測10min,測試截圖如下:
壓測過程中,兩次間隔幾分鐘時間的top截圖如下,檢視到load average遠遠超過了cpu的核數,但是cpu的使用率卻不是很高。但是wa指標很高,說明可能磁碟IO問題。
接著用iostat -d -k -x 1檢視磁碟IO,截圖如下,發現%util指標都達到了100%,平均每次裝置I/O操作的等待時間await達到了500多ms,確定到確實是磁碟IO壓力太大導致,具體是哪個執行緒/程序的操作對導致的磁碟的讀/寫壓力大的呢?下面使用iotop命令檢視
通過iotop檢視具體是哪些執行緒使用的磁碟IO資源大,截圖如下,實際讀取速率是60 k/s左右,寫入是73.48m/s,說明是寫操作導致的磁碟IO壓力大。具體的執行緒就是TID,檢視DISK_WRITE列,比如看到執行緒1631寫入磁碟的速率是914.39k/s
iotop命令按p切換到程序模式,確實是自己這個java應用,看到磁碟寫入速率是96.59m/s
這裡忘記使用jstack檢視執行緒堆疊了,檢視執行緒堆疊結果執行緒應該是RUNNABLE狀態,堆疊都在io()方法上。
測試完畢檢視jemter的聚合報告如下:tps是290,吞吐量確實不高,瓶頸在磁碟IO上。
該例子證明磁碟IO會對load average有直接的影響。
解決磁碟IO瓶頸做飯
1.對叢集擴容,這個效果最顯著,成本也最高,不過實際工作中通常都是擴容。
2.減少磁碟IO操作,比如減少日誌等列印,檔案的寫入。
3.如果必須要進行檔案寫入,考慮使用記憶體對映,比如java.nio.channels.FileChannel#transferTo(long, long, WritableByteChannel)
3.網路IO阻塞分析
那麼網路IO阻塞會對load average造成影響嗎?會導致load average飈高嗎?
測試下網路阻塞io對load average的影響,測試程式碼如下:
//每次請求都會連線目標伺服器的80埠,然後讀取超時(因為目標伺服器的nginx並不返回資料)
@Override
public void socketIO() {
Socket socket = new Socket();
try {
socket.setSoTimeout(60000);
socket.setTcpNoDelay(true);
socket.connect(new InetSocketAddress("xxx.xxx.xxx.xxx", 80));//目標ip機器上啟動個nginx
InputStream in = socket.getInputStream();
byte[] b = new byte[1024];
int read = in.read(b);//網路io阻塞,阻塞60s後超時丟擲異常。
} catch (IOException e) {
log.error("讀取超時,異常{}", e.getMessage());
} finally {
try {
socket.close();
} catch (IOException e) {
log.error("socket關閉異常", e.getMessage());
}
}
}
使用jmeter壓測100併發,壓測10min,測試過程系統監控截圖如下:
測試過程檢視top命令結果
說明cpu和wa即磁碟io都沒壓力,而且負載很低。
通過jstack -l PID檢視,執行緒狀態基本處於java.lang.Thread.State: RUNNABLE
,具體堆疊如下
"http-nio-8111-exec-372" #413 daemon prio=5 os_prio=0 tid=0x00007ff6a4072000 nid=0x166b runnable [0x00007ff668d9d000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:170)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at java.net.SocketInputStream.read(SocketInputStream.java:127)
at org.allocate.memory.allocate.service.impl.AllocateServiceImpl.socketIO(AllocateServiceImpl.java:178)
at org.allocate.memory.allocate.controller.AllocateController.socketIO(AllocateController.java:56)
//省略其它堆疊
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1038)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:897)
//省略其它堆疊
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:834)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1415)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
- locked <0x00000000ae7e1638> (a org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
Locked ownable synchronizers:
- <0x00000000aca06880> (a java.util.concurrent.ThreadPoolExecutor$Worker)
執行緒狀態是可執行狀態RUNNABLE,但是執行緒實際並沒有消耗cpu,因為執行緒阻塞了。具體可以檢視我的筆記 java執行緒生命週期
說明網路IO阻塞並不會對linux的平均負載造成影響。
4.linux系統監控命令
top
top命令截圖解釋
top命令截圖如下:
按照圖中的紅色來說
1:任務佇列資訊
top - 15:29:10
當前時間
up 117days, 5:04,
是 系統執行時間 格式為時:分
1 user,
表示當前只有一個使用者登入
load average:0.18, 0.11, 0.04
, 系統平均負載。 分別為 1分鐘、5分鐘、15分鐘前到現在的平均值。 這個指標重點關注
2:程序資訊
Tasks: 176 total,
當前總共176個程序
1 running,
1個在執行中的程序
175 sleeping,
175個休眠的程序
0 stopped,
0個停止程序
0 zombie
0個殭屍程序,重點關注,有殭屍程序了需要注意
3:cpu資訊,顯示所有cpu需要top後按數字鍵1
顯示2個說明是2個cpu,有多個cpu就顯示多個。
us 使用者空間佔用CPU百分比。 重點關注,應用使用的cpu指標
sy 核心空間佔用CPU百分比。 重點關注,使用率高,說明可能是執行緒上下文切換過多
ni 使用者程序空間內改變過優先順序的程序佔用CPU百分比。 暫時沒關注過,不知道用處
id idle的縮寫,空閒cpu。
wa 等待輸入輸出的CPU時間百分比。磁碟io的監控指標。重點關注,磁碟io的指標,該值越大,說明磁碟的io壓力越大。
hi si st,沒關注過,不知道用處。忽略
4:實體記憶體使用情況
total 是物理空間總大小,單位是bytes
free 剩餘物理空間大小
used 已使用的物理空間大小
buff/cache 用作核心快取的記憶體量
檢視記憶體也通常使用free -h
linux使用記憶體是儘可能都使用了,把檔案快取到記憶體(buff/cache),這樣可以快速使用,檢視記憶體可用,並不是看free,要使用free+buff/cache專案。
buff/cache緩衝的交換區總量,即記憶體中的內容被換出到交換區,而後又被換入到記憶體,但使用過的交換區尚未被覆蓋,該數值即為這些內容已存在於記憶體中的交換區的大小。相應的記憶體再次被換出時可不必再對交換區寫入。
計算可用記憶體數有一個近似的公式: free + buff/cache
5:交換空間使用情況
如果有使用了,說明可能使用到了交換空間,這樣會影響應用速度。應該關注,我這裡顯示為0是關閉了交換空間。
6:程序資訊
PID 即程序的id 通過ps -fe |grep XXX檢視
USER 程序所屬的使用者
PR 程序優先順序,通常不用關注
NI nice值。負值表示高優先順序,正值表示低優先順序
VIRT 程序使用的虛擬記憶體總量,單位kb。VIRT=SWAP+RES
RES 程序常駐記憶體大小,單位kb。RES=CODE+DATA。通常可簡單認為該值就表示當前程序使用的實體記憶體大小。
SHR 共享記憶體大小,單位kb
S 程序狀態。D=不可中斷的睡眠狀態 R=執行 S=睡眠 T=跟蹤/停止 Z=殭屍程序
%CPU% 上次更新到現在的CPU時間佔用百分比。簡單認為程序使用cpu的百分比
$MEM 程序使用的實體記憶體百分比
TIME+ 程序使用的CPU時間總計,單位1/100秒
COMMAND 命令名/命令列
top命令常用引數
top顯示後,輸入M按照記憶體排序,輸入P按照cpu排序
top -Hp PID 按照執行緒維度顯示,常用來分析cpu開銷高的執行緒
vmstat
vmstat用法
vmstat用法格式:vmstat [選項引數] [數字] [數字]
選項引數:
1) -d: 顯示磁碟相關統計資訊。
2) -a: 顯示活躍和非活躍記憶體
3) -f: 顯示從系統啟動至今的fork數量。
4) -p: 顯示指定磁碟分割槽統計資訊
5) -s: 顯示記憶體相關統計資訊及多種系統活動數量。
6) -m: 顯示slabinfo
vmstat常用命令
通常vmstat工具的使用是通過兩個數字引數來完成的,第一個引數是取樣的時間間隔數,單位是秒,第二個引數是取樣的次數,如:
如果使用vmstat 1 表示每秒採集一次,直至使用者ctrl+c手動關閉該命令。
vmstat採集結果解釋
procs 程序
r 即running,等待執行的任務數,展示了正在執行和等待cpu資源的任務個數。當這個值超過了cpu個數,就會出現cpu瓶頸。重點關注
b 即blocked,等待IO的程序數量。重點關注
---memory---
swpd 正在使用虛擬的記憶體大小,單位k
free 空閒記憶體大小
buff 已用的buff大小,對塊裝置的讀寫進行緩衝
cache 已用的cache大小,檔案系統的cache
---swap---
si 每秒從交換區寫入記憶體的大小(單位:kb/s),縮寫是swap input。重點關注
so 每秒從記憶體寫到交換區的大小(單位:kb/s), 縮寫是swap output。重點關注
注意:記憶體夠用的時候,這2個值都是0,如果這2個值長期大於0時,系統性能會受到影響,磁碟IO和CPU資源都會被消耗。有些人看到空閒記憶體(free)很少的或接近於0時,就認為記憶體不夠用了,不能光看這一點,還要結合si和so,如果free很少,但是si和so也很少(大多時候是0),那麼不用擔心,系統性能這時不會受到影響的。因為linux有快取功能,buff/cache也是表示是可以使用的實體記憶體。
---io---
bi 每秒讀取的塊數(讀磁碟),塊裝置每秒接收的塊數量,單位是block,這裡的塊裝置是指系統上所有的磁碟和其他塊裝置,現在的Linux版本塊的大小為1024bytes。重點關注
bo 每秒寫入的塊數(寫磁碟),塊裝置每秒傳送的塊數量,單位是block。重點關注
注意:隨機磁碟讀寫的時候,這2個值越大(如超出1024k),能看到CPU在IO等待的值也會越大。
---system---
in 每秒中斷數,包括時鐘中斷
cs 每秒上下文切換數。重點關注
注意:in和cs這兩個值越大,會看到由核心消耗的cpu時間sy會越多,說明每秒執行緒上下文切換次數越多,說明cpu實際用來真正工作實際少了,都花費在了切換上。那麼可以考慮減少執行緒數目。什麼時候回進行切換呢?比如導致執行緒的阻塞動作都會進行切換,比如Thread.sleep,Thread.yield,Thread.join,synchronized同步程式碼塊,Object.wait,LockSupport.park,網路呼叫(包括資料庫呼叫)。
---cpu---
us 使用者程序執行消耗cpu時間(user time),us的值比較高時,說明使用者程序消耗的cpu時間多,但是如果長期超過50%的使用,那麼我們就該考慮優化程式了。重點關注
sy 系統程序消耗cpu時間(system time),sy的值過高時,說明系統核心消耗的cpu資源多,說明是執行緒上下文切換過多。這裡us + sy的參考值為80%,如果us+sy 大於 80%說明可能存在CPU不足。重點關注
id 空閒時間(包括IO等待時間),一般來說 us+sy+id=100
wa 等待IO時間,wa過高時,說明io等待比較嚴重,這可能是由於磁碟大量隨機訪問造成的,也有可能是磁碟的頻寬出現瓶頸。重點關注
st 虛擬機器偷取的CPU時間的百分比 。沒用過,忽略
如何認為到系統的cpu和io達到了瓶頸呢?什麼指標可以分析呢?
通過vmstat判斷系統瓶頸:
cpu瓶頸:
1.如果id持續為0且sy是us的兩倍則系統則面臨著CPU資源的短缺。
2.如果proce中的r持續高於cpu核數,說明有較多的程序在等待cpu資源,可能是有些對cpu開銷大的程序在佔用cpu,也可能是cpu資源不足。
IO瓶頸:
1.讀寫IO,bi bo增多超過2000
2.cpu等待時間用於 IO等待,wa增加 超過20
3.sy 系統呼叫時間長,IO操作頻繁會導致上下文切換增加 sy>30%
4.wa io等待時間長
iowait% <20% 良好
iowait% <35% 一般
iowait% >50%
5.進一步使用iostat觀察
vmstat -d顯示磁碟相關統計資訊
以sda磁碟為例
---reads---:總計讀取了6411次,合併讀請求23次,讀取扇區(sectors)次數是464053次,每ms讀取次數是5257bytes資料。
---writes---:總計寫了3976次,合併寫請求426次,寫扇區是41075次,每ms寫入129159bytes資料。
---IO---: cur和sec是什麼含義呢? 沒找到資料
個人認為這個引數可以用來判斷磁碟是否壞了。實際並沒使用過。
iostat & iotop
iostat命令解釋
iostat安裝命令apt/yum install sysstat
來源http://linux.51yip.com/search/iostat
iostat 可以提供更豐富的IO效能狀態資料
iostat提供幾個用於定製輸出的開關。最有用的有:
-c 只顯示CPU行
-d 顯示裝置(磁碟)使用狀態
-k 以千位元組為單位顯示磁碟輸出
-t 在輸出中包括時間戳
-x 在輸出中包括擴充套件的磁碟指標
rrqm/s: 每秒進行 merge 的讀運算元目。
wrqm/s: 每秒進行 merge 的寫運算元目。
r/s: 每秒完成的讀 I/O 裝置次數。
w/s: 每秒完成的寫 I/O 裝置次數。
rsec/s: 每秒讀扇區數。read-sectors sectors扇區
wsec/s: 每秒寫扇區數。write-sectors
rkB/s: 每秒讀K位元組數。是 rsect/s 的一半,因為每扇區大小為512位元組。(需要計算)
wkB/s: 每秒寫K位元組數。是 wsect/s 的一半。(需要計算)
avgrq-sz: 平均每次裝置I/O操作的資料大小 (扇區)。
avgqu-sz: 平均I/O佇列長度。
await: 平均每次裝置I/O操作的等待時間 (毫秒)。 重點關注
svctm: 平均每次裝置I/O操作的服務時間 (毫秒)。如果svctm的值與await很接近,表示幾乎沒有I/O等待,磁碟效能很好
%util: 一秒中有百分之多少的時間用於 I/O 操作。 重點關注
關於Merge的解釋:當系統呼叫需要讀取資料的時 候,VFS將請求發到各個FS,如果FS發現不同的讀取請求讀取的是相同Block的資料,FS會將這個請求合併Merge
await:每一個IO請求的處理的平均時間(單位是毫秒)。這裡可以理解為IO的響應時間,一般地系統IO響應時間應該低於5ms,如果大於10ms就比較大了。
%util:在統計時間內所有處理IO時間,除以總共統計時間。例如,如果統計間隔1秒,該 裝置有0.8秒在處理IO,而0.2秒閒置,那麼該裝置的%util = 0.8/1 = 80%,所以該引數暗示了裝置的繁忙程度。一般地,如果該引數是100%表示裝置已經接近滿負荷運行了(當然如果是多磁碟,即使%util是100%,因 為磁碟的併發能力,所以磁碟使用未必就到了瓶頸)。
iostat常用命令引數組合
iostat -d -k 1 5 結果解釋
Device表示裝置,有4塊磁碟sda,sdb,dm-0,dm-1
tps是每塊磁碟的IO tps,即磁碟每秒傳輸次數
KB_read/s 每秒從該磁碟的讀取速率
KB_wrtn/s 每秒寫入該磁碟的速率
kB_read 累計從該磁碟讀取的總量
kB_wrtn 累計寫入該磁碟的總量
這個命令並不能看出io的平均,需要加-x引數
iostat -d -k -x 1 5
檢視裝置使用率(%util)、響應時間(await) iostat -d -k -x 1 5
,其中1 5分別是間隔時間和總計執行次數, -d 後面可跟磁碟比如sda,只顯示sda磁碟:iostat -d sda -k -x 1 5
rrqm/s: 每秒進行 merge 的讀運算元目。read-request-merg的縮寫
wrqm/s: 每秒進行 merge 的寫運算元目。write-request-merg的縮寫
r/s: 每秒完成的讀 I/O 裝置次數。
w/s: 每秒完成的寫 I/O 裝置次數。
rkB/s: 每秒讀K位元組數。是 rsect/s 的一半,因為每扇區大小為512位元組。(需要計算)
wkB/s: 每秒寫K位元組數。是 wsect/s 的一半。(需要計算)
avgrq-sz: 平均每次裝置I/O操作的資料大小 (扇區)。average-request-size
avgqu-sz: 平均I/O佇列長度。average-queue-size
await: 簡單理解為IO的響應時間(ms)。重點關注
svctm: 平均每次裝置I/O操作的服務時間 (毫秒)。
%util: 一秒中有百分之多少的時間用於 I/O 操作。重點關注,該值越高,說明磁碟io壓力越大。
iostat總結:
iostat 統計的是通用塊層經過合併(rrqm/s, wrqm/s)後,直接向裝置提交的IO資料,可以反映系統整體的IO狀況,但是有以下2個缺點:
1 距離業務層比較遙遠,跟程式碼中的write,read不對應(由於系統預讀 + pagecache + IO排程演算法等因素, 也很難對應)
2 是系統級,沒辦法精確到程序,比如只能告訴你現在磁碟很忙,但是沒辦法告訴你是哪個程序在忙,在忙什麼?
因此需要使用到iotop命令,該命令可以確定是哪個程序/執行緒對磁碟的開銷最大。
iotop命令解釋
安裝iotop工具: apt/yum install iotop -y
iotop是一個用來監視磁碟I/O使用狀況的 top 類工具,可監測到哪一個程式使用的磁碟IO的資訊
iotop 顧名思義, io版的top,該命令可以按程序統計IO狀況,因此作為iostat的補充,可以解決以下2個問題:
1.當前系統哪些程序在佔用IO,百分比是多少?
2.佔用IO的程序是在讀?還是在寫?讀寫量是多少?
用法:iotop [OPTIONS]
--version #顯示版本號
-h, --help #顯示幫助資訊
-o, --only #顯示程序或者執行緒實際上正在做的I/O,而不是全部的,可以隨時切換按o
-b, --batch #執行在非互動式的模式
-n NUM, --iter=NUM #在非互動式模式下,設定顯示的次數,
-d SEC, --delay=SEC #設定顯示的間隔秒數,支援非整數值
-p PID, --pid=PID #只顯示指定PID的資訊
-u USER, --user=USER #顯示指定的使用者的程序的資訊
-P, --processes #只顯示程序,一般為顯示所有的執行緒
-a, --accumulated #顯示從iotop啟動後每個執行緒完成了的IO總數
-k, --kilobytes #以千位元組顯示
-t, --time #在每一行前新增一個當前的時間
在執行iotop命令後按相應鍵位:
使用left和right改變排序(方向鍵改變排序列),還可使用以下命令:
r:反向排序,
o:切換至選項--only,
p:切換至--processes選項,
a:切換至--accumulated選項
q:退出
i:改變執行緒的優先順序
iotop常用命令引數
直接執行iotop即可,截圖如下
前兩行字面意思很簡單,就是所有磁碟的讀寫速率
TID:執行緒id,按p可轉換程序pid
PRIO:優先順序
DISK READ:磁碟讀取速率
DISK WRITE:磁碟寫取速率
SWAPIN:swap交換百分比
IO>:IO等待所佔用百分比。重點關注,該值越高,說明當前執行緒/程序對磁碟的壓力越大。
COMMAND:執行緒/程序詳細資訊
磁碟IO的排查思路總結:
1.通常給伺服器做壓測時,容易碰到磁碟IO讀取瓶頸,持續高的IO會導致磁碟讀取密集讀寫磁碟IO成為短板,程式執行過慢;常見的IO操作有列印日誌、操作資料庫,網路呼叫。
2.使用top命令檢視%wa指標,該項閾值表示io waiting等待磁碟磁碟寫入完成的時間,一般不能高於30%,假如該值過大且持續較久,說明遇到了磁碟I/O瓶頸,需要對應用進行優化,或對伺服器資源進行升級。
3.iostat -d -k -x 1輸出磁碟相關統計資訊,主要關注await,%util指標。
4.iotop定位負載來源於那個程序/執行緒。
5.通過jstack列印堆疊,分析iotop中開銷IO資源高的幾個執行緒。