接上回,经过一番艰苦的调试和观察,我们已经发现了一些线索。本篇,我们将循着这些线索,进一步深入挖掘内存居高不下的奥秘。
ViewScoped Bean 虽然看似生命周期很短,View在ViewScoped Bean在。但是,仔细深究就会发现一个残酷的现实,ViewScoped的实现是基于Session的,只要你使用了 ServerSideStore 的策略,不可避免的会导致Session因此而膨胀。Session膨胀带来的后果,就是一旦ViewBean里面引用了大量的体积庞大的Bean,在Session没有过期或者没有人为退出的情况下,占用的内存会一直不能释放。
图1. Article Mem – mat
继续上回的mat分析,来看看这个对象不释放有多严重。图1中我标记了两处,第一个当然内存占用大户 char[],第二处就是被ViewBean引用而不得释放的 Article 对象。有人要说,图上看起来,Article 占用也不算多啊,你怎么就信口开河说是这个对象的问题呢?好的,再请看下图。
图2. Article Count – mat
图2中我标记了一处,在这个dump文件中,显示 Article 对象目前有 3300 多个,而据我问某同学 ,实际的 Article 数量也就70左右。到这里,读者恐怕也应该意识到什么问题了吧?对啦,Article 冗余了,被重复查询存储了。Article 对象里面存储的是人见人恨的大字段,没错,由于字符串的背后存储又是由 char[] 完成的,所以,String 的内存占用会体现在 char[] 上,才导致堆dump中的 char[] 一家独大,非常大!这也就解释了为什么char[] 数量只有区区 65409 个,在总内存占用 157.8MB的情况下,单此一项就占到 149MB 之巨!非常惊人,可以说整个JVM的对空间几乎都被它给占了。更要命的是,服务器事实上才启动半小时,解决这个问题刻不容缓。
了解到此项目是用的 JSF + Spring + PrimeFaces 混搭实现的,由于Spring没有提供ViewScoped支持,自行实现了一个。这里我们暂不去讨论如何实现一个更好的ViewScoped。因为通过测试发现,即便是原生JSF的ViewScope,也一样存在这类问题。我们需要讨论和解决的是一个相对通用的解决方案,另外,针对JSF做一些适当的优化和调整。
还有一个细节比较耐人寻味,由于本着能凑合绝不卖力气的屌丝精神,该同学直接使用了 PrimeFaces 的分页条控件,而该控件实际背后的实现是一个假分页,也就是实际上,虽然名义上每次查看一页(比如10条),但是也会把整个库所有记录全部查出,然后取出这一页。这个是非常糟糕的一件事情。第一,这给数据库带来的巨大的压力,每访问一次列表页,就查询全体数据一次,全部字段,一个不落。第二,假如我的库里有10000篇文章,那么,每次一刷新列表,要显示10条的概要,也得查出10000来,截取10条出来。50个人过来访问,50个Session,每人就看一下列表页面,那么就有 50×10000 个大对象成功的驻留在了堆里,恭喜你,30分钟之后才能释放。假如这个时候,来的不是50个人,是5000个人,对不起,你的服务器已溢出。
抛开JSF不谈,就从互联网站点高性能、高可用的基本观点上,该站就存在不少的问题,比如直接让应用服务器暴露于前端流量入口,比如,整站没有采用任何的缓存措施,机器资源和带宽浪费很严重。所有流量直达数据库,这也得亏是流量不大,流量稍微大点,首先死的就是数据库。
罗罗嗦嗦说了这么多,本来打算这篇挖掘一下JSF的代码,来发现点什么。奈何机器不给力,也没找到趁手的 UML 绘图工具,那就暂且放一放,先来谈谈怎么解决这类问题。
既然是流量穿透到了后端,甚至已经压到了数据上,那么就先要解决的就是想办法把流量挡住,前端挡的越多,数据库越轻松,并发性自然也就更好。
图3. cache arch
图3 是我觉得一个理想的、由Java构建的小站,应该有的基本架构(记得最简,暂不讨论分词索引之类的进阶内容)。在你的可控范围,两层缓存,足以应付相当的流量冲击。至于本图的风格,我认为是返璞归真,你怎么看?
俗话说的好,麻雀虽小,五脏俱全。哪怕你的站再小,流量再少,如果不做一些基本的防护措施,一旦流量稍有波动,马上死给你看。下一篇将从 A 和 B 两个点的缓存,结合JSF来探讨如何去优化JSF站点。
(待续)
Bill
2014-01-03
PS: 过一年,老一岁,刚刚写日期还是习惯的写出2013来,舍不呀舍不得。