事发情况
当天晚上生产服务一直在告警,刚刚开始只是一个数据保存接口耗时过长,紧接着整个服务接口都耗时,对外表现为整个服务不可用。
于是优先排查宽带情况,其次看代码逻辑,看错误日志(无一例外都是接口耗时过长报警)。
接着看监控发现goroutine明显增多,依然没有头绪。于是想到重启服务。
重启服务过程中,弹出一个告警日志-db连接数过多,已经超过筏值。这个时候问题才确定。
问题原因
是最初的那个数据保存接口,逻辑很简单,接口拿到请求时,insert或者update进入db内。问题在于update时未对where的条件加上索引,导致数据量大时,会锁住全表,导致整个服务不可用。
该接口上线一个多月稳定,事发晚上数据量达到某一临界点,update时锁住全表,后面该接口的请求一直在等待前面的响应。随着请求数量不断增多,从而引发雪崩效应。
定位原因后排查日志,发现错误信息:
|
|
为什么该表被锁会影响到服务其他业务响应
一个服务内有配置数据库的连接数上限,而这个接口因为锁表了,导致把连接数占光。从而影响服务其他正常业务的响应。
同时go中每个http请求底层都会new一个goroutine出来,当大量请求访问时,也就解释了为什么goroutine数量会暴涨。
反思
- 先从排查方向来说,当时排查带宽时思路是错误的,如果带宽有问题那么其他服务应该也是异常的,而不只是这个服务。
- 当一个服务不可用时,应当优先考虑重启该服务或者回滚到之前版本,而不是先分析错误原因。
- mysql的存储引擎用的是innodb,update时基于
next-key lock
,如果不加上索引会锁住整张表进行操作,这是个大坑。之前本人没注意过该问题 - 一些关键的地方开发时没有及时加上短信/邮件告警及时通知开发人员,导致这么久错误才被定位到,也浪费了大量的排查时间。
该问题更优解决方案
即使加了索引,但是mysql update时也有可能会锁住全表(可以通过explain
查看)。以使用 force index([index_name]) 可以告诉优化器使用哪个索引,以此避免有几率锁全表带来的隐患。
面对这种频繁更新的需求,维护索引也是有很高的代价。最适合方案应该考虑非关系型数据库来做存储(如mongodb
)