场景:公司某项目,采用mongoDB存一些数据更新日志(XX时候导入了多少条数据这样的),然后在某个小角落给了个按钮,点进去可以查这些数据
分析:按照业务来说,数据更新日志并不会非常的多(一个用户可能一个月都导不了几次数据),然后更新日志的查询量也不会很高,毕竟是在边边角角的一个按钮而已
实际部署之后:我们的部署方式是8个tomcat,对1个mongodb,在8个tomcat全部启动之后,发现mongodb连接数瞬间飙升至全满,检查mongodb.log,报“connection refused because too many open connections: 819”错误,这个的意思是“连接数已经达到819了,无法再新增连接”(非直译),并且应用还持续的想要申请更多的连接,但一直被拒绝。另外,从应用方面看,无法读取或写入任何mongodb的数据。
故障排查:虽然对这么小的查询量和更新量会导致连接数飙升感到很诧异,但当时并不了解mongodb的整个运行机制等知识,只能从想的到的方面入手,首先,第一个想到的,就是调大连接数。
1、调大连接数可以修改mongodb.conf文件中的maxConns=3000,修改后,重新启动就生效,我这里连接数给改成了3000。
2、这时候问题来了,要重新启动,就要先关闭mongodb,网络上查关闭mongodb的方法大部分都是说先用mongo登进去,然后use admin 再 db.shutdownServer() 来关闭mongodb,问题是现在已经连接数全满了,mongo根本是登不进去的。
3、考虑采用kill -9?不可以,mongodb不能用杀进程的方式关,可能会引起数据丢失或者损坏,更何况这是生产库。
4、经过多方查询,发现可以这么关:./mongod --shutdown --config mongodb.conf (就是在启动的命令上加入一个--shutdown),就可以正常关闭mongodb了
5、关闭之后重新启动,瞬间又全满,再查日志,还是819,为什么无效?因为这个最大连接数,不仅和mongodb本身有关系,还和操作系统有关系,linux操作系统默认只允许819(具体原理是linux的ulimit -a,然后看openfile,1024*0.8=819),那么这时候,就考虑改大操作系统限制,那么我采用ulimit -n 3000的方式改大了最大连接数,再重启mongodb,发现连接数升到2300+就停止了,然后稳定运行。
至此mongodb的连接数问题就告一段落,直到好几个月以后,业务变更,将更新日志的内容放到首页来展示(进入首页就要读取内容进行展示)。在这个功能上线的第二天,mongodb再次挂掉(其实上线当天就挂了,只是我们只测了刚刚上线之后的那一小段时间,没发现),再次重启mongo排查连接数,发现又变成819了,爆满。
继续排查,mongodb.conf文件中的maxConns=3000配置正常,但是ulimit -a中的openfile又变成了1024,导致连接数又只剩819(经过查资料发现ulimit -n 3000这种改法只对当前这一次shell连接有效,断开再连接就又变回1024了),那么再次改大。但3000仍然瞬间就满。此时意识到单纯扩大连接数不是个好的解决方法,要从应用入手,找出连接数暴增的原因。
1、将所有的tomcat都停掉,只启动一台,看看连接数有多少,经过测试,只启动一台的情况下,连接数达到了1300+,那么8台,连接数要破W,改大连接数显然不现实。
2、排查应用的代码,然后。。发现了非常典型的错误示范:
//这是一个controller中的一个方法
@RequestMapping("/xxx.do")
public Response pageLog(HttpServletRequest request) {
//获取host
String host = pro.getProperty("mongodb.host").trim();
//获取端口
int port = Integer.parseInt(pro.getProperty("mongodb.port").trim());
//根据host和端口获取mongoClient
MongoClient mongoClient = new MongoClient(host, port);
/*
..............用mongoClient 读取数据,处理业务逻辑................
*/
java中使用的是mongoClient这个工具来操作mongo,mongoClient本身就是一个连接池,在默认的情况下,这个连接池会开出10个(据说较新版本中是100个)连接,然后我们只要用mongoClient这个对象来进行通讯就好,具体的连接我们可以不用管。
那么,就把它当做连接池来看,一般来说,一个应用中,只需要一个连接池与数据源通信,然后池子里面有很多个连接,随拿随用,用完了放回池子里(大部分情况下,这部分细节都不需要我们管理)。
这段代码,在每次请求controller的时候初始化mongoClient,然后去处理数据,先不说这个初始化mongoClient的开销如何,首先,10个人请求10次,这就生成了100个连接池。。。。我只能说。。连接池能养鱼不?
3、那么问题明了,连接数暴增的原因是程序中错误使用mongoClient导致的。那么,正确是使用方式应该是这样
//这是一个controller中的一个方法
@RequestMapping("/xxx.do")
public Response pageLog(HttpServletRequest request) {
//MongoClients里面有一个静态的公共的mongoClient,每次有需要用的时候
//都找这个MongoClients去拿一下公共的mongoClient
MongoClient mongoClient= MongoClients.getMongoClient();
/*
..............用mongoClient 读取数据,处理业务逻辑................
*/
更改完成之后,将代码发布到生产环境测试,8台tomcat,共计使用mongoDB连接数:60+
从原来的单台1300+到现在的8台60+,原来是浪费了多少资源啊。。