近段时间,我们项目中用到的WebSphere 应用服务器(WAS) ,但在客户的production 环境下极不稳定,经常宕机。给客户造成非常不好的影响,同时,也给项目组很大 压力。为此,我们花了近一个月时间对其诊断,现在基本上稳定了,需要继续观察一段时间。现在我主要将工作做一个阶段性的总结。
我们的产品环境是:WAS6.0 +DB2 8.1 +AIX5.3+RS/6000 。在该产品环境下,出现的问题非常多,现象如下:
WAS经常不稳定、宕 机几乎一天一次,经常报告OutOfMemory ( 内存泄漏吗?NO)。
DB2 连接数过大,有时把DB2 撑死,有时也把AIX 撑死。
AIX虚拟内存报错、分页报错 、IO 也报错、还有很多其它莫名奇妙的错。
总是,每次问题发生的现象和理论上的总是不一致,导致我们不知道从何入手,也无从检测自己的优化参数。咨询过多次IBM 技术支持,只解决了某些局部问题。
虽然问题依然存在,但我想,解决问题的思路、特别是理论基础,还是有一些规律和原则。
对于WAS 这块,我近段时间的主要时间集中在以下几个方面( 时间顺序) :
1 、Java 性能监测工具:Jprofiler ,也用到Jprobe 。后来发现Jprofiler 在AIX 下几乎不可用。
2 、IBM Java 虚拟机和WAS 技术细节,特别是IBM JVM的GC 原理,我发现它和sun 、bea 的差别很大。
3 、IBM 的heap 分析器Heap Analyzer、GCCollector 。这两个事后监测工具非常实用,特别是我们的产品运行环境,非测试环境。
4、某些Application 的怀疑和诊断。
5 、AIX 诊断,我几乎没有这个能力,只能常规监测一下,需另请高人。
我打算将本文分成以下几个部分总结:
JVM 原理、IBM JVM 的GC 策略和调优。
Jprofiler 和IBM 工具的实际体会
WAS的诊断体会和AIX 调优
下面开始主题吧,可能比较零碎,另外,开始的理论篇基本上看书都可以,我只是总结一下,再添加一些自己的理解。
以下是我参考的最重要的两本电子书和一些网站:
《Inside Java Vrtual Machine 》: 半部分有约四章我认为非常 棒,其它章节可能意义不大。
《The Java Virtual Machine Specification, 2nd 》:前半部分有两三章很不错,不过可以对照上一本书看。
sun 的hotspot 虚拟机技术:http://java.sun.com/javase/technologies/hotspot/
BEA 的JRockit 虚拟机技术:http://edocs.bea.com/jrockit/geninfo/genintro/index.html JVM技术文档入口,虚拟机理论,内存泄漏诊断等的索引页。
IBM诊断资料:http://www-128.ibm.com/developerworks/java/jdk/diagnosis/ 上面有一个500 多页的pdf 文档,对IBM JVM技术和诊断讲解很深入。
我不得不提的是,在查资料这块,BEA 和Sun 都有很好的官方文档和论坛支持,并且官方文档导航非常好。虽然IBM 的诊断资料也不少,但需要搜索,其搜索是很痛苦的。而且,IBM 官方论坛很差。如果用IBM的产品出问题,切记:找IBM 技术支持,千万不要蒙着头搞!反正它们的产品很少免费。说实话,它们的技术支持还是挺负责的,一般会为你推荐很多support资料,而该资料往往都在developerworks 网站上,属于support那个频道,但你就是搜不着。
Java 虚拟机规范概要
研究Java 虚拟机,首先要了解Sun 的Java 虚拟机规范。现在,该实现版本很多,如比较有名的Sun、IBM 、BEA 、Apple、HP 、MS 、Apache Harmony。它们都实现了JVM 规范,但有各自扩展。譬如,针对IBM虚拟机的堆碎片导致OutOfMemory (OOM),在Sun 的虚拟机上就不会发生。Sun 的JVM有maxPermSize 的概念,IBM就没有,如果你设置这个参数,虚拟机根本就启动不了。
比较有意思的是,学Java ,就一定要了解各种规范,这和MS 的风格很不一样。Sun总是在定义一些规范,实现都留给各厂商。我们除了理解规范本身外,一定要理解规范和实现之间的关系,譬如JDBC规范和JDBC 驱动的关系,它们是怎么组合到一起的。要是你用过php 的xml 解析库,或db函数,就会体会深刻,它们可没有什么规范可言,所以每个数据库厂商的db 函数用法都不一样。我推荐大家研读一下HSQLDB的jdbc 和Tomcat的servlet 相关实现,因为我认为它们还是比较好懂的。
JVM规范只是定义一个虚拟机该做什么,但它并没有要求你该怎么做。例如我们最常见的Servlet 规范,在该规范中,有HttpServletRequest 、HttpServletResponse ,HttpSession 等接口,但它们的实现都留给了各个容器厂商。遗憾的是,规范留下的空白,会把我们这些开发人员给整惨了:容器间移植有时候就是恶梦。譬如J2EE 并没有SSO 规范,但它很重要,我以前专门针对它做过WebSphere AppServer 和Weblogic AppServer 的SSO 项目,差别还是不小,不过还是有点共通,那就是都遵循JAAS规范。
JVM 的结构
从功能上分,Java 虚拟机主要由六个部分组成,可以分成三类:
第一类:JVM API :就是我们最常用的Java API ,它是开发人员和Java交互的入口,它主要是JAVA_HOME/jre /lib 下的运行时类库rt.jar 和编译相关的tools.jar
第二类:JVM 内部组件
类装载器 (ClassLoader ) :将Byte Array 的 .class 文件装载、链接和初始化。
内存管理 (Memory Managent ) :为对象分配内存,以及释放内存。后者就是垃圾回收Garbage Collector(GC )。由于JVM最复杂的、最影响性能的就是GC ,所以内存管理一般就指垃圾回收。
诊断接口 (Diagostics Interface) :这主要体现在JVMTI(jdk1.4 下的JVMPI 和JVMDI),它主要用来诊断程序的问题和性能,一般提供给工具 厂商实现。如eclispe IDE 下的debug功能,Jprofiler 性能调优工具。
类解释器 (Interpreter) :解释装载进虚拟机的class对象,包括JIT 等特性相关。
第三类:平台相关接口(Platform Interface) :主要为了跨操作系统平台重用JVM 代码,不过,它和我们开发人员关系不大。
在以上六个组件中,我们开发人员最关心的是ClassLoader 和GC ,用Java 做系统框架、容器和它们密切相关。做业务系统时一些基础代码也和它们打交道,譬如最常用的Class.forName (),Thread.currentThread.getContextClassLoader ()。我们仔细想想,为什么是上面两个问题?因为,它和我们class的整个生命周期最为相关:怎么将一个class 和相关class加载进来,class 实例什么时候创建,什么时候被销毁?
所以,下面的部分我们要专门讨论这些问题。
ClassLoader
JVM主要有三类ClassLoader :Bootstrap 、Extention 、Application ,该三类ClassLoader 从上到下是分级(hierarchy) 结构,遵循代理模型(Delegation Model)。
Tip :大家可以看看sun.misc.Launcher 的源码,Bootstrap和Extention 就在该文件里。该src 可以在sun 的网站上下载该压缩包,约60M (jdk-1_5_0-src-scsl.zip),它不在jdk 自带的那个src.zip 里。
Bootstrap ClassLoader :也称为primordial(root) class loader 。主要是负责装载jre /lib 下的jar文件,当然,你也可以通过-Xbootclasspath 参数定义。该ClassLoader 不能被Java 代码实例化,因为它是JVM本身的一部分。
Extention ClassLoader :该ClassLoader 是Bootstrap classLoader 的子class loader。它主要负责加载jre /lib/ext/下的所有jar 文件。只要jar包放置这个位置,就会被虚拟机加载。一个常见的、类似的问题是,你将mysql 的低版本驱动不小心放置在这儿,但你的Web 应用程序的lib下有一个新的jdbc 驱动,但怎么都报错,譬如不支持JDBC2.0的DataSource ,这时你就要当心你的新jdbc 可能并没有被加载。这就是ClassLoader 的delegate 现象。常见的有log4j、common-log 、dbcp 会出现问题,因为它们很容易被人塞到这个ext 目录,或是Tomcat下的common/lib 目录。
Application ClassLoader :也称为System ClassLoaer 。它负责加载CLASSPATH环境变量下的classes 。缺省情况下,它是用户创建的任何ClassLoader 的父ClassLoader ,我们创建的standalone 应用的main class缺省情况下也是由它加载( 通过Thread.currentThread ().getContextClassLoader ()查看)。
我们实际开发中,用ClassLoader 更多时候是用其加载classpath 下的资源,特别是配置文件,如ClassLoader.getResource (),比FileInputStream 直接。
ClassLoader 是一种分级(hierarchy ) 的代理(delegation )模型。
Delegation :其实是Parent Delegation ,当需要加载一个class时,当前线程的ClassLoader 首先会将请求代理到其父classLoader ,递归向上,如果该class已经被父classLoader 加载,那么直接拿来用,譬如典型的ArrayList ,它最终由BootstrapClassLoader 加载。并且,每个ClassLoader 只有一个父ClassLoader 。
Class查找的位置和顺序依次是:Cache 、parent 、self。
Hierarchy :上面的delegation 已经暗示了一种分级结构,同时它也说明:一个ClassLoader 只能看到被它自己加载的classes,或是看到其父(parent) ClassLoader 或祖先(ancestor) ClassLoader 加载的Classes。
在一个单虚拟机环境下,标识一个类有两个因素:class 的全路径名、该类的ClassLoader 。
我碰到的一个典型的例子是:在做WAS 的SSO 开发时,由于我们的类是由WAS 在启动时加载,该ClassLoader 比下面的部署的Applicaton 的ClassLoader 的级别高。所以,在我们自己的类中没法用到应用程序的连接池,必须自建。
代理模型是Java 安全模型的保证。譬如,我们自己写一个String.java ,并且编译、package 到自己的java.lang 包下。按照代理模型,当前线程的ClassLoader 会将其代理到父ClassLoader ,父ClassLoader ( 最终会是Bootstrap)会找到rt.jar 下的String.class ,也就是说我们的String.class 不会捣乱。
自定义ClassLoader
我们前面说过,自定义ClassLoader 的缺省父ClassLoader 是Application ClassLoader 。一般的应用开发用不到它,但我们最好理解。因为在内存泄漏查找、应用程序部署出问题时,很多都和它有关。
譬如,内存泄漏是怎么产生的?这就涉及到ClassLoader 和Class的生命周期。我曾经碰到这样一个问题:我们的程序用到了Webwork 和Spring 框架,当部署到Tomcat 下时没有任何问题,但部署到WAS下,报告找不到Webwork 的xml的DTD 文件,而且Spring 的日志也总是失效。Why?因为解析xml dtd 时,用的是IBM的Xerces ,不是我们的。而Spring日志问题是因为应用程序用的是WAS 的Common-log.jar ,而不是我们的。将应用的ClassLoader 从默认的Parent-First ,改成Parent-Last就可以解决,不过我们项目中用到其它库,又发生了其它问题。
一般来说,用到自定义ClassLoader 有三种情况:
1 、应用框架可以自己控制Classes 的目录,并且自动部署。
我读过Jive 公司的Wildfire( 著名的即时通讯服务器),它自己有一套应用框架,非常灵活,遵循该框架插件规范的的 第三方的plug-in放置在指定目录可以自动部署,实现某些扩展功能,如文件传输、语音聊天。
2 、区分用户代码
这被广泛应用在Servlet 容器和类似容器,譬如EJB Container设计中,大家看到Tomcat 下有common、server 、share 三个目录吧(ClassLoader 顺序从左到有) ,另外也有用户应用的WEB-INF目录,它是我们自己开发的。
3 、允许Classes 卸载
如果没有自定义的ClassLoader ,那么我们自己应用中的classes永远都不能被卸载,因为这些类被Application ClassLoader 加载后cache 起来了,我们的classes一直对该ClassLoader 有引用,而该系统级的ClassLoader 永远都不会被卸载,除非JVM shutdown了。JSP 和Servlet 的动态部署就用到这个特性。
Note: 还有JVM 运行时(Runtime) 架构,ClassLoader 加载class 过程没有总结,这两部分我觉得太重要了,但内容太多,写不完啊。
这部分内容,《Inside Java Virtual Machine 》讲解非常清楚,BEA的官方网站这部分也非常不错,要理解深刻,我建议结合JProfiler 工具,非常直观。
续写这篇文章,已经过去一个半月了。直到现在,系统一直运行平稳。
先说说我接手这项工作的经历吧:该项目大部分是06 年10 月就部署在客户那边了,到07年3 月份,WAS 宕机问题实在无法忍受,我才加入进来,前半年有另外一位同事断断续续处理,但对问题一直都无可奈何,而且项目负责人也没有引起足够的重视。可想而知,最后付出的代价是非常惨重的。在这近半年的时间内,服务器宕机63次。每次宕机时,WAS 的JVM 会dump出一个heapdump.phd 文件(heap快照) ,然后JVM 就死掉了,当然,此时WAS也停止了响应。一般我们的做法是重启,最后是干脆AIX 每天晚上定时重启。有时候一天还死多次。大家见附件的截图(all-GC.png)。这是我接手后,用IBM 的分析工具得到的截图。对截图的分析,留给后面对应的部分吧。
服务器不稳定、宕机问题,拖延到最后,客户愤怒了,公司高层也害怕了,部门还专门成立了八人攻关组。当然了,我当时的压力也非常大,因为我是技术负责人,也就是实实在在干活、想主意的。
服务器诊断那段时间,从前到后,我们也是沿着一条线走下来,虽然最后发现很多路都走不通。现在就按这个思路,也就是时间先后一步步叙述吧。我想,大家如果也碰到类似应用服务器诊断,应该思路差不多。
术语说明:
IBM Websphere Application Server :WAS ,WebSphere本身是一个平台,产品家族
OutOfMemoryError :OOM ,内存泄漏,内存溢出
Gabage Collection :GC ,自动垃圾回收
Content Management System :CMS ,就是给新闻类门户网站编辑们用的系统
我们诊断大体上经历了以下几个阶段:
1 、按Job 调度线程池引起内存泄漏诊断:因为很多次OOM 是发生在某个特定时候,譬如14:30 、22 :40左右。
2 、按应用程序引起内存泄漏诊断:用JProfiler 等工具探测:因为总是发生OOM。
3 、分离WAS 怀疑有OOM 的应用:因为每个WAS应用太多,20 来个,混一起没法定位。
4、用IBM 官方heap 、GC分析工具。以及和IBM 技术支持联系。WAS 、AIX参数优化。
5 、隔离出WAS 超级恶魔程序:一个CMS 产品。
6、WAS 、AIX 参数优化、设置。
我们走到第5 步时,才出现效果。计算一下,那时已经过去一个月了。服务器宕机、系统不稳定,在这个验收的时候,客户已经忍无可忍,以致后来的每一次行动都得胆战心惊得去做。
一、按Job 调度线程池导致内存泄漏诊断
因为从我们WAS 的日志( 默认是native_stderr.log) 来看,最近半年的宕机时间都有一个明显时间规律。见附件截图Job1-1.png。
我想,做过Java 服务器性能调优的朋友,都知道在Web 容器里面启线程池是个不太好的做法,因为Web容器本身有一个线程池,譬如Servlet 线程池(Tomcat默认起25 个),而自启的线程池很容易导致Servlet线程管理混乱,最终导致GC 问题。我们的现象似乎和那很符合。如果我们沿着这个思路做下去,具体怎样实施呢?
我们的WAS 上部署了20 个左右的Web 应用,譬如Lucene全文检索、B2B 行业数据同步等,都是通过Quartz的Job 调度做的,当然还有很多其它调度。当时,由我负责,通知相关负责人,将定时调度暂时去掉。观察了几天,后来发现问题依然存在,不过时间有点随机了。
不过,最后还是发现OOM 不是由Job 调度引起的。
也就是说,我们这个方案是失败的。而且,我们的很多想法都是臆测的,没有可靠的根据,也没有方向,再加上我是第一次处理这种问题,这导致后来查找问题的艰难。但是,仔细想想,我们又能拿什么做依据呢?出现OOM错误,我想大多数人想到的,除了JVM 参数设置,就是内存泄漏。其实,OOM发生有很多种情况,在IBM 、Sun 、BEA等不同虚拟机上,因为GC 机制不一样,所以原因一般都不同,容易定位难度也不一样。下文会谈到。
于是,我们干脆釜底抽薪分析问题吧:用JProfiler 检测。
二、按应用程序导致内存泄漏诊断,JProfiler 检测
如果遇到OOM 问题,我想大家都会想到内存检测工具,现在最可靠的还是下面三种分析工具:Borland 的Optimizeit Suite ,Quest的JProbe ,ej-technologies 的JProfiler。但面临三个问题:
1 、三个都是商业产品,公司暂时没有买,必须自己下载,而且要找序列号。
2 、工具必须支持AIX5.3 +JDK1.42 +WAS6.0,不是Windows 平台。
3 、工具必须在客户真实环境下部署,对客户的业务不能有冲击,也就是说部署测试工具前,必须做个大量测试,对工具非常熟练,遇到意外可以立即恢复现场。
Note:项目上线后,而不是测试或试运行阶段遇到此类问题,非常考验人;另外一个,就是性能和可伸缩性问题,很可能把整个项目给毁了。
当我决定要这么做后,就立即动手查阅这些工具的官方文档,用emule 下载,最终都下载到了。试用后,最终选择了JProfiler4.03 ,比起其它工具,它界面美观、清晰、功能强大、集成度高(Heap,Memory,CPU,Thread都统一了) 。另外,JProbe没有AIX 版本,这也是放弃它的一个原因。
JVM 的Profiler 原理,都是通过JVM 内置的的标准C 语言Profiler 接口收集数据,然后通过Profiler 工具的客户端展现。也就是说各厂商的Profiler工具,都有两个部分,一个部分是Profiler Agent ,和JVM绑定,负责收集JVM 内部数据,譬如方法调用次数、耗费时间等;另外一个部分就是Profiler front-end。通过Profiler 工具的自定义local或remote 协议传输到front-end ,其实,我们最常用的JavaIDE的debug 功能就是在此基础上的(JPDA)。(JProfiler 的截图http://www.ej-technologies.com/products/jprofiler/screenshots.html )。
下面是Sun 官方文档:
JDK1.42 及以前是JVM PI :http://java.sun.com/j2se/1.4.2/docs/guide/jvmpi/jvmpi.html
JDK1.5 是JVM TI :http://java.sun.com/j2se/1.5.0/docs/guide/jvmti/jvmti.html
具体到JProfiler 的配置上,专门针对JDK1.4 和1.5的JVM 配置差别很大。
我用的JProfiler 是4.31 版本,透露给大家一个万能序列号吧(这东西不太好找) ,对各版本应该都支持。深入了解Java,这类工具是不可少的:
Name: License for You
Lincese Code: A-G667#42616F-10kg1r134fq9m#2217
为了保证真实环境的检测成功,我做了大量的试验,譬如:
1 、Windows 系列的本地、远程测试。
2 、AIX 的远程测试。
3 、Tomcat5.0 、WebLogic8.14 、WebSphere6.02,以及上述两种方式的组合测试,排列组合,场景不下10 个。
当时也参阅了大量JVM 文档,JProfiler 官方几百页英文文档,辅助的JProbe对照。而且也制造过内存泄漏造成的OOM 场景。
当然,要是在几个月前,在客户那边部署的测试环境时,就进行测试该多好啊。
在公司内部,我用JProfiler 测试了我们当时部署的几个应用,没有发现内存泄漏,所以,我们最怀疑的是就是CMS系统。因为出问题的那个WAS 上它占去了90%的负荷(我们有多台AIX 、WAS 服务器)。该CMS超级庞大,感觉著名的赛迪网就用它,当时该CMS 厂商给我们部署都花了快一个月。所以再重新部署一套测试环境也挺困难。另外,CMS提供商不给lisence 。现在测试,客户早就对我们恼火了,当然不怎么配合,这对我们工作的开展就有很大的挑战。
在大致可以确定万无一失的情况下,我们最终决定在客户的真实环境下测试。也就是让JProfiler 的agent 端直接在WAS 的JVM里面启动(北京IDC ),然后远程(大连)监控。
本来该模式在另外几个应用的测试都通过了(因为北京IDC 那边好几台AIX服务器)。但一部署上,客户的一些编辑用CMS 时就感觉超级慢,尽管我们用了JProfiler的最小负载模式。半个小时后,客户实在无法忍受,打电话过来,又把我们部长和经理训了一顿,还要写书面报告。我们被迫马山中止测试,恢复现场。
虽然JProfiler 也支持客户那边的环境,但还是有bug ,至少负载一高就有严重的性能问题,几乎导致系统挂起,这是我当时没有料到的。JProfiler一启动,WAS 的启动时间就由原来的3分钟降到10 分钟,而且系统响应明显变慢,我们具体的环境如下(排列组合恐怕不下20种):
1 、AIX5.3 ,Power PC64 位(不是32位)
2 、WebSphere6.0
3 、IBM JVM1.42
4 、Remote 模式
我后来仔细读了一下JProfiler 的changeLog ,发现对上面的环境支持不够也很正常,因为官方在最近的4.3.x版本下陆续有对IBM JVM 和Websphere6.0 的features和bug fix :http://www.ej-technologies.com/download/jprofiler/changelog.html
进行到这一步,我忽然觉得无计可施了 ,此时已经过了三周。
上面的策略,我认为是很正统的处理方法。谁怪我们当初项目上线时没有进行充分的测试呢?其实,这类测试没做也正常,OOM 这类问题谁都无法预测。
到这个时候,我想肯定有人会问我?你知道导致JVM 的OOM 有几种情况吗?在当时,我想到了以下五种:
1、JVM 的heap 最小、最大值没有设,或不合理。
2、JVM 的maxPermSize 没有设置(这个IBM的JVM 没有,一设置JVM 就起不来)。
对于Sun 和BEA 的JVM,以上两种参数设置,可以排除90 %以上的非程序内存溢出导致的OOM。
3 、程序内存泄漏
4 、有的JVM ,当在80% 的CPU时间内,不能GC 出2% 的heap时,也发生OOM (IBM 的JVM就是,但我没有验证)
5 、JVM 本身内存泄漏(JVM 有bug不是不可能)
现在的难题是,如果是那个可怕的CMS 程序本身有内存溢出,在产品环境下,我们怎么去验证?我们自己开发的10 来个web应用,测试并不是很难,在公司测试都可以。但是,我现在最想解决的,就是CMS 测试的问题。而且,在我们那种环境下,该CMS产品供应商并没有透露成功案例。
其实,最后发现,并不是内存泄漏造成的,因为我们的heap 走势都很平稳。纳闷的是,有1000M 的heap ,每次在heap只被占用400 来M 时就发生OOM,没有任何预兆。大家猜猜,会是什么原因 ?这个问题我到后面相关章节再说吧。
既然我们所有的矛头都指向那个可怕的CMS 系统,现在就是考虑隔离的问题。如果我们验证这个问题是CMS 造成的,那么大部分责任就应该由CMS厂商承担 。
既然CMS 我们不敢移(费劲,而且客户在正式用),那我就移我们开发的10来个web 系统吧。
三、移出除CMS 系统以外的所有应用
说起来容易啊,做呢? 隔离(移动)工作由我负责,具体涉及到10 来个相关负责人。
转移工作,必须处理好很多问题,就说几个印象最深的吧:
1 、某些应用,如Blog 和BBS ,都涉及到文件、图片上传目录和产品本身的环境,如 JDBC连接池、Cache 位置。
2 、目标服务器本身的环境,WAS 安装环境、网络等。
3 、移植时的先后顺序、调度,各应用内部本身的约束关系。
4 、移植后的测试。
当然,还有一个最严峻的问题,客户允许我们这么做吗?对它们目前运行的系统有多大影响?风险如何评估?
这个工作持续了一天,已经完成了80 %的工作,到第二天,客户又恼火了:WAS 又宕机了。
为什么?这确实是WAS 的一个bug :WAS的后台随便一操作,heap 就会突然上升几百M,导致JVM 内存不够。不过WAS 撑住的话,过半小时后就会降下来,我估计是WAS后台对用户操作状态、文件都缓存到Session 里面。你们可以检查类似这样的一个文件夹:d:\IBM\WebSphere\AppServer\profiles\AppSrv01\wstemp,我不知道为什么WAS不主动去清除它,它偷偷的就上升到几个G ,系统硬盘可能不久就后就会空间不足,WAS莫名迟缓、最后死掉。听过WAS6.0 以前这个目录更夸张。大家见我附件的截图WAS_Console.png那个尖峰。
咋办?经理也已经不敢让我们继续铤而走险了。这个方案最终又以失败告终 。
不过,最后我们还是发现问题出在CMS 上。我们以前把这个问题向CMS 技术支持反映,有大量依据和现象,并且把相关日志都给它们。过了两天,他们最后竟然只回了一句话“从给我的两个日志来看,没有找到任何与XXX 有关的东西....”。TMD !我真的很生气 ,它们的产品都折磨我们半年之久了。不过,看他们对IBM 的WAS 和JVM也不懂,我也就不想再说什么了。下面是我的邮件,公司机密部分都隐去了:
引用
附件是我们这段时间服务器宕机的日志。我们用IBM PatternModeling and Analysis Tool for Java Garbage Collector Version 1.3.2 分析了一下虚拟机日志,没有发现是内存泄漏导致;用IBM HeapAnalyzer Version 1.4.4 分析heap 文件,也没有发现很可疑的内存泄漏。
我想以前你们也这样做过,现在我们分析错误日志,发现有一个现象,在宕机时,总是找不到文件,我看就是Websphere 或是AIX IO资源不够,不知道是什么导致的。但是,我们自己的应用,基本上没有什么IO ,除了一次load几个配置文件。不过,我觉得你们WCM 的IO操作挺多的,不知道你对日志有什么新的发现。
客观的说,这几个月来,宕机那台服务器,除了你们的XXX ,就以论坛和blog为主,而且他们都是开源的。在频繁宕机的06 年11月份,我们的论坛和blog 还没有上线。现在我们不得不每天晚上11点定时重启,但这也不是长久之计。
现在,我们进行分离遇到很大阻力,原来想把你们的XXX 单独分离出来,在当前的环境下,不是很现实,如安装、测试(负载、定时服务),所以现在分离我们自己的应用,但当前在产品环境下,客户方阻力也很大。
希望尽快能够得到你们的问题建议和方案。
文中说到了IBM 的两个分析工具,这也是我们后来的救星:我们就是需要这种离线分析 工具,因为实时检测 已经证明不现实。但我始终对该分析出来的结果抱怀疑态度,直到我去深入IBM的JVM 以及和IBM 的技术支持交流......
柳暗花明啊 ,至少看到了一点希望,不过最后我们还是失望而返。
四、用IBM 的HeapAnalyzer 和GarbageCollector 检测
找到这两个工具,已经是够费劲了,因为以前找的IBM HeapRoot 工具,让我对这类工具很失望。而且,这两个工具,只有在IBM 的Techinical Support网站能够搜索到,但很不容易,因为那两个工具,并不是象IBM 的Websphere产品那样宣传,它只在IBM Techinical Support 文章的某些角落里出现。要知道,Techinical Support是IBM 很重要的收入来源,这类文档,他们并不会让你很轻易就拿到,比起BEA WLS的支持网站dev2dev 差远了。
具体诊断细节我就不详述了。我认为,IBM 的WAS 或JVM出了性能和OOM 问题,这两个工具是最有效的,而且是离线分析工具,比起那些实时Profiler工具,某些场合有绝对的优势:譬如我们目前的产品环境,你只能分析宕机后的日志,实时分析前面已经验证是不可行的。
从日志分析,我们最终得出结论,我们购买的CMS 系统有严重的碎片(大对象)问题,而该问题是OOM的罪魁祸首,而且IBM 工程师也得出了同一结论。不过,在起先我们得出这一结论一周后,我还始终不相信heap碎片会导致OOM ,直到IBM 工程师总是向我强调。
我想很多人也是不太相信,因为大多数人用的都是Sun 的JVM ,譬如Windows 、Solaris上的hotspot 。而且,SunJVM 出问题,如果是配置的问题,一般通过配置heap 最大最小值,以及maxPermSize都可以解决。Heap 碎片导致的OOM,只有BEA 的JRockit 和IBM JVM上发生,不过JRockit 有专门文档说明,而且很容易找到(就在jdk的文档里面)。
配置heap 最小最大值 ,我想大多数人都有经验。对于Sun 的JVM 来说,一般可以设置heap 最大最小值一致,也是推荐的做法。因为它的GC策略默认是复制、分代算法。也就是说,它会将heap 分成不同的几个区,譬如Solaris JVM中最上面有两个大小相等的区。GC 时刻,将一个区的存活对象复制到另外一个对等区,垃圾对象就算遗弃了。这样在heap里面,就不存在碎片问题。另外,根据Java 对象的存活期不同,将之转移到不同的区(Tenured区),存活最长的在最底部(火车算法),这也就是分代模型。具体请参考官方文档:http://java.sun.com/docs/hotspot/gc1.4.2/
对于maxPermSize (Permanent Generation ), 主要和那些加载到JVM 里面的JavaClass 对象相关,它的空间不是在Java Heap 里面分配。如果你当前的heap有1000M ,permSize是200M ,那么JVM 至少占用1200M 。
在这个空间内的对象的生存期和JVM 是一样的,譬如JDK 的核心类库,它们被System Classloader加载到JVM 的Method Area(方法区)后,就不会被GC 掉的,这些对象一般是Class对象,而不是普通的实例对象,也就是JVM 的元数据。我们在用反射时经常用到它们。所以,对于现在象Spring、Hibernate 这些框架经常通过反射创建实例,可能对maxPermSize要求就大了,缺省的64M 很多时候是不够的,特别是对于应用服务器里的应用,象JSP 就会产生和加载很多classes。不过,如果是它导致的OOM ,一般会有类似 perm size提示。
但是,对于IBM 的JVM ,情况就完全不一样。它的默认GC策略并没有采取复制、分代。这个可以从GC 日志分析出来。它不像Sun的JVM 那样,有个单独的方法区,它的方法区就放在Java Heap里面。JVM 规范里面并没有要求方法区的必须存放的位置,因为它只是一个JVM实现问题。
在IBM 的JVM 里面,这些对象一般分配在称为k-cluster和p-cluster 里(cluster又是属于Heap ),而后者一般是临时在heap里面申请。并且,这些cluster 是不能GC,或是被移动重排的(Compact 过程)。这就导致Java Heap里面就如同马蜂窝,但不同的蜂孔又不能合并,于是,当我们程序里面产生一个大对象,譬如2M 的数组(数组必须分配在连续的内存区) 时,就没有可分配空间了,于是就报告OOM。这些不能被移动的cluster 就称为所谓的碎片。此时,JVM的Heap 利用率可能不到50% 。
当然,通过一定时期的GC 日志,可以计算出cluster 的合理大小(专门在Java Heap的底部),另外,还可以为这些大对象专门分配大对象区的(超过64k 的对象)。
通过上面的理论介绍,我想大家一定知道了为什么IBM 的JVM 里面不推荐heap 的最大最小值相同,因为这样碎片问题会非常严重:如果我们每次大对象申请内存时,heap都扩展5% ,譬如50M ,碎片问题很大程度上可以避开,程序性能也高些(寻找可用空隙和分配耗时,以及每次GC时间拉长)。
以上的具体阐述,请参考我在上文推荐的几个URL ,另外再推荐三个宝贵的链接:
http://www-1.ibm.com/support/docview.wss?rs=180&context=SSEQTP&q1=fragmentation&uid=swg21176363&loc=en_US&cs=utf-8&lang=en
http://www-900.ibm.com/cn/support/viewdoc/detail?DocId=2447476A10000 (IBM 技术支持告诉我的,太重要了!)
http://www-900.ibm.com/cn/support/viewdoc/detail?DocId=2847476B08000
我想大家应该会问:我怎么能够肯定我的OOM 问题是heap 碎片造成的呢?下面的方法可以验证。
在OOM 发生时,JVM 会产生一个heapdump文件。然后用GarbageCollector 分析出该OOM发生时刻,JVM 去申请的空间,譬如约235k。此时,你再用HeapAnalyzer 去分析此时的heap快照里面的gap size 大小(空隙大小)和各自的可用数目。你会发现,大于235k的空隙个数为0 。这就是碎片导致OOM 的证据。
另外,有人会问:我怀疑我的OOM 是因为程序内存泄漏造成的,怎么去验证?
你可以用HeapAnalyzer 分析发生OOM 时刻的heap快照,工具会罗列出哪些对象怀疑有内存泄漏,譬如Cache 对象都非常大(但你可以确定它不是内存泄漏)。另外,分析这次宕机(从这次虚拟机启动到宕机这段时间)的heap走势,如果曲线明显是向上倾斜,也就是那种典型的内存泄漏图,就有可能是内存泄漏。当然,还必须结合heap快照。
内存持续上升在JVM 开始一段时间很正常,因为JVM 对第一次访问到的Class 对象,譬如一个典型的Web 应用,就有jdk的class 、Spring 或Hibernate的class 对象,它们都会被缓存下来(ClassLoader原理) ,一般均不会被GC。当大多数class 对象缓存差不多(当然还可能有一些Singleton对象,不过不怎么占分量),JVM 的Heap就平稳了,呈一水平波浪或锯齿线。
如果可以用JProfiler 这类工具实时监控,就更容易确诊了。
经过一番周折,我们终于看到了一线希望了 。
在一定的准备后,我们决定对WAS 进行性能调优了。WAS 的调优参数,可以分为两个部分:JVM级别和WAS 级别:
JVM :主要是GC 和Heap 。
WAS:Thread Pool ,JDBC DataSource 。
当然要调节,你需要明白你的目标是什么,调节依据是什么,怎么计算,绝对不是凭空想象的,譬如heap 最小值1024M ,日志证明,该参数非常不适合我们的环境。具体细节,留给后文吧。
战战兢兢地,中午12:00 ,我们给产品环境下的WAS 调节参数、重启,同时优化了AIX的IO 相关参数。
我试着设置了一下JVM 的k-cluster 和p-cluster 。下午15:00左右,WAS 挂了,AIX 也挂了。这下麻烦可大了。我们都慌了,马山客户的老总就来电话了,一阵哗哗啦啦。实在无奈,让客户那边工作人员通知机房(服务器托管处)工作人员重启AIX。我也不得不强行更改刚才的参数,立即设为另外一个值。
其实,我把那个两个cluster 值确实设置太大了,我把它们设置为推荐值的5倍,譬如p-cluster 是65k×110%×5 。另外一个愚蠢的设置就是把最小heap设置为2048M (AIX有4G 内存)。
后来我恢复到约正常的值,也就是去掉那个cluster 的5 ,另外分配了一个30%的大对象区(如果1000M 的heap ,就是700M +300M )。
就这样,系统持续正常运行了三天,以前可是一天一down 。当在三天后再次宕机时,我们都没有自信了 。不得不通过AIX 的cron ,继续每天深夜11点的WAS 定时重启。
不过,那次宕机,包括以后的几次宕机,再也没有出现OOM 错误了 ,但系统依然不稳定。虽然我可以说OOM问题解决了,但领导和客户需要的并不是这种结果。
其实,在这个时候,我们已经发现我们系统的四大问题:
1 、WAS 和JVM 参数:OOM问题
2 、AIX 的IO 和Paging Spacing不足:AIX 日志后来显示错误
3、AIX 的WAS 分区空间不够:WAS的日志膨胀一周就把那个opt 分区塞满了。
4、应用程序的JDBC 连接池:我们20 来个应用,一个20 connections,DB2 数据库有时被撑死。
也就是说,我们最初在客户那儿部署时,用的默认值根本不行。而且,部署涉及多人,人员之间出现断层。如果我们只是按OOM ,无疑是走入死胡同,必须全局考虑!
但是,项目组实力薄弱,公司范围内就没有对AIX 精通的。不过项目组原来有一个搞银行系统,在AIX下开发,就他熟悉些。我当时对AIX 也比较陌生,你们从Linux转到AIX ,你就知道它有多别扭了。命令都自定一套(也许因为是Unix元老吧),那个shell 也超级别扭,而且参考书特少。不是自诩,我两年前负责一个高负载的Linux服务器管理一年多,也是玩得很转的。
就这样,他负责AIX 的相关问题,我负责WAS 相关的。
但是,现实环境,已经不允许我们再试验下去了 。我们必须找到一条绝对可靠的对策!
这就是下文的CMS 系统大迁移,服务器再次优化。
五、隔离CMS 系统,服务器优化
从前面的介绍,大家应该记得,我们开始是固定CMS ,分离其它应用,但遭遇失败。现在是反过来,干脆把CMS 系统赶出WAS 平台 。
说实话,项目经理做这个决定,我认为已经是鼓出很大勇气了 。
当时我们想在一个备用AIX 机上安装CMS 产品测试,但最后还是没有做成:
CMS 这类文章发布系统很难安装,也不好测试,又没有liscence ,而且还有一堆准备工作。绝对没有著名的openCMS安装那么简单,当然功能远远比它复杂。而且,我们当时也低估了后来的工作,总觉得问题好解决。
在很遥远的06 年中期,CMS 厂商在客户那边一台AIX 的Tomcat 上部署了一套CMS产品。但当时客户执意要求将其跑在WAS 上,也就是现在的情况。最开始,客户还要求我们必须用WAS的集群( 我们买的就是WAS 的ND版) ,无奈该CMS 不支持。要是集群,又是死伤一遍。其实,现在想想,我们当时太被动,CMS这种东西,就供公司的几十个编辑用,一个普通Tomcat 就完全够用。而且,把它和面向公网的Internet应用混在一起,完全没有必要。也许,被动是因为我的实力造成的。
我们决定背水一战时,已经做过周密的计划:某年某月某日晚上8:00......
CMS 产品负责人现场切换
xx (我)负责WAS 相关参数调整
yy 负责AIX 参数。
zz 负责应用的测试
…..
总之,该行动涉及到客户方、产品提供商、公司高层、项目组。每个人都密切关注,不下20 人。每个人都守在电脑前,随时听候调遣,当天晚上,我们都没有准备回家睡觉,大家齐心协力。
真没想到,整个式切换工作,一个小时就顺利完成 !第二天,客户编辑打开浏览器,她们一定想不到昨晚大家准备经历一场厮杀….
系统持续平稳地运行了一周,然后是漫长的五一,我回湖北黄冈老家休息了八天。回来时,一切依旧。