性能调优

 

  • 性能诊断工具

    • 操作系统诊断

    • Java应用诊断工具

  • 性能优化实践

    • JVM调优:GC之痛

    • 应用层调优:嗅到代码的坏味道

    • 数据库层调优:死锁噩梦

  • 总结与建议

Java应用性能优化是一个老生常谈的主题,典型的性能问题如页面响应慢,接口超时,服务器负载高,并发数低,数据库复制死锁等。尤其是在“大规模快猛”的互联网开发模式大行其道的Java应用程序性能的指向点非常多,磁盘,内存,网络I / O等系统因素,Java应用程序代码,JVM GC ,笔者根据个人经验,将Java性能优化分为4个层级:应用层,数据库层,框架层,JVM层,以1表示。

图.Java性能优化分层模型

每应用程序需要理解代码逻辑,通过Java线程栈定位有问题代码行等;数据库层次需要分析SQL,定位死锁等;框架层需要懂源代码,理解框架机制;JVM层需要对GC的类型和工作机制有深入了解,对各种JVM参数作用了然于胸。

围绕Java性能优化,有两种最基本的分析方法:现场分析法和事后分析法。现场分析法通过保留现场,再采用诊断工具分析定位。事后分析法需要通过多收集现场数据,然后立即恢复服务,同时针对收集的现场数据进行事后分析和复现。下面我们从性能诊断工具出发,分享搜狗商业平台在其中的一些案例与实践。

性能诊断工具

性能诊断一种是针对已经确定有性能问题的系统和代码进行诊断,还有一种是对预上线系统提前性能测试,确定性能是否符合上线要求。此处主要针对前者,多数可以使用各种性能压针对Java应用程序,性能诊断工具主要分为两层:OS外观和Java应用程序(包括应用程序诊断和GC诊断)。

操作系统诊断

OS的诊断主要关注的是CPU,内存,I / O三个方面。

CPU诊断

对于CPU主要关注平均负载(平均负载),CPU使用率,多个切换次数(上下文切换)。

通过top命令可以查看系统平均负载和CPU使用率,图2为通过top命令查看某系统的状态。

图.top命令示例

平均负载有三个数字:63.66,58.39,57.18,分别表示过去1分钟,5分钟,15分钟机器的负载。遵循经验,若数值小于0.7 * CPU个数,则系统工作正常;若超过这个值,甚至图2中15分钟负载已经高达57.18,1分钟负载是63.66(系统为16核),说明系统出现了负载问题,并且存在进一步升升达到CPU核数的四五倍,则系统的负载就明显偏高。高趋势,需要定位具体原因了。

通过vmstat命令可以查看CPU的切换次数,如下3所示:

图.vmstat命令示例

两者切换次数发生的场景主要有以下几种:1)时间片用完,CPU正常调度下一个任务;2)被其他优先级更高的任务抢占;3)执行任务碰到I / O一段,挂起步当前任务,切换到下一个任务;4)用户代码主动挂起当前任务让出CPU;5)多任务抢占资源,由于没有抢到被挂起;6)硬件中断。但在一个访问频度高,对多个对象连续加锁的代码块中就可能出现大量的切换,成为系统变量。。在我们系统中就曾出现log4j 1.x在出现并发下大量打印日志时,出现串联切换,大量线程中断,导致系统骨折大降级的情况,其相关代码如清单1所示,升级。到log4j 2.x才解决这个问题。

清单1. log4j 1.x同步代码片段

for(Category c = this; c != null; c=c.parent) {
 // Protected against simultaneous call to addAppender, removeAppender,…
 synchronized(c) {
 if (c.aai != null) {
 write += c.aai.appendLoopAppenders(event);
 }
 …
 }
}

记忆

从顶层操作系统角度,内存关注应用进程是否足够,可以使用-m命令查看内存的使用情况。通过top命令可以查看进程使用的虚拟内存VIRT和物理内存RES,根据公式VIRT = SWAP + RES可以推算出具体应用使用的交换分区(Swap)情况,使用交换分区过大会影响Java应用性能,可以将swappiness值调到重置小。因为对于Java应用来说,占用过多交换分区可能会影响性能,之后磁盘性能比内存慢太多。

输入输出

I / O包括磁盘I / O和网络I / O,一般情况下磁盘更容易出现I / O中断。通过iostat可以查看磁盘的读写情况,通过CPU的I / O等待可以裁剪磁盘I / O如果磁盘I / O一直处于很高的状态,则说明磁盘太慢或故障,成为了性能瓶颈,需要进行应用优化或磁盘更换。

