Linux下Java进程性能调优

最近刚刚对Linux下的Java进程进行了性能调优,对用到的工具方法进行总结如下:

CPU/内存

分析CPU、内存最重要的工具就是top,整体把握系统的CPU、内存消耗

%Cpu(s):  0.3 us,  0.2 sy,  0.0 ni, 99.5 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  7864876 total,  4528528 free,  1697828 used,  1638520 buff/cache
KiB Swap:        0 total,        0 free,        0 used.  5826908 avail Mem 

主要关注几个指标:

  • us:系统整体的CPU利用率,如果长时间保持在80-90%,那么就应该考虑优化或者扩容
  • avail Mem:系统整体可用的内存,如果已经低于10%,那么就应该考虑优化或者扩容

如果需要对系统进行优化,首先需要考虑找到对系统资源消耗最大的应用进行优化调整,在top命令下,可以用Shift+P对CPU占用进行排序,用Shift+M对内存进行排序,一般一次对消耗前三的应用进行分析就够了,可以优化调整后再分析优化,反复进行直到达成目标。
需要注意的是,有些应用的使用模式比较特别,它是起很多个子进程完成工作,可能每个子进程消耗的量不是特别大,但是加起来就比较可观了。所以单纯统计前几个进程可能发现不了此类问题,有时候可以比较某个应用启动前后的资源消耗来判断该应用的影响。

对单个进程的CPU消耗,可以考虑使用

top -Hp pid

来看特定进程中某个线程的CPU消耗。注意使用该命令后,PID一列已经不再是进程ID,而是线程ID。

因为是对Linux下的Java进程进行优化调优,此处就以Java进程为例:
通过上述命令可以获取到占用CPU最高的线程,然后获取到Java线程信息,就可以根据线程ID查找到对应的线程,查看该线程运行的内容。获取Java线程信息有多种方式,可以采用如下命令(也可以在jvisualvm图形界面中的线程Tab中导出)。

jstack -l pid > ./xxx.tdump

比如占用CPU最高的线程号是:8426,转化为16进制为:20ea,在xxx.tdump中搜索,可以得到:

"POST /acs (1731)" #1731 prio=10 os_prio=0 tid=0x00007f3b1c11b1d0 nid=0x20ea runnable [0x00007f3a369f3000]

接下来就可以对该线程的内容进行分析,查看是否有优化空间。
分析内存的话,可以通过如下命令(也可以在jvisualvm图形界面中的监控Tab中导出)

jmap -dump:live,format=b,file=heap001  pid

可以直接在jvisualvm中分析,也可以使用Eclipse的MAT工具进行分析。推荐使用Eclipse的MAT工具,功能更丰富,可以帮助分析可疑的内存泄漏点。
Linux下Java进程性能调优_第1张图片
可以一目了然的看出可疑的内存泄漏点:有一处占用了1.4G内存,有22w多个对象。

磁盘IO

可以使用如下命令查看系统IO占用情况(如果没有,可以通过yum install sysstat安装)

iostat -x -d 2

输出如下:

Linux 3.10.0-693.el7.x86_64 (pim) 	2019年8月28日 	_x86_64_	(4 CPU)

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
vda               0.00     0.17    0.62    3.39    35.45    49.18    42.21     0.31   77.74   24.89   87.40  16.30   6.54

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
vda               0.00     0.50    0.50   13.50     4.00   138.00    20.29     0.04    2.57   17.00    2.04   0.86   1.20

可以首先查看util指标,如果此处比较高,可能磁盘IO就是瓶颈,需要调整优化,指标详细含义参考:iostat命令详解。
可以进一步查看是哪个进程占用比较高:

pidstat -d 2

输出结果:

Linux 3.10.0-693.el7.x86_64 (pim) 	2019年08月28日 	_x86_64_	(4 CPU)

15时49分58秒   UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s  Command
15时50分00秒   997      1350      0.00      3.96      3.96  mongod
15时50分00秒    26      1639      0.00      3.96      0.00  postmaster

网络IO

可以采用如下命令查看:

sar -n DEV 2 

输出如下:

Linux 3.10.0-693.el7.x86_64 (pim) 	2019年08月28日 	_x86_64_	(4 CPU)

