经过前三篇文章的准备,终于到了可以优化慢查询到阶段了,一个好的API,响应客户端的时间当然是越短越好,大体上如下图所示。
![0f7e70b93a0ce05530a61143d7a9e9d1.png](https://i-blog.csdnimg.cn/blog_migrate/82ef89984b2df4e765553b54755cb1fe.jpeg)
基本上API的响应时间从发起Request 到 Response要控制在1.0s之内,这是一个大家能接受的值,在加上前端UI到渲染,基本上加载一个页面在1.5秒之内,用户会感觉到流畅,用户体验很好,随着响应时间的增加,用户体验会越来越差,直到用户完全没办法接受。
前段时间,线上有一个API,是用来统计 成员进度的报表「统计一个团队内所有人的任务进度包括 」如下图展示的效果图
![7a2061ffbd9522972e51f8cda769a515.png](https://i-blog.csdnimg.cn/blog_migrate/3fa5d2bb3ed8ff67d12fd13ed3daee2c.jpeg)
奇慢无比,甚至超出了Request请求的限制时间,直接报错,由于这是一个老的API,运行了好几年了没问题,突然在发布一个新版本的时候就挂掉了,原本以为是做迭代功能或者改Bug引起的,很快在测试服务器上,按照在生产环境的操作步骤,在同一个团队下面进行测试,结果不能复现,奇怪了,不知道哪里出的问题,在测试服务器上面这个API的响应效率还非常的高效,然后在本地开发环境,运行起来,去测试,发现也很快,没有任何错误,看来不是代码bug的问题,于是继续排查,在生产环境,新注册了一个团队,然后运行,依然没有问题,奇怪了,唯独这一个团队有问题,这时才想起来,可能是数据量大的原因导致的,然后继续排查,查看这个API返回的报错信息,是一个Code:96的错误码,然后用这个错误码,在项目中搜索没有这个错误码,这时候基本上就定位了,是一个未捕获的异常,无奈之下google吧,MongoDB code 96 错误解析 「mongodb在排序时,如果排序字段没有索引,那么排序数据占用的内存空间不能超过32MB」解决思路进一步的清晰起来了,就是数据量大的原因,导致的,因为这个在UI前端还用分页来展示数据,服务端对数据首先进行了排序,然后返回给客户端,采用的是sort ,skip, limit的机制来实现的。
好了,开始阅读这个API的代码,代码不算多200 -- 300行,大概了解了API的具体实现,开启Debug模式,在mongoose 发给 MongoDB 这个最底层的方法,打一个断点,抓取查询语句,看看这个API到底是怎么查询数据库的,一切顺利,成功的拿到了原生的Sql 语句,直接在Mongodb client 运行这个语句,报了一个同样的错误,非常兴奋,问题终于定位了。下图是主要问题的代码片段:
![848a1b15ee8c3af2078871daecb36bfe.png](https://i-blog.csdnimg.cn/blog_migrate/5c3d06e69536aece339de91d91e14da2.jpeg)
但是为什么会报这个错误呢,排序字段是_id,这个可是Mongodb自己默认创建索引的字段啊,为什么不走这个索引啊,在看下面的代码就一目了然了,这里强制命中了一个其它的索引,这个索引没有_id这个字段,所以报错了,强制命中一个索引,估计当时也是为了提高效率才这么写的,去掉这个强制命中的索引,让mongo自己选择合适的索引来查询,不报错了,但是估计性能也会慢成狗,那好,问题来了,既然的强制命中一个索引,而这个索引还必须包含_Id字段和原来的字段,答案是显而易见的,需要新创建一个我们前面提到的复合索引,包括所需要的字段,然后它强制命中这个新创建的索引,OK,思路有了,马上动手,创建复合索引在相关的表上,索引创建成功之后,开始用PostMan发请求来测试,结果还是很慢,但是基本上不报错了,API的响应速度在10s,奔溃,搞半天,没啥大进步啊。
![777ea2164cb561b948c2a68a3976c670.png](https://i-blog.csdnimg.cn/blog_migrate/3d3b9321deac817b4597c9a85d6d5dfb.jpeg)
想想一百多万的数据,mongo不至于是这性能,肯定是哪里出问题,继续翻代码,debug调试,抓取Sql语句,发现抓取的Sql 语句直接在mongo里面运行很快啊,毫秒级的,怎么程序运行起来就10S了继续排查,如下图
![bcce8f05e1b105a5e06d47f07274eeac.png](https://i-blog.csdnimg.cn/blog_migrate/6e2d12b4ea393dc9ace4115a9d55a007.jpeg)
有点经验的估计反应过来了,估计是数据大,从数据库传输到服务端估计用了很长的时间,继续排查,把服务端用来计算的数据,保存到一个Json文件中,结果发现,这个文件大约50MB,难怪这么慢了,就算数据库查询很快,把查询到的数据,返回到服务端,50MB也够喝一壶的了,那问题来了? 为啥会从数据库返回这么多数据来统计呢,原因很简单,就是这个报表统计相对来说比较复杂,涉及到, users, teams ,workload ,project,task 这几张主要的表,因为要统计各种工时的状态,和计算完成率,延误率等,所以把一个team 内的所有的用户的工时信息返回到内存中来计算,统计。理论上一个team的user 不会太多,一个人一天也工作不了几个任务,但是随着时间的推移,任务数的增加,用户的增加,就慢慢的出问题了。问题来了,就继续死磕....
既然是回传数据导致慢的,那能不能把计算的这部分放到数据库层面来做呢 ,在以前的关系型数据库,例如sql server 直接写个存储过程就搞定了,但是mongo好像对存储过程支持的不是很好啊 ,查阅半天资料,还真可以搞个Mongo版本的存储过程来实现这个复杂逻辑的统计,然后把统计的最后结果发给服务端。
总结
一个慢的查询优化是一个复杂的过程,更多的时候不是一步就能定位问题,要有耐心,细细的排查,总结。
首先,重现线上的生产环境的错误,分析线上运行的环境,数据库的版本,服务器的性能,是稳定重现呢,还是部分重现,然后在本地尽可能的搭建一个贴近生产环境的环境,然后重试,直到能稳定重现这个问题,然后在进一步的想解决方案,这个过程可能要涉及多个部门的合作,需要一定的协调性,因为研发部门,一般是没有,线上生产环境的数据库查询权限的,所以运维部门来一起配合完成。
参考文章
mongodb存储过程 - 绿茶叶 - 博客园www.cnblogs.com![e90f8a2684b13bbba902c4c2015b5718.png](https://i-blog.csdnimg.cn/blog_migrate/26b6feebf16aaf1fd5f8d02414155ae6.png)