除了常用的top,ps,vmstat,iostat等命令,还有其他Linux工具可以诊断系统问题,例如mpstat,tcpdump,netstat,pidstat,sar等。Brendan总结列出了Linux不同设备类型的性能诊断工具,如图4所示,可以参考。

图.Linux性能观察工具

Java应用诊断工具

应用代码诊断

通过一些应用程序监控警报,如果确定有问题的功能和代码,直接通过代码就可以定位;或者通过top + jstack,发现有问题的线程栈对于更复杂,逻辑更多的代码段,通过Stopwatch打印性能日志往往也可以定位大多数应用代码性能问题。

常用的Java应用诊断包括线程,变量,GC等方面的诊断。

jstack

jstack命令通常配合top使用,通过top -H -p pid定位Java进程和线程,再利用jstack -l pid转换线程栈。由于线程栈是瞬态的,因此需要多次dump,一般3次dump,一般每次隔5s就行。将top定位的Java线程pid转成16二进制,得到Java线程栈中的nid,可以找到对应的问题线程栈。

图。通过top –H -p查看运行时间嵌入Java线程

如图5所示,其中的线程24985运行时间长度,可能存在问题,转成16二进制后,通过Java线程栈找到对应的线程0x6199的栈如下,从而定位问题点,如图6所示。

图.jstack查看线程尺寸

JProfiler

JProfiler可对CPU,堆,内存进行分析,功能强大,如图7所示。同时结合压测工具,可以对代码耗时采样统计。

图。通过JProfiler进行内存分析

GC诊断

Java GC解决了程序员管理内存的风险,但GC引起了应用暂停且另一个需要解决的问题。JDK提供了一系列工具来定位GC问题,比较常用的有jstat,jmap,还有第三方工具MAT等。

统计

jstat命令可打印GC详细信息,年轻GC和完整GC次数,堆信息等。其命令格式为

jstat –gcxxx -t pid ,如图8所示。

图.jstat命令示例

映射

jmap打印Java进程堆信息jmap –heap pid。通过jmap –dump:file = xxx pid可转储堆到文件,然后通过其他工具进一步分析其堆使用情况

MAT是Java堆的分析利器,提供了直观的诊断报告,内置的OQL允许对堆进行类SQL查询,功能强大,传出的引用和传入的引用可以对对象引用追根溯源。

图.MAT示例

图9是MAT使用示例,MAT有两列显示对象大小,分别是Shallow size和Retained size,前者表示对象本身占用内存的大小,不包含其引用的对象,而是对象自己及其直接或间接引用的对象的Shallow size之和,即该对象被回收后GC释放的内存大小,一般说来关注关注大小即可。对于某些大堆(几十G)的Java应用,需要充分利用才能打开MAT 。通常本地开发机内存过小,是无法打开的,建议在线下服务器端安装图形环境和MAT,远程打开查看。或者执行mat命令生成堆索引,复制索引到本地,不过这种方式看到的堆信息有限。

为了诊断GC问题,建议在JVM参数中加上-XX:+ PrintGCDateStamps。常用的GC参数表示10。

图。常用GC参数

对于Java应用,通过top + jstack + jmap + MAT可以定位大多数应用和内存问题,可谓必备工具。有时,Java应用诊断需要参考OS相关信息,可使用一些更全面的诊断工具,例如Zabbix(集成了OS和JVM监控)等。在分布式环境中,分布式跟踪系统等基础设施也对应用性能诊断提供了有力支持。

性能优化实践

在介绍了一些常用的性能诊断工具后,下面将结合我们在Java应用调优中的一些实践,从JVM层,应用程序层以及数据库层进行案例分享。

JVM调优:GC之痛

通过观察GC日志,发现服务自启动后每一个,都可以将RMI作为内部远程调用协议,系统上线后开始出现的服务的停止响应,暂停时间由数秒到几十秒不等。一小时会出现一次Full GC。由于系统堆设置长度,Full GC一次暂停应用时间会贯通,这对在线实时服务影响穿透。经过分析,在前面的系统没有出现定期Full GC的情况下,通过公开资料,发现RMI的GDC(分布式垃圾回收,分布式垃圾收集)会启动守护线程定期执行。GC来回收远程对象,清单2中展示了其守护线程代码。

清单2.DGC守护线程源代码

private static class Daemon extends Thread {
 public void run() {
 for (;;) {
     //…
 long d = maxObjectInspectionAge();
 if (d >= l) {
    System.gc();
 d = 0;
 }
 //…
 }
     }
}

一种是通过增加-XX:+ DisableExplicitGC参数,直接替换系统GC的显示调用,但对使用NIO的系统,会有堆外部内存溢出的风险。另一种方式是通过调大-Dsun.rmi.dgc.server.gcInterval和-Dsun.rmi.dgc.client.gcInterval参数,增加Full GC间隔,同时增加参数-XX:+ ExplicitGCInvokesConcurrent,将一次完全Stop-The-World Full图调整为一次并发GC周期,减少应用暂停时间,同时对NIO应用也不会造成影响。从图11可知,调整之后的Full GC次数在3月之后明显减少。

图。完整的GC监控统计

GC调优对高并发大数据量交互的应用还是很有必要的,尤其是JVM参数通常不满足业务需求,需要进行专门调优。GC日志的解读有很多公开的资料,此处不再赘述。GC调优目标基本有三个思路:降低GC频率,可以通过增加堆空间,减少少量对象生成;降低GC暂停时间,可以通过减少堆空间,使用CMS GC算法实现;避免完整GC,调整CMS触发比例,避免促销失败和并发模式失败(老年代分配更多空间,增加GC线程数快速回收速度),减少大对象生成等。

应用层调优:嗅到代码的坏味道

从应用层代码调优入手,剖析代码效率下降的根源,足以提高Java应用性能的很好的手段之一。

某商业广告系统(采用Nginx进行负载均衡)某次日常上线后,其中有几台机器负载急剧增加,CPU使用率迅速打满。我们对线上进行了紧急回滚,并通过jmap和jstack对其中某台服务器的现场进行保存。

图。通过MAT分析粒度现场

初始现场如图12所示,根据MAT对dump数据的分析,发现最多的内存对象为byte []和java.util.HashMap  Entry对象存在循环引用。定位在该HashMap的放置过程中有可能出现了死循环问题(图示java.util.HashMap $ Entry 0x2add6d992cb8和0x2add6d992ce8的下一个引用形成循环)。查阅相关文档定位这属于类别的并发使用的场景错误://bugs.java.com/bugdatabase/view_bug.do?bug_id = 6423457),简要的说就是HashMap本身并不意味着多线程并发的特性,在多个线程同时将操作的情况下,内部进行进行扩容时会导致HashMap的内部链表形成环形结构,从而出现死循环。

针对此次上线,最大的扩展在于通过内存缓存网站数据来提升系统性能,同时使用了懒加载机制,如清单3所示。

清单3.网站数据懒加载代码

private static Map<Long, UnionDomain> domainMap = new HashMap<Long, UnionDomain>();
    private boolean isResetDomains() {
        if (CollectionUtils.isEmpty(domainMap)) {
            // 从远端 http 接口获取网站详情
            List<UnionDomain> newDomains = unionDomainHttpClient
                    .queryAllUnionDomain();
            if (CollectionUtils.isEmpty(domainMap)) {
                domainMap = new HashMap<Long, UnionDomain>();
                for (UnionDomain domain : newDomains) {
                    if (domain != null) {
                        domainMap.put(domain.getSubdomainId(), domain);
                    }
                }
            }
            return true;
        }
        return false;
    }

可以看到此处的domainMap为静态共享资源,它是HashMap类型,在多线程情况下会导致其内部链表形成环形结构,出现死循环。

通过对前端Nginx的连接和访问日志可以看到,由于在系统重启后Nginx积攒还原的用户请求,在Resin容器启动,大量用户请求涌入应用系统,多个用户同时进行网站数据的请求和初始化工作,导致HashMap出现并发问题。在定位故障原因后解决方法则比较简单,主要的解决方法有:

(1)采用ConcurrentHashMap或同步块的方式解决上述并发问题;

(2)在系统启动前完成网站缓存加载,删除懒加载等;

(3)采用分布式缓存替换本地缓存等。

对于坏代码的定位,除常规意义上的代码审查外,采用例如MAT之类的工具也可以在一定程度上对系统性能指标点进行快速定位。但是某些与特定场景绑定或业务数据绑定的情况,,却需要辅助代码走查,性能检测工具,数据模拟甚至在线引用流等方式才能最终确认性能问题的出处。以下是我们总结的一些不良代码可能的一些特征,供大家参考:

(1)代码清晰性差,无基本编程规范;

(2)对象生成过多或生成大对象,内存数量等;

(3)IO流操作过多,或者忘记关闭;

(4)数据库操作过多,事务过长;

(5)同步使用的场景错误;

(6)循环迭代耗时操作等。

数据库层调优:死锁噩梦

对于大部分Java应用来说,与数据库进行交互的场景非常普遍,尤其是OLTP这种对于数据一致性要求较高的应用,数据库的性能会直接影响到整个应用的性能。主的广告发布和投放平台,进行物料的实时性和一致性都有极高的要求,我们在关系型数据库优化方面也积累了一定的经验。

对于广告物料库而言,较高的操作交替度(特别是通过批量物料工具操作)很极易造成数据库的死锁情况发生,其中一个比较典型的场景是广告物料调价。客户经常会取代的对物料的估计进行调整,从而间接给数据库系统造成的负载压力,也加剧了死锁发生的问题。下面以搜狗商业平台某广告系统广告物料调价的情况进行说明。

某商业广告系统某天访问量突增,造成系统负载升高以及数据库重复死锁,死锁语句如图13所示。

图13.死锁语句

其中,groupdomain表上索引为idx_groupdomain_accountid(accountid),idx_groupdomain_groupid(groupid),primary(groupdomainid)三个单索引结构,采用Mysql innodb引擎。

此场景发生在更新组预期时,场景中存在着组,组块(groupindus表)和组网站(groupdomain表)。当更新组发生时,若组行业使用规模的组别(通过isusegroupprice标示,若为1则使用组标记)。同时若组网站替换使用组行业校正(通过isuseindusprice指示,若为1则使用组行业校正)时,也需要同时更新其组网站标记。由于每个组下面最大可以有3000个从上面发生死锁的问题可以看到,事务1和事务2均选择了idx_groupdomain_accountid的单列索引。根据Mysql innodb引擎加锁的特点,在一次事务中只会选择一个索引使用,而且如果再次使用二级索引进行加锁后,会尝试将主键索引进行加锁。进一步分析可知事务1在请求事务2持有的idx_groupdomain_accountid二级索引加锁(加锁范围“ space id 5726 page no 8658 n bits 824 index”),但是事务2已获得该二级索引(“ space id 5726 page no 8658 n bits 824 index”)上所加的锁,在等待请求由事务2等待执行时间过长或连续不释放锁,导致事务1最终发生回滚。

通过对当天访问日志跟踪可以看到,当天有客户通过脚本方式发起大量的修改推广组标题的操作,导致有大量事务在循环等待前一个事务释放锁定的主键主索引。该问题的根源实际上在于Mysql innodb引擎对于索引利用有限,在Oracle数据库中此问题并不突出。解决的方式自然是希望事务处理锁定的记录数越少越好,这样产生死锁的概率也会大大降低。accountid,groupid)的复合索引,缩小了一部分事务锁定的记录条数,也实现了不同计划下的推广组数据记录的隔离,从而减少了该类死锁的发生几率。

通常来说,对于数据库层的调优我们基本上会从以下几个方面出发:

(1)在SQL语句中进行优化:SQL分析,索引分析和调优,事务分解等;

(2)在数据库配置方面进行优化:细分设计,调整缓存大小,磁盘I / O等数据库参数优化,数据碎片整理等;

(3)从数据库结构层次进行优化:考虑数据库的垂直分割和水平分割等;

(4)选择合适的数据库引擎或类型适应不同场景,出于考虑NoSQL等。

总结与建议

性能调优同样一致2-8原则,80%的性能问题是由20%的代码产生的,因此优化关键代码事半功倍。同时,对性能的优化要做到按需优化,过度优化可能发布更多问题。对于Java性能优化,既要了解系统架构,应用程序,同样需要关注JVM层甚至操作系统集成。总结起来可以从以下几点进行考虑:

1)基础性能的调优

这里的基础性能指的是硬件层级或操作系统层级的升级优化,某些网络调优,操作系统版本升级,硬件设备优化等。诸如F5的使用和SDD硬盘的发布,包括新版本Linux在NIO方面的升级,都可以极大的促进应用的性能提升;

2)数据库性能优化

包括常见的事务分解,索引调优,SQL优化,NoSQL的更新,最终解决一致性等做法的发布,包括针对具体场景发布的NoSQL数据库,都可以大大缓解传统数据库在高并发下的不足;

3)应用架构优化

以前的一些新的计算或者存储框架,利用新特性解决方案并对其进行了性能评估等;或者在分布式策略中,在计算和存储进行水平化,包括提前计算准备等,利用典型的空间转换时间的做法等;都可以在一定程度上降低系统负载;

4)业务预算的优化

技术并非提升系统性能的唯一手段,在很多出现性能问题的场景中,实际上可以看到很大一部分都是因为特殊的业务场景引起的,如果能在业务上进行规避或调整,实际上往往是最有效的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值