15时52分56秒     IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s   rxcmp/s   txcmp/s  rxmcst/s
15时52分58秒      eth0      1.00      0.00      0.18      0.00      0.00      0.00      0.00
15时52分58秒      eth1      0.00      0.00      0.00      0.00      0.00      0.00      0.00
15时52分58秒      eth2      2.50      0.00      0.11      0.00      0.00      0.00      0.00
15时52分58秒        lo     39.50     39.50      6.19      6.19      0.00      0.00      0.00

可以通过rxkB/s、 txkB/s评估带宽占用情况,其他参数参考:sar命令使用详解。

在本次调优过程中基本上磁盘IO和网络IO没有遇到瓶颈,因此就没有展开分析。

数据库

数据库涉及MongoDB和postgre。
MongoDB性能分析可以使用mongostat命令,输出如下:

insert query update delete getmore command dirty used flushes vsize   res qrw arw net_in net_out conn                time
    *0    *0     *0     *0       0     3|0  0.0% 0.2%       0 1.00G 82.0M 0|0 1|0   217b   47.4k    5 Aug 29 09:17:42.264
    *0    *0     *0      1       0     2|0  0.0% 0.2%       0 1.00G 82.0M 0|0 1|0   583b   47.0k    5 Aug 29 09:17:43.266
    *0    *0     *0     *0       0     1|0  0.0% 0.2%       0 1.00G 82.0M 0|0 1|0   157b   46.8k    5 Aug 29 09:17:44.267
    *0    *0     *0     *0       0     3|0  0.0% 0.2%       0 1.00G 82.0M 0|0 1|0   216b   47.3k    5 Aug 29 09:17:45.262
    *0    *0     *0     *0       0     1|0  0.0% 0.2%       0 1.00G 82.0M 0|0 1|0   157b   46.8k    5 Aug 29 09:17:46.263
    *0    *0     *0     *0       0     2|0  0.0% 0.2%       0 1.00G 82.0M 0|0 1|0   158b   46.9k    5 Aug 29 09:17:47.262
    *0    *0     *0      1       0     2|0  0.0% 0.2%       0 1.00G 82.0M 0|0 1|0   584b   47.0k    5 Aug 29 09:17:48.263

可以看到mongodb操作的insert、query、update、delete操作的个数。mongoDB默认的最大连接数为100(在com.mongodb.MongoClientOptions.Builder的构造函数中指定):

this.maxConnectionsPerHost = 100;

通过以上统计数据可以分析mongodb并发数是否满足要求,如果不满足,可以在初始化mongodb连接池时进行设置,例如在代码中直接配置:

        MongoClientOptions.Builder builder = new MongoClientOptions.Builder();
        builder.connectionsPerHost(500);
        MongoClientOptions options = builder.build();

        return new SimpleMongoDbFactory (new MongoClient (new ServerAddress(DB_HOST, DB_PORT), options), DB_NAME);

也可以通过mongotop分析操作最频繁的数据库,分析操作是否正常:

2019-08-29T09:24:27.882+0800	connected to: 127.0.0.1

                    ns    total    read    write    2019-08-29T09:24:28+08:00
    admin.system.roles      0ms     0ms      0ms                             
  admin.system.version      0ms     0ms      0ms                             
 ......
                    ns    total    read    write    2019-08-29T09:24:29+08:00
    admin.system.roles      0ms     0ms      0ms                             
  admin.system.version      0ms     0ms      0ms                             
......

如果某个数据库表操作特别频繁,需要考虑是否合理,是否需要缓存以减少数据库的频繁访问。
postgre可以通过如下命令统计并发的连接数:

ps -ef | grep \"postgres\" | wc -l

如果发现达到并发上限,可以通过指定数据库名称查看特定数据库的并发操作数:

ps -ef | grep \"postgres xxx\" | wc -l

其中xxx是要查看的数据库名称。通过以上数据跟数据库连接池的最大连接数进行比较分析是否属于并发度不够导致数据库操作缓慢。
也可以通过查询pg_stat_activity表分析数据库操作的统计信息,可以参考:postgresql 中的动态统计视图中的pg_stat_activity。

