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打满)

image-20201203135047778

压测一段时间后通过top命令查看,发现cpu被打满了,平均负载已经超过了2*0.7

image-20201203140227795

1.1.1.使用传统方式top -Hp PID 和jstack PID确定开销cpu最高的线程

step1:间隔几秒多次执行jstack PID >> regex ,多次执行是为了查出共性

step2:top -Hp PID 查看并记录几个线程ID,如下图

image-20201203140601116

image-20201203140701887

上图中的1443,1458,20023就是线程ID,转换为16进制数

printf "%x\n" 20023结果为4e37,在jstack结果文件regex1问题内进行搜索,比如执行grep -A 50 4e37 regex1 查询4e37后面50行

image-20201203141118813

在确定cpu消耗上,只查看java.lang.Thread.State: RUNNABLE的堆栈,这个符合,但是这个堆栈是阻塞在socket读取上,并不会消耗CPU的,因此这个忽略(这也是为什么要多次执行jstack的原因,多次执行找能找出共性)

printf "%x\n" 1443结果为5a3,在jstack结果文件regex1问题内进行搜索,比如执行grep -A 100 5a3 regex1 查询4e37后面50行

image-20201203141656962

image-20201203141806550

发现业务方法内的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如图

image-20201203161401349

top -Hp PID结果如下图

image-20201203161712736

发现线程对cpu的开销比较均衡,再通过查看show-busy-java-threads 2 10 > format.log查看结果,在format.log文件内搜索Busy(,截图如下:

image-20201203162822526

每个线程对cpu的消耗是差不多的,和通过top -Hp PID结果是相同的。接着搜索farmat.log文件内的java.lang.Thread.State:查看线程整体状态,发现基本处于TIMED_WAITING (parking),截图如下,这个状态是由于执行了有超时时间限制的LockSupport.parkXXX操作

image-20201203163100463

具体的堆栈如下

image-20201203163236228

这个堆栈说明tomcat线程阻塞在从队列获取执行任务。继而说明大多数线程都是处于阻塞状态,cpu频繁处于线程切换状态(线程在cpu执行,只有在阻塞情况下,cpu才会切换到其它线程执行),通过vmstat 2 10000查看cs每秒上下文切换数指标如下:

image-20201203165952664

每秒线程要切换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 查看结果截图如下

image-20201203172002906

这次代码里面没有耗费cpu的操作,但是线程阻塞两次,查看到每秒上下文切换是3w多次,cpu用户使用率是70%左右,系统使用率是30%左右,发现系统使用cpu也高了,通过top命令查看cpu使用率基本在200%(我测试机是2C,打满就是200%),截图如下

image-20201203172301149

top -Hp PID截图如下图,看到每个线程使用cpu资源很均衡

image-20201203182215714

通过show-busy-java-thread 2 10 查看堆栈,发现cpu使用很均衡,截图如下

image-20201203175756192

查看栈的dump结果,基本都是TIMED_WAITING (parking)

image-20201203182326632

这些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

image-20201203144832822

压测期间top的两张截图,发现平均负载上升很快

image-20201203143643062

image-20201203143858427

vmstat结果如下图,可以看出等待cpu执行的线程比较多,瓶颈在cpu上。cs表示线程上下文,数量并不大,2000左右。 us使用率较高。

image-20201205182938024

使用show-busy-java-threads 2 5查看结果,筛选Busy结果如下图,说明每个线程使用的cpu资源均衡。

image-20201205183237816

接着筛选线程状态java.lang.Thread.State,大部分处于TIMED_WAITING状态。

image-20201205183326786

选取其中一个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

image-20201205184445606

感觉自己这个测试翻车了,莫非自己这个测试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问题。

image-20201205203838814

image-20201205204346762

接着用iostat -d -k -x 1查看磁盘IO,截图如下,发现%util指标都达到了100%,平均每次设备I/O操作的等待时间await达到了500多ms,确定到确实是磁盘IO压力太大导致,具体是哪个线程/进程的操作对导致的磁盘的读/写压力大的呢?下面使用iotop命令查看

image-20201205203904850

image-20201205204714510

通过iotop查看具体是哪些线程使用的磁盘IO资源大,截图如下,实际读取速率是60 k/s左右,写入是73.48m/s,说明是写操作导致的磁盘IO压力大。具体的线程就是TID,查看DISK_WRITE列,比如看到线程1631写入磁盘的速率是914.39k/s

image-20201205203933208

iotop命令按p切换到进程模式,确实是自己这个java应用,看到磁盘写入速率是96.59m/s

image-20201205204029735

这里忘记使用jstack查看线程堆栈了,查看线程堆栈结果线程应该是RUNNABLE状态,堆栈都在io()方法上。

测试完毕查看jemter的聚合报告如下:tps是290,吞吐量确实不高,瓶颈在磁盘IO上。

image-20201205204817120

该例子证明磁盘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命令结果

image-20201205211409429

说明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命令截图如下:

image-20201205153213583

按照图中的红色来说

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

image-20201205155146548

linux使用内存是尽可能都使用了,把文件缓存到内存(buff/cache),这样可以快速使用,查看内存可用,并不是看free,要使用free+buff/cache项目。

buff/cache缓冲的交换区总量,即内存中的内容被换出到交换区,而后又被换入到内存,但使用过的交换区尚未被覆盖,该数值即为这些内容已存在于内存中的交换区的大小。相应的内存再次被换出时可不必再对交换区写入。

计算可用内存数有一个近似的公式: free + buff/cache

5:交换空间使用情况

​ 如果有使用了,说明可能使用到了交换空间,这样会影响应用速度。应该关注,我这里显示为0是关闭了交换空间。

6:进程信息

image-20201205154308405

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工具的使用是通过两个数字参数来完成的,第一个参数是采样的时间间隔数,单位是秒,第二个参数是采样的次数,如:

image-20201205164906283

如果使用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显示磁盘相关统计信息

image-20201205175353719

以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 结果解释

image-20201205195348414

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

image-20201205195708221

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即可,截图如下

image-20201205201834442

前两行字面意思很简单,就是所有磁盘的读写速率

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资源高的几个线程。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM