调优案例分析与实战

1、高性能硬件上的程序部署策略

        一个每天15万PV(Page View,页面浏览量)的在线类型文档网站,硬件升级前使用32位系统1.5GB的堆,用户感觉使用网站比较缓慢。升级为64位系统、4CPU、16GB物理内存的服务器后,管理员为了尽量利用硬件资源选用了64位的JDK,并将堆大小设置为12GB,使用一段时间后,发现网站经常不定期出现长时间失去响应的情况。

        监控服务器运行服务状况后发现网站失去响应是由GC停顿导致的,虚拟机运行在Server模式,默认使用吞吐量优先收集器,回收12GB的堆,一次Full GC的停顿时间高达14s。且由于程序设计的关系,访问文档时要把文档从磁盘提取到内存中,导致内存中出现很多文档序列化产生的大对象,这些对象大部分都进入了老年代,没有在Minor GC中清理掉。这种情况下即使是12GB的内存也很快被消耗殆尽,导致网站每十几分钟出现十几秒的停顿。

        在高性能硬件上部署程序,目前主要有两种方式:

        通过64位JDK来使用大内存。面临如下问题:对大内存进行垃圾收集意味着更长的GC时间,会加长服务器的停顿时间;由于指针膨胀、内存对齐等原因,相同程序在64位JDK消耗的内存比32位JDK大;大内存产生的堆转储快照很大甚至无法产生,即使产生了也几乎无法分析。给用户交互性强、对停顿时间敏感的系统分配超大堆的前提,是有把握将应用程序的Full GC频率控制得足够低。控制Full GC频率关键是看应用中绝大多数对象是否“朝生夕灭”,即大多数对象的生存时间不应太长,尤其不能有成批量、长时间生存的大对象产生。对于网站应用,主要对象的生存周期应该是请求级或页面级的,会话级和全局级的长生命对象很少,应当都能实现在超大堆使用而没有Full GC。

        使用若干个32位虚拟机建立逻辑集群来利用硬件资源。具体做法是在一台物理机器上启动多个应用服务器进程,每个进程分配不同的端口,然后在前端用负载均衡加反向代理的方式把请求分配给各个应用服务器。可能面临如下问题:对于多台服务器如何记录用户的状态;32位的节点受最高内存(2^32≈4GB)限制,尤其是在32位windows平台中每个进程只能使用2GB的内存,考虑到堆以外的内存开销,堆一般最多只能到1.5GB;节点对共享资源的竞争,多个节点同时操作同一资源带来的问题;节点内资源池的闲置,各个节点建立自己独立的资源池可能出现有些节点的资源池用满而有些节点的资源池空闲,可以考虑配置集中式资源池;节点内的本地缓存重复,可以考虑缓存数据库统一存放缓存,如Redis。考虑到建立集群的目的仅仅是为了尽可能利用硬件资源,无须关心状态保留、热转移之类的高可用性需求,因此选用亲和式集群。用户首次访问时,前端记录下用户访问的应用服务器,该用户以后的所有请求都发送到同一台应用服务器,即固定用户的请求由固定的应用服务器处理。由于只在用户首次访问的时候进行了负载均衡分发请求,会导致各个服务器负载“不均衡”。

        最后的部署方案调整为建立5个32位JDK的逻辑集群,每个进程为2GB内存(其中堆固定为1.5GB),建立一个Apache服务作为前端负载均衡代理访问门户。考虑到用户比较关心响应速度,且文档服务的主要压力集中在磁盘和内存访问,CPU资源敏感度较低,因此改用CMS收集器进行垃圾回收。

2、集群同步导致的内存溢出

        一个基于B/S的MIS(Management Information System,管理信息系统)系统,硬件为2台2个CPU、8GB内存的小型机,每台机器启动了3个WebLogic实例,构成一个6节点的亲和式集群。由于是亲和式集群,节点之间没有Session同步,但有部分数据需要在各个节点间共享。开始这些数据存放在数据库中,但由于读写频繁,竞争激励,性能影响较大,后面使用JBossCache构建了一个全局缓存。启用全局缓存使用一段时间后,不定期地出现内存溢出问题。

        为了实现用户无法在一段时间内同时在多个节点登录的功能,在MIS的服务端有一个负责全局校验的Filter,每次接收到请求,都会更新一次最后操作时间,并将这个时间同步到所有的节点。由于请求有传输失败需要重发的可能性,在确认所有注册的节点都收到请求前,数据必须存储在缓存中。服务使用过程中,一个页面往往会会产生数次甚至数十次的请求,因此这个Filter导致了集群各个节点之间的频繁交互。当网络不能满足传输要求时,重发数据在缓存中不断堆积,从而产生内存溢出。

        共享数据使用缓存同步时,可以允许允许频繁的读操作,但不应该有频繁的写操作,否则会带来很大的网络同步开销。

3、堆外堆存导致的溢出错误

        直接内存不像新生代、老年代那样发现空间不足就通知虚拟机进行垃圾回收,只能等老年代满了触发Full GC,顺便清理掉废弃对象。否则只能等到抛出内存溢出异常时,在catch块里调用system.gc()方法,如果虚拟机打开了-XX:+DisableExplicitGC开关,即使堆中还有很多空闲内存也只能抛出内存溢出异常。

        除了Java堆和永久代之外,如下区域也会占用较多内存,内存总和受操作系统进程最大内存限制。

        Direct Memory:可通过-XX:MaxDirectMemorySize调整大小,内存不足时抛出OutOfMemoryError或OutOfMemoryError:Direct bufffer memory;

        线程堆栈:可通过-Xss调整大小,内存不足时抛出StackOverflowError(纵向无法分配,即无法分配新的栈帧)或者OutOfMemoryError:unable to create new native thread(横向无法分配,即无法创建新的线程);

        Socket缓存区:每个Socket连接都有Receive和Send两个缓存区,分别占大约37KB和25KB内存,连接多的话内存占用也很可观。如果无法分配,会抛出IOException:Too many open files异常;

        JNI代码:如果代码中使用JNI调用本地库,那本地库使用的内存也不在堆中;

        虚拟机和GC:虚拟机和GC代码的执行也要占用一定的内存。

4、外部命令导致系统缓慢

       虚拟机执行Java的Runtime.getRuntime().exec()方法来调用外部命令时,首先克隆一个和当前虚拟机拥有一样环境变量的进程,再用这个新的进程去执行外部命令,最后再退出这个进程。如果频繁执行这个操作,即使外部命令本身能很快执行完成,CPU和内存负担也很重。

        解决方法是改为使用Java的API去调用外部命令。

5、服务器JVM进程崩溃

        一个与OA门户系统做了集成的MIS系统,当MIS系统工作流的待办事项变化时,要通过Web服务通知OA门户系统,把代办事项的变化同步到OA门户系统中。由于MIS系统的用户多,待办事项变化很快,为了不被OA系统的速度拖累,使用异步的方式调用Web服务。但由于两边速度不对等,随时间累积了很多没有完成调用的Web服务,导致等待的线程和Socket连接越来越多,最终超出了虚拟机的承受能力导致虚拟机进程崩溃  。  

        解决方法是将异步调用改为生产者/消费者模式的消息队列的实现方式。

6、不恰当数据结构导致内存占用过大

        

7、由Windows向虚拟内存导致的长时间停顿

        GUI程序最小化时工作内存被自动交换到磁盘的页面文件中,因此资源管理中显示的占用内存大幅度减小,但是虚拟内存则没有变化。这样发生GC时可能因为回复页面导致不正常的GC停顿。

        解决方法是加入参数-Dsun.awt.keepWorkingSetOnMinimize = true来保证程序在恢复最小时能够立即响应。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值