那天下午,正在愉快地摸鱼,突然工作群里炸了:
张三:卧槽,线上系统怎么这么慢?
李四:你网络有问题吧,我这边还好啊
张三:我特意换了个网络,还是慢得要死
王五:奇怪,其他网站都正常,就我们系统慢,服务器挂了?
赵六:等等吧,可能过一会就好了
结果这一等不要紧,系统越来越慢,有的人直接进不去了,有的人页面加载超时。
王麻子:我本地开发环境一点事没有,飞快
刘能子:我本地连线上后端,卡得要死(前端同学,直接用的线上接口)
我一听这个对比就知道问题所在了。本地环境用的测试数据库,线上用的生产数据库。既然本地快、线上慢,那问题八成出在生产数据库上。
赶紧登录服务器,进入MySQL看看到底什么情况。
SHOW PROCESSLIST;
这个命令能看到当前MySQL所有正在执行的操作,一跑出来我就傻眼了。
有一条update语句已经跑了300多秒了!而且还在updating状态,下面排了一堆等待的查询。
看到这我就明白了,肯定是有人在跑大批量的update操作,把整个数据库都堵住了。
群里问了一圈,果然有同事承认了:她刚才执行了一个update语句,而且更新的数据量是几百万条!
我当时就懵了,几百万条数据,这是要干啥?
另一个同事着急问:这个可以kill掉吗?然后就直接kill了。结果kill完之后,那条SQL的状态变成了killed,但是还在那里,时间还在增长。
我一看就知道不妙了:事务回滚了!
更新几百万条数据已经够恐怖了,现在还要回滚几百万条,这简直是雪上加霜啊。
光知道有update在跑还不够,我得看看具体的事务状态:
SELECT * FROM information_schema.innodb_trx;
这个结果让我倒吸一口凉气!
从图片可以看出,现在有好几个事务:
trx_weight这个数字代表什么意思? 简单来说就是这个事务影响了多少数据。386万的权重,意味着这个事务修改或锁定了386万行数据!
现在我彻底明白了:有人执行了巨大的update操作,然后被kill掉了,现在MySQL正在疯狂回滚这些修改。而回滚的过程中,这些数据依然是被锁定的,所以其他查询统统被阻塞了。
这就像是高速公路上翻了几辆大货车,后面所有车都得堵着等清理完才能通行。
这时候就面临选择了,要么暴力解决,要么等着。
最简单粗暴的办法,直接重启MySQL服务,什么事务、什么回滚,统统不要了。
但是这样搞风险很大:
第二种方法就是躺平,老老实实等回滚完成。
我们讨论了一下,最终还是选择了等待。毕竟生产数据不能出问题,宁可慢一点也不能冒险。
结果等了足足1个多小时,回滚才完成,系统速度也恢复了正常。
这次事故让我对大批量update有了更深的认识。
我们这个表有500万条数据,同事这次update了100多万条。这种规模的操作为什么会把整个数据库拖慢呢?
最直接的原因:锁!
MySQL在执行update的时候会给相关的行加锁,100万条数据就是100万把锁。其他所有想要查询这些数据的操作都得排队等着。
还有个更要命的:回滚!
当我们kill掉那个update语句后,MySQL不会简单地停止,而是要把已经修改的数据全部撤销回去。你想想,如果已经update了50万条,那么回滚的时候就要撤销50万条,这个过程可能比原来的update还要慢!
再加上索引更新:
每次数据修改,相关的索引也要跟着改。我们这个表上有好几个索引,等于是一次update要同时维护多个索引,CPU和磁盘IO都吃得很紧。
这次事故之后,我专门研究了一下大批量数据更新的正确做法。如果真的需要更新几百万条数据,应该怎么搞?
最重要的原则:小步快跑
不要一次性update几百万条,要分批次来:
-- 每次更新1万条,然后暂停一下
UPDATE your_table
SET column1 = new_value
WHERE condition
LIMIT 10000;
-- 暂停几秒钟,让其他查询有机会执行
-- 然后继续下一批
这样做的好处:
千万别在业务高峰期搞大批量操作!
我们这次就是踩了这个坑,下午2-3点正好是用户活跃时间,结果把所有用户都影响了。
以后这种操作:
这种大批量操作不能偷偷摸摸搞,要提前通知:
我们现在制定了规范:超过10万条数据的操作必须提前申请。
这次事故之后,我也趁机优化了一下MySQL的配置。有两个参数平时大家可能不太关注,但我觉得挺重要的:
SET GLOBAL innodb_buffer_pool_size = 8G;
这个参数就是告诉MySQL用多少内存来缓存数据。设置大一点,更多数据能放在内存里,读写速度会快很多。
我们服务器有16G内存,我给MySQL分了8G。这样大部分热点数据都能缓存在内存里,不用频繁读磁盘。
SET GLOBAL innodb_io_capacity = 2000;
这个参数控制MySQL每秒能做多少IO操作。如果你的磁盘性能比较好(比如SSD),可以调大这个值,让MySQL更充分地利用磁盘性能。
我们用的是SSD,IOPS挺高的,所以调到了2000。这样在做大批量操作的时候,MySQL能更快地把数据写到磁盘上。
几百万条数据的update,这在生产环境就是个定时炸弹。我们现在规定:
以前我们对数据库权限管得比较松,开发人员基本都有线上数据库的访问权限。现在看来这样不行,得分级管理:
如果我们有完善的监控,这种情况应该能更早发现。我们现在增加了:
这次我们就是没准备,临时讨论要不要重启,耽误了不少时间。现在我们有了标准流程:
总的来说,这次虽然闹得挺大,但也让我们的数据库管理更规范了。现在回想起来,当时那个场景还挺搞笑的,一群人在群里讨论为什么这么慢,结果是有人在后台偷偷update几百万条数据😂
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。