MySQL上云后引发的雪崩

甘兵 2018-05-10 22:34:30

作者介绍

甘兵高级运维工程师,6年运维工作经验。曾就职于国家互联网应急中心、天音控股等企业。拥有丰富的系统运维经验、大型网络架构设计经验。热衷于开源技术的研究,关注Docker、DevOps等技术方向

注:本文系原创投稿,未经 DBAplus社群及作者 允许,不得擅自转载。

 

一、事件起源

 

整个事件的起源还要从笔者最近入职了一家区块链金融公司说起,公司业务发展比较迅猛,突破百万用户也是近在眼前。整个×××都在阿里云上运行,每天都能看到用户的不断增长,既兴奋又担忧,为什么这么说呢?

 

由于笔者入职的时候这里的业务就已经上线了,×××接过来之后,快速了解了所有的应用服务都是在Docker Swarm跑起来的,也包括MySQL数据库,以至于笔者有了迁库的想法:按照这种用户量发展下去,MySQL在容器中运行用不了多久肯定会撑不住。

 

笔者隐隐担忧起来,毕竟不想每天提心吊胆地做运维,所以立马重新规划了新的方案和大家一起探讨。最终,总监和相关技术负责人都敲定用RDS作为数据库新的方案,正所谓“天下武功,唯快不破”,于是我们立马动身干起来。

 

二、迁移计划

 

1
 
原架构图

 

分析架构图

 

1、从入口层(CDN)→到安全层(WAF)→ 最后到达应用层 (ECS集群);
2、Docker Swarm打通了ECS集群中的每台服务器,在每台ECS宿主机安装Docker Engine并部署了公司需要的应用服务和数据库(Nginx、PHP、Redis、MySQL等);
3、MySQL容器通过宿主机的本地(目录)挂载到容器中实现数据持久化;
4、业务项目以PHP为主,PHP也是运行在容器中,通过PHP指定的配置文件连接到MySQL容器中。

 

其中一个库的docker-compose yaml文件,笔者就随便展示一下其中一个库的YAML文件,比较简单:

 

 

version: "3"services:

  ussbao:

    # replace username/repo:tag with your name and image details

    image: 隐藏此镜像信息

    deploy:

      replicas: 1

      restart_policy:

        condition: on-failure

    environment:

      MYSQL_ROOT_PASSWORD: 隐藏此信息

    volumes:

      - "/data//mysql/db1/:/var/lib/mysql/"

      - "/etc/localtime:/etc/localtime"

      - "/etc/timezone:/etc/timezone"networks:

  default:

    external:

      name: 隐藏此信息

 

从上面的信息可以看出来,每个库只运行了一个MySQL容器,并没有主从或读写分离的方案,而且也没有对数据库进行任何优化,数据库这样跑下去让笔者很担忧,正常来说,都会把数据库独立部署运行。

 

2
 
调整后的架构图

 

 


从上图可以看出,笔者只是把MySQL独立出来了,开通RDS实例来跑数据库,当然还开通了其它的一些服务(比如OSS、云Redis等),这些不是本文的重点,就没有画出来。Nginx和PHP服务还是在Docker Swarm中运行。本文只是对迁移后出了问题的库进行分享。

 

下面来看看迁移的方案:

 

3
 
迁移流程方案

 

开通RDS实例→备份SQL→导入到RDS→修改数据库配置文件→测试验证

 

1、根据业务量规划开通RDS实例,创建数据库和用户;
2、提前做好RDS白名单,添加允许访问RDS的IP地址;
3、MySQL Dump备份Docker中的MySQL
4、把备份好的.sql文件导入到RDS中;
5、修改PHP项目的数据库配置文件;
6、清空PHP项目的缓存文件或目录;
7、测试验证;
8、RDS定时备份。

 

具体迁移细节就不展示了,笔者是在夜深人静的时候进行迁移操作的,确定大半夜没人访问我们的APP和网站了才开始干的。

 

我们的业务情况还有点像股市,是晚上12点不许操作和交易,第二天早上9点开盘,9点钟是并发的高峰期,就像朝阳大悦城上午开门一样,大批的顾客同时并发过来。

 

所以那天晚上在12点15分准时开干,按计划和提前准备的配置、命令、脚本进行操作。非常顺利地把Docker中运行的MySQL迁移到RDS上,好几个库的迁移不到半个小时就结束了,并且把网站和APP的流程都跑了一遍,也都是妥妥的,最终把提前准备好的备份脚本放在crontab中定时执行,可以来看下脚本内容:

 

 

#!/bin/bash#数据库IP

dbserver='*******'#数据库用户名

dbuser='ganbing'#数据库密码

dbpasswd='************'#备份数据库,多个库用空格隔开

dbname='db1 db2 db3'#备份时间

backtime=`date +%Y%m%d%H%M`

out_time=`date +%Y%m%d%H%M%S`#备份输出路径

backpath='/data/backup/mysql/'

logpath=''/data/backup/logs/'

echo "################## ${backtime} #############################" 

echo "开始备份" #日志记录头部

echo "" >> ${logpath}/${dbname}_back.log

echo "-------------------------------------------------" >> ${logpath}/${dbname}_back.log

echo "备份时间为${backtime},备份数据库 ${dbname} 开始" >> ${logpath}/${dbname}_back.log

#正式备份数据库

for DB in $dbname; do

  source=`/usr/bin/mysqldump  -h ${dbserver} -u ${dbuser} -p${dbpasswd} ${DB} > ${backpath}/${DB}-${out_time}.sql` 2>> ${backpath}/mysqlback.log;

  #备份成功以下操作

  if [ "$?" == 0 ];then

    cd $backpath

    #为节约硬盘空间,将数据库压缩

    tar zcf ${DB}-${backtime}.tar.gz ${DB}-${backtime}.sql > /dev/null

    #删除原始文件,只留压缩后文件

    rm -f ${DB}-${backtime}.sql

    #删除15天前备份,也就是只保存15天内的备份

    find $backpath -name "*.tar.gz" -type f -mtime +15 -exec rm -rf {} \; > /dev/null 2>&1

    echo "数据库 ${dbname} 备份成功!!" >> ${logpath}/${dbname}_back.log

  else

  #备份失败则进行以下操作

  echo "数据库 ${dbname} 备份失败!!" >> ${logpath}/${dbname}_back.log

  fi

done

echo "完成备份"

echo "################## ${backtime} #############################"

 

到了1点钟,确定没问题后发通知到群里,发微信给领导表示已迁移完成,进行很顺利,然后笔者打车回家,睡觉。

 

三、雪崩来临

 

其实这一夜笔者睡得也不踏实,到了早上8点半就醒了,因为我们9点开盘,会有大量的客户涌进,每天开始产生新的交易(买入和卖出),给大家看下截图:

 

果不其然,9点过后,笔者打开APP,一切正常,但点击切换几个界面后,就发现其中一个功能的请求超时了,一直在转,然后紧接着其它功能也超时了。

 

完了,出问题了。我赶紧开笔记本查问题,过了一会儿群里就开始沸腾了(反映好多客户打开APP都显示请求超时了),我的电话也立马响了,技术总监打来的,问我怎么回事,我说正在开笔记本排查。

 

这个时候,我要说明一下:运维人员此时需要冷静并且安静地处理问题,公司领导千万别催得太急,以免打乱处理人的思路。我们总监临场处理能力做得真是非常到位,马上跟我说不用担心上面压力,有他扛着,叫我只管排查和解决问题。

 

四、紧急处理

 

1
 
排查问题

 

笔记本打开后,首先想到的就是RDS数据库出了问题,登录阿里云,进入RDS中的DMS数据管理控制台,一进去就傻眼了——CPU爆了!这么多连接数,如下图:

 

 

 

进入会话去看看,发现会话“炸锅了”,几百页的select都挤在ub_user_calculate这个表中,这个表是数据量相对大一些,目前这张表有200多万条数据:

 

 

自然反应就是去查看此表的结构,发现此表没有索引,被惊讶到了,竟然没有索引?!这...... 

 

然后笔者返回源数据库查看这张表,也发现没有索引,由此可以确定我导过来的这张表就是没有创建索引,如下图:

 

 

 

分析:当数据库中出现访问表的SQL没创建索引导致全表扫描,如果表的数据量很大,扫描大量的数据,执行效率过慢,占用数据库连接,连接数堆积很快达到数据库的最大连接数设置,新的应用请求将会被拒绝,导致故障发生。

 

2
 
解决问题

 

赶紧把此事反应给开发负责人,表明问题根源找到了,会话锁死了,由其中的一张表没有索引而导致的,并询问需要给哪几个字段加索引,接着操作增加索引:

 

 

点击保存后,发现创建索引的SQL一直卡死着,如下图所示:

 


突然想起来还有一堆会话在那里,先Kill掉所有会话吧,不然索引肯定创建不了。然后又发现会话根本杀不完,如下图:

 

怎么办呢?会话杀不完……

 

没办法,先把访问入口切断吧,反正现在用户访问也超时,就果断先把域名停了,访问入口给切断了,然后再增加索引。

 

入口断了,索引也加上了,但发现CPU还下不去,如下图:

 

 

为了快速让CPU降下去,重启这个实例吧:

 

 

实例重启完后,CPU下去了,会话也下去了:

 

 

开启入口层的域名访问吧,再次观察现在的会话和CPU等状况,如下图:

 

 

 

这就对了,会话也正常了,随后通知领导业务恢复。

 

这是笔者后期补的一张图:再来看一下服务器CPU情况(迁移MySQL后的情况),明显逐渐好转。

 

 

五、索引使用策略及优化

 

1
 
创建索引

 

  • 在经常查询而不经常增删改操作的字段加索引。

  • order by与group by后应直接使用字段,而且字段应该是索引字段。

  • 一个表上的索引不应该超过6个。

  • 索引字段的长度固定,且长度较短。

  • 索引字段重复不能过多,如果某个字段为主键,那么这个字段不用设为索引。

  • 在过滤性高的字段上加索引。

 

2
 
使用索引注意事项

 

  • 使用like关键字时,前置%会导致索引失效。

  • 使用null值会被自动从索引中排除,索引一般不会建立在有空值的列上。

  • 使用or关键字时,or左右字段如果存在一个没有索引,有索引字段也会失效。

  • 使用!=操作符时,将放弃使用索引。因为范围不确定,使用索引效率不高,会被引擎自动改为全表扫描。

  • 不要在索引字段进行运算。

  • 在使用复合索引时,最左前缀原则,查询时必须使用索引的第一个字段,否则索引失效;并且应尽量让字段顺序与索引顺序一致。

  • 避免隐式转换,定义的数据类型与传入的数据类型保持一致。

 

参考:

https://help.aliyun.com/document_detail/52274.html?spm=a2c4g.11174283.6.812.ZGPyBQ

 

六、总结

 

1、此次故障虽然是表没有索引造成的,但笔者是有责任的,没有挨个表检查一下表的结构;
2、通过此次故障也可以看出开发在设计表的时候真的要非常重视、注意细节;
3、之前在容器中运行的MySQL也时不时出现CPU瓶颈(比如CPU使用率偶尔会达到80%以上),笔者应该提前发现这些问题,彻底排查,找出问题根源所在,再进行迁库操作。

活动预告