NOTE:特别值得一提的是需要特别关注数据库日志,如果日志级别设置比较低,比如DEBUG级别,在运行期间一些数据库中间件(比如ibatis,hibernate)会打印出大量的日志(如sql语句,操作结果,连接等等),而大量日志会导致大量的等待:

   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x00000005b47d2c38> (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.ArrayBlockingQueue.put(ArrayBlockingQueue.java:353)
	at ch.qos.logback.core.AsyncAppenderBase.put(AsyncAppenderBase.java:160)
	at ch.qos.logback.core.AsyncAppenderBase.append(AsyncAppenderBase.java:148)
	at ch.qos.logback.core.UnsynchronizedAppenderBase.doAppend(UnsynchronizedAppenderBase.java:84)
	at ch.qos.logback.core.spi.AppenderAttachableImpl.appendLoopOnAppenders(AppenderAttachableImpl.java:51)
	at ch.qos.logback.classic.Logger.appendLoopOnAppenders(Logger.java:270)
	at ch.qos.logback.classic.Logger.callAppenders(Logger.java:257)
	at ch.qos.logback.classic.Logger.buildLoggingEventAndAppend(Logger.java:421)
	at ch.qos.logback.classic.Logger.filterAndLog_0_Or3Plus(Logger.java:383)
	at ch.qos.logback.classic.Logger.log(Logger.java:765)
	at org.apache.ibatis.logging.slf4j.Slf4jLocationAwareLoggerImpl.debug(Slf4jLocationAwareLoggerImpl.java:61)
	at org.apache.ibatis.logging.slf4j.Slf4jImpl.debug(Slf4jImpl.java:74)
	at org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:159)
	at org.apache.ibatis.logging.jdbc.ConnectionLogger.invoke(ConnectionLogger.java:53)
	at com.sun.proxy.$Proxy201.prepareStatement(Unknown Source)
	at org.apache.ibatis.executor.statement.PreparedStatementHandler.instantiateStatement(PreparedStatementHandler.java:86)
	at org.apache.ibatis.executor.statement.BaseStatementHandler.prepare(BaseStatementHandler.java:88)
	at org.apache.ibatis.executor.statement.RoutingStatementHandler.prepare(RoutingStatementHandler.java:59)
	at org.apache.ibatis.executor.SimpleExecutor.prepareStatement(SimpleExecutor.java:86)
	at org.apache.ibatis.executor.SimpleExecutor.doUpdate(SimpleExecutor.java:49)
	at org.apache.ibatis.executor.BaseExecutor.update(BaseExecutor.java:117)
	at org.apache.ibatis.executor.CachingExecutor.update(CachingExecutor.java:76)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:198)

因此导致的结果就是数据库操作很慢,常常需要10几秒,甚至更长,但是无论是查看并发的连接数还是磁盘IO都不高,就是wait在数据库日志的锁上,导致等待。

代码调优

本次调优的场景是高并发场景下的,因此大量的优化分析是高并发场景下的阻塞问题。分析的主要方法是利用前面导出的线程栈,分析线程栈:
统计总的线程数,找到自己写的代码,统计调用该段代码的线程数量,针对top3的代码进行分析,反复进行,直到代码不再阻塞,系统资源成为瓶颈。

  1. 找到有问题的线程:统计某个线程栈,搜索“java.lang.Thread.State:”关键字大致统计总的线程数量,然后根据线程名字统计相应线程的数量(这要求编写代码时要规范,要给线程池,线程进行命名,不能采用默认名字),比如发现某类线程占到了60%以上,那就应该重点对该线程进行分析,或者根据统计情况找到占有率靠前的2-3类进行分析。一般此类高并发场景下的应用,如果代码有问题,必然有大量线程存在等待,阻塞现象。
  2. 找到对应的代码:同样是采用统计分析的方法。针对有问题的线程,找到调用应用代码的地方,进行搜索。一般应用自己编写的代码,包名一般比较特殊,还是比较好搜的。统计完成后,同样对靠前的2-3处进行分析,一般可以找到优化的点。

优化过程中遇到的问题:

  • 并发度不够,例如SOAP解析实例个数太少,高并发下成为瓶颈,参考JDK SOAP解析阻塞问题分析及解决:
  • new实例过多:虽然单个消耗不多,但并发度高的情况下,消耗还是比较可观,再加上内部可能存在的IO,锁,更是可能成为瓶颈,如果可能的话,单实例(或者类的静态成员)是一个好的解决办法。
  • 必要的时候再创建实例:原因同上。有的时候为了简便,往往定义为类成员,但实际上可以定位为local变量,避免不必要的实例构造。

参考

  • iostat命令详解
  • sar命令使用详解
  • postgresql 中的动态统计视图中的pg_stat_activity
  • JDK SOAP解析阻塞问题分析及解决

你可能感兴趣的:(Java,Linux,性能调优,Java,linux)