求生欲很强的数据库
最近一个月来一直在做某局的项目,涉及到了微信支付相关的业务。业务本身也是一套完整的从下单到支付到退款的全流程,我司和三方联调开发,个中滋味不表。
其中从最开始就遇到一个反复出现(偶发性)的问题,有时候数据库无法快速且正常返回查询结果。我先后从多个方面进行尝试调试,寻求解决办法。
我先还原一下场景。
服务器:2台a和b。
负载均衡:访问任意到打到a或者b。
数据库:前期没有使用持续化存储,用的热数据缓存。此次使用mongo,最开始版本3.0.x,后升级到我司其他线上项目运行版本3.2.6。a和b公用同一台database service,且mongo安装在b上。
现象:数据库有时候能返回查询结果,有时候好像卡死在那里,从接口调用到最终数据库服务本身皆无错误日志。我查询的那个集合只有50多条记录,并且我增加了索引在我的查询字段上,按照道理来说应该是毫秒级别的耗时。
首先我先从项目数据库调用入手,写了最小化的脚本,单独去读取某一条订单记录,能快速且正常返回。至此说明项目是用的mongo driver是能正常调用到mongo的。后我开启mongoose debug模式,开启方式为在我操作类里添加如下语句:
mongoose.set(‘debug’, true);
开启后,每当调用到数据库操作会输出类似mysql的sql语句,更方便调试跟踪问题。然而我看到的现象是,有时候能正常执行查询的时候就有该debug日志吞吐,而有时候没有任何输出,也无数据返回。
期间,我反复尝试安装并部署不同版本的mongo和对应版本的mongoose,甚至调整了项目框架版本,先排除了各模块之间兼容性问题。
后我怀疑到是否是负载均衡到了a(mongo是安装在b上),而a不能访问到b上的mongo呢?
我查询相关资料,修改了mongo的配置文件,将bind_ip从127.0.0.1改为0.0.0.0,且把maxConns改为2000。试探性地临时关闭了iptables服务,然而没有解决该问题。
在这个过程里面我有意识到,每当重启服务之后短暂的十几秒里面,可以正常进行查询,最多能成功2次,到第三次的时候就会前端调用超时报错。
后为了能顺利进行后续开发(因为很多接口是用到数据库存储本地数据的),我暂时关闭了a上的项目服务,先排除因为网络不通造成的问题。到这个时候数据库查询就开始正常了一段时间,我的开发也能顺利进行下去。
我那个时候暂时把问题归结于网络,因为脚本本地调用是ok的,关闭了负载,b上本地部署mongo调用也是正常的。我和某科的老师一起排查问题,a和b两台机子相互ping和telnet均成功,说明数据链路层是通的,问题应该是在应用层上的。通过尝试添加output设置,将27017端口暴露出去。然后重启a上的服务,再次进行负载均衡,惊喜地发现两边都可以正常使用mongo查询了。到这个时候,我再次把问题归结于网络设置导致每次请求转到a的时候无法连接到mongo,而有时候负载到b是正常的,所以才会偶发性地出现查询结果无法返回导致超时的问题。
Xx大会如期上线了我们的项目,大会期间非常稳定,此后的一周我再也没想起这段为了解决诡异bug而烦恼的时光。直到后面和某海联调退费接口的时候,再次出现该问题。
说实话,到这次复现问题的时候,我是有点没有思路。因为之前几乎把所有可能性都尝试过了。在同事探讨和帮助的过程,我想到了会不会是有其他代码引起mongo操作阻塞呢?
在网上也有人遇到过类似问题,讨论回复如下:
有个比较常见的坑是mongodb使用数据库级的锁,在写数据的时候要快,否则很容易把其他mongodb的请求阻塞。你可以尝试在mongodb起来的时候是就开起mongostat,然后查看locked%和qr|qw列的值是否出现异常,飙升。
还有如果collection中记录很多的话,一定要建立索引。你可以直接在mongo shell下验证你的查询是否有用到索引,我记得mongo语句后跟explain()可以显示索引是否起作用的。
简单说就是有频繁写入的操作和频繁未命中索引(包括无索造成引)查询(被查询的集合数据量较大)的逻辑代码导致数据库级别锁,让其他请求一直排队等待。我想起了每次查看日志的时候总有调用人社接口的日志大量输出,而在那个公共调用的代码里面有一段读写mongo的操作代码。
我仔细看了一下,这段历史遗留代码是为了缓存从人社接口拉取到的数据,其他的我不说了,缓存这段代码毫无意义,首先从量级来看全cd人的数据有无必要存是其一,其二查询没有索引,业务场景上是存储的数据是只增不减的,数据数量越来越大,查询会非常慢。其三我全文搜索也没有找到用到这个数据的其他地方。经过查询,该集合数据量已经超过200万条。
我先屏蔽了该段代码,重启服务后发现之前timeout的接口马上毫秒级返回了数据。多次尝试跑通了全支付流程,再无mongo查询异常出现。之后和写这段代码的前同事沟通后确认该代码是可以删除的,至此该问题尘埃落地。
所以,之前时好时坏,只是因为数据库顽强的求生欲而已。本质是因为那段历史遗留代码会非常频繁地读写数据库,导致其他请求排队等待。通过在这次排查问题,我又一次对分析问题有了新的思考:
- 对一些临时接手后有大量历史遗留问题的项目,一定要深入细节,不能因为是继承的代码,默认它是正常无错的,关于那段存储人社数据的代码我也曾经怀疑过,但是没有去深究,默认了它一直使用就是不能轻易改动的。
- 结对编程是很重要的。这在一些其他公司已经很常见了,对于小的创业公司,我们考虑更多的是一个工程师去support一个或者多个完整的项目,有时候个人的思路是非常有限的,常见的问题是很容易通过自身努力解决,而一些思路有死角的疑难杂症可能需要群策。我这里说的结对编程特指对该项目负责且全程参与的人。因为往往一些google都很难找到特别接近的解决方案的问题,往往有项目自身特殊原因。
God is in details.God bless my code.