不知道从哪天开始,在开启 SchedulerJob(定时任务) 的情况下,产品环境的数据库连接在一天之内全被系统占满了(MySQL开了200个连接),只能重启数据库,重启服务。因为之前在系统中使用了C3p0数据库连接池,猜想可能是c3p0的配置问题或本省的问题?于是去掉数据库连接池,恢复到以前的设置,运行了一天,没有出现问题。没想到跑了两三天之后,后台直接死掉了,查看日志,是EntityManager nullpoint,似乎又是数据库连接拿不到?于是想到了以下的原因:
数据库连接不释放?
可能是运行后数据库连接不释放?长期累积导致数据库连接数耗尽?于是自己写了小测试来调用dao方法,做数据库操作。做了20000次顺序操作,没问题,数据库连接没有超过设定的连接池最大上限。做了3000次(3000个线程)并发操作,总体连接数没有超过数据库连接池的上限(maxPoolSize)上限。这就奇怪了,既然我压力测试dao接口,连接数没出现异常,那问题在哪里?那可能只是跑SchedulerJob的时候出现问题了?
内存占用过多?
将BatchJob的频率提高(每秒一次),使用YourKit对application进行监控,发现内存上升的非常快,没跑到一个小时就把设定的512M最大内存跑完了,请看下面两张图
Figure 1 在系统运行的短短一分钟之内,app就使用了近140M内存
Figure 2 在不到一个小时的时间之内占用了460M内存,接近设定的head最大值
查看系统Log ,发现系统在不断地产生新的Service类(假设为 SchedulerJobService ),怪不得系统内存消耗得特别快,那在Spring 配置文件中(applicationContext.xml)中将这个service的scope改为singleton不就好了吗?
产生了太多的Spring ApplicationContext(IoC Container)实例!
Spring reference 3. The IoC container
改之,以为万事大吉,再跑,发现还是不断地产生SchedulerJobService,难道scope改错了,查看文档,没错啊。彷徨之际只能再回过头看一下写的代码,我们用的是Quartz来做SchedulerJob的,在执行每一个Job的时候需要拿到 SchedulerJobService 的实例,这个实例当然就配置在Spring ApplicationContext里面啦。问题终于找到了!在拿SchedulerJobService这个Bean的时候我们是这么写的。
private ApplicationContext appContext = new ClassPathXmlApplicationContext("applicationContext.xml ");
每次都创建了一个ApplicationContext ,当然包括每一个Bean的实例化,Bean之间的依赖关系,以及创建一些数据库连接,这是在ApplicationContext中配置好的。
这就难怪了,不管我在ApplicationContext设置数据库最大连接数还是设置Scope都不管用了。因为现在面临的问题已经超出了单个ApplicationContext的管辖范围了。
假设SechdularJob是15分钟跑一次,那一个小时就会产生4个ApplicationContext, 一天就会产生96个ApplicationContext,即使我限制了在一个ApplicationContext中的最大连接数是10,可是一天下来产生的连接数可能有960之巨。在Mysql中默认的等待时间(wait_timeout)是8个小时,也就是说,一旦创建了一个连接,即使什么都没做,MYSQL也会在8个小时之后关闭这个连接。同时也解释了为什么内存消耗如此快的原因了。
wait_timeout, 详见MySQL system variable:wait_timeout
这就好像遇到了黄牛党,虽然每人限购两台,但是派了100 个黄牛过来,一下子就被抢购了200 台。
简单改之,变成静态变量
private static ApplicationContext appContext = new ClassPathXmlApplicationContext("applicationContext.xml ");
保证一个Job只有一个ApplicationContext。改完后测试,问题大大改善,见下面两图
Figure 3启动一分钟之后,内存使用相当平稳
Figure 4跑了40分钟不到,最大内存也只有120M
内存消耗比较稳定,没有明显的上升趋势。
进一步改进
刚才的那个方案只是保证了每一个Job只产生一个ApplicationContext,如果有两个SchedularJob的话还会产生两个ApplicationContext. 加上原有的一个ApplicationContext(随Web容器启动的),有好几个独立的ApplicationContext,其实他们之间完全可以共享一个ApplicationContext。所以接下来的改进步骤
1. 使用同一个ApplicationContext
2. Quartz使用Spring来管理
延伸阅读
· YourKit – Java Profiler (可选:jconsole, 特方便,JDK自带,但是比较迟钝,仅限不时之需)
· Quartz