最近刚刚对Linux下的Java进程进行了性能调优,对用到的工具方法进行总结如下:
分析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
主要关注几个指标:
如果需要对系统进行优化,首先需要考虑找到对系统资源消耗最大的应用进行优化调整,在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工具,功能更丰富,可以帮助分析可疑的内存泄漏点。
可以一目了然的看出可疑的内存泄漏点:有一处占用了1.4G内存,有22w多个对象。
可以使用如下命令查看系统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
可以采用如下命令查看:
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的代码进行分析,反复进行,直到代码不再阻塞,系统资源成为瓶颈。
优化过程中遇到的问题: