MySQL数据安全系列之一:现实案例

原由
最近,公司某个游戏部门一台用于分析游戏数据的MySQL服务器由于文件系统损坏导致宕机,经过SA修复后,用于保存数据的MySQL服务再也无法启动,即使使用了innodb_force_recovery=6的配置忽略redo恢复,结果还是一样,查看error log会得到如下提示:
150123 17:32:44 [ERROR] Plugin 'InnoDB' init function returned error.
150123 17:32:44 [ERROR] Plugin 'InnoDB' registration as a STORAGE ENGINE failed.
150123 17:32:45 [ERROR] Unknown/unsupported storage engine: InnoDB
150123 17:32:45 [ERROR] Aborting
初步分析由于文件系统的损坏,造成MySQL数据库的全局表空间被破坏了,从而导致MySQL服务无法启动。这是一台单点服务,没有master-slave机制,更为糟糕的是,由于数据量巨大(约8T,近4万张表),该台服务上的数据没有任何备份,这样的后果无疑是灾难性的,一旦遇上,多少让人有点手足无措,那么这样的情况下,数据能不能找回?

前提
最终数据能不能找回,这个不是绝对的,需要根据当时的实际情况来定,需要满足几个条件:
  1. 数据库设置了innodb_file_per_table=1,这样每个表的数据都会有个单独的表空间,不会放在全局表空间中
  2. 单个表的表空间没有被损坏,如果损坏,那该表的数据将无法恢复
  3. 保存表结构的.frm文件没有损坏
幸运的是,该部门的这台MySQL服务满足上述几个条件,这为我们提供了恢复相应数据的可能,后面通过对一个表的恢复操作,简单介绍下整个恢复流程,多个表可以采用脚本形式对此执行即可完成所有数据恢复。

恢复过程
对于上述这样的数据库服务,数据要想原地恢复的可能性几乎没有,我们只能通过单个表恢复——导出——导入的方式恢复到另外一个数据库实例中,具体操作如下:
  • 找到要恢复的那张表的数据,记录要恢复的表的名称,如果名称中带有@符号,如:account@6fc0@6d3b,说明该表名在创建的时候包含了特殊字符(非字母、数字、-等),真实的表名需要进行反转义后获取,6fc0、6d3b分别是‘激活’两字的utf8编码,那么真是的表名应该是‘account激活’
  • 在新的实例中以前面获取的表名创建一张同名表,字段任意,表的类型为InnoDB,然后从老的.frm文件覆盖新实例中的同名文件,这样在新实例中,通过show create table table_name的方式可以获取到真实的表结构,不要在意任何warning或者错误提示,我们只需要获取表结构
  • 正确的表结构已经获取了,接下去就是创建真是的表,先drop掉前面建的表,用新的创建语句创建新表,如果实现知道要恢复的表的表结构,上面几步可以省略
  • 通过mysqladmin的shutdown命令正常关闭数据库,主要目的是让内存中的数据都刷到盘上
  • 在新实例中,用hexdump查看新创建表的ibd文件,通过找到头上连续出现多行0的位置,提取上面的文件头
  • 用新的文件头覆盖需要恢复的ibd文件头后,用来的ibd替换新的ibd文件
通过上述的操作,我们通过innodb_force_recovery=6启动数据库后,就可以读取原来表中的数据了,通过mysqldump把数据导出到sql文件

恢复实践
用sysbench生成一张有1000条数据的表,记录下第1,500,1000行的部分数据方便验证,然后人为删除共享表空间
删除表空间后再启动MySQL服务,提示如下:

上述提示说明当前库已经损坏,需要对数据进行恢复
新建同名表,然后通过覆盖获取建表语句创建新表
覆盖前表结构
show create table sbtest1结果

覆盖后表结构
show create table结果
从上面结果可以看到,在替换了.frm文件后,需要恢复的表空间已经可以被读取了,但是这个时候的数据库是空的,也就是说,现在数据是没有恢复的,接下去就是要进行数据恢复
数据恢复前需要对MySQL执行正常关闭操作:
mysqladmin -uroot --socket=/tmp/mysql.sock shutdown
正常关闭的主要目的是把内存中的未落盘数据刷到盘上,主要是ibd文件,因为下面我们需要读取新的ibd文件的头部:
         
             新的ibd文件头部                                                                                  老的ibd文件头部
用上述新的ibd文件头部覆盖老的ibd文件头部,然后把覆盖后的ibd文件拷贝到新的空库下面,用innodb_force_recovery=6启动数据库后进行查询:
select id,k,c from sbtest1 where id=1 or id=500 or id=1000;
通过上述的查询,我们发现恢复后的数据与开始创建的数据完全一致,数据恢复成功,通过mysqldump导出数据进行重建即完成数据的恢复。
------------------------------------------------------------------------------------------------------------------------------------
上述这个数据库,如果损坏的不止是全局表空间,同时其他表空间也被破坏了,或者干脆没有设置innodb_file_per_table=1,那么,数据的永久丢失是不可避免的,几年的游戏数据损失,其后果真的不堪设想。那么我们应该怎么做才能避免类似上文中出现的问题?
    其实对于数据库来说,避免数据大面积丢失的方法还是有很多的,其中最常用的就是对重要数据进行定期备份,而对于MySQL来说,官方的和非官方的备份工具有很多,接下去就让我们来一一认识一下。


本文来自网易实践者社区,经作者蒋鸿翔授权发布。