DBA和运维同学的大救星又来了!

这次谈谈这个大救星的具体实现细节。

我前段时间发的“DBA和运维同学的大救星来了”的文章,引起了业界同行的广泛关注,很多DBA和运维同学表示,能够找回删除的库表(下文简称 “库表回收站” 功能)对他们会非常有帮助,希望能够合并到官方mysql代码中。目前来说,库表回收站是TDSQL特有的功能,未来的话,什么好事都有可能发生的。

今天我主要讲一下,我是怎么设计和实现这个库表回收站功能的,另外还会讲一下与之相关的表文件慢速删除功能。

在此之前先补充介绍一下自上次撰文后,我对这块功能做了一些调整,主要是当你drop table t1;或者drop database db1后再立刻create table t1或者create database db1的话,原来是会失败报错的,现在修改为可以成功。并且这样的drop->create可以不做等待无数次重复。示例如下:

MySQL的表定义简介

首先简述一下mysql的元数据管理。MySQL的表定义是存储在一个.frm文件中的,每个表有一个同名(这里暂且忽略lower_case_table_names的细节)的.frm文件,该文件位于数据目录的database子目录下面。比如test.t1这个表,它的frm文件和ibd文件位于该db实例的数据目录下的test/t1.frm和test/t1.ibd。 一个表的frm文件存储这个表的元数据,包括库表名称,每个列的信息(名称,数据类型,长度,约束等),索引信息,使用的存储引擎,trigger,表分区定义等。每执行一个SQL语句,都需要在本连接中打开这个语句使用的每个表,语句执行结束后关闭这些表。打开一个表的时候,mysql会读取frm文件的元数据信息,装入一个TABLE对象,这些信息都是查询执行过程中会用到的。另外,视图的定义也是存储在frm文件中的,不过其内容和格式与表的frm完全不同,本功能不涉及视图,因此不再赘述。

隐藏和找回表的实现

表的Frm文件头部数据结构中,有一些未使用的空白字节(值为),这些字节不存储任何信息,并且其中很多字节是mysql没有为未来预留的,我的实现就利用了其中的第46个字节(下文简称为hide_byte,意为表的隐藏字节),该字节为代表这个表可见,为1代表它被隐藏了。另外说一句,由于下一个mysql大版本(8.0)已经不再使用frm文件,所以也更不需要担心会与未来的mysql版本冲突。

当mysql执行一个drop table语句时候,在获取mysql server层的表级IX lock并且权限检查通过后,就会删除其frm文件并且通过handler接口调用存储引擎的表删除功能。在TDSQL加入库表回收站功能后,执行drop table t1;语句时,不会删除frm文件也不会做存储引擎的表删除功能,而是只把其hide_byte从修改为1。当打开一个表的时候,总是检查hide_byte,如果是则正常打开,如果是1则open_table_def()函数返回特殊的错误,由上层调用代码处理。这样隐藏表的好处是mysql server层完全无法看到这个表的存在,但是表的定义文件(.frm)和数据文件(.ibd)都完好无损。这个表对于innodb来说仍然存在,所以会对它做正常的维护,包括redo/undo日志的处理,purge,刷脏页,change buffering合并和启动时刻的数据恢复。另外,表文件也不需要为了隐藏而做任何重命名等操作。

当需要找回一个隐藏的表时候,只需要把这个hide_byte重新改为,即可正常打开了,这个表也就找回了。当show tables时只返回hide_byte为的表;当show hidden tables时只返回hide_byte为1的表。当drop table immediate时候就不再设置hide_byte而是按照原来的做法删除表。

如果在drop table t1后立刻执行create table t1,那么会对已经存在的隐藏的t1表做rename:当再次create table t1的时候,发现已经有重名的隐藏的表t1,所以就rename这个t1表(而不是rename t1.frm文件),给表名后缀一小段tdsql特定的字符串(_tdsqlhid_6个数字)。这6个数字是操作执行当时的time()秒数后3位和微秒数的后3位。当找回一个表时,如果这个表已经被重命名为后缀了tdsqlhid_6个数字的话,这个表名并不会修改,用户需要手动rename为他需要的名字。如果这个后缀导致表名超长的话,那么第二次create table就会失败。这段话如果不够形象的话,请结合本文开头的示例来理解。

读者知道为什么不能简单地重命名frm文件和/或对应的ibd文件吗? 欢迎在评论中留言回答。

隐藏和找回数据库

Mysql中一个数据库对应数据目录下面的一个同名目录(暂不考虑lower_case_table_names的细节)。由于db没有类似表的frm文件,所以隐藏db的方法,就是重命名这个db,在其名称末尾增加‘_tdsqlhid_6个数字’。也就是用’tdsqlhid’来标识一个db为隐藏的db。这6个数字也是操作执行当时的time()秒数后3位和微秒数的后3位。当create database时候,禁止名称中出现包含tdsqlhid字符串;当要打开一个表的时候,禁止库名中包含tdsqlhid字符串,也就是不许访问隐藏的库中的表;当show databases;时候,不显示包含’tdsqlhid’字符串的db;当show databases hidden;的时候,只显示名称中包含tdsqlhid的db。当要找回一个隐藏的db时候,需要重命名这个db,去掉其中的tdsqlhid字符串,并且用户可以在形如expose database test_tdsqlhid_123456 to test1的语句中为找回的库test_tdsqlhid_123456指定新名称test1。见本文开头的示例。

重命名一个db,主要涉及的重命名其中的表,把每个表的db名称改为新db名称;以及重命名db目录。读者知道为什么不能简单地重命名db目录名文件吗? 欢迎在评论中留言回答。

库表隐藏与复制

与隐藏,找回,删除库表相关的所有sql语句都会记录binlog并且备机能够正确地复制执行。库表的隐藏功能,是由2个session变量drop_hide_table和drop_hide_db来动态打开或者关闭的,默认是打开的。如果关闭的话则删除库表将按照原来的方式进行,不需要加immediate就可以立刻删除库表。这就要求复制的时候对每一个drop table/drop database语句的binlog事件,记录该语句执行当时的drop_hide_table和drop_hide_db的值,并且在复制的时候,根据该binlog事件中记录的值来设置slave的对应的drop_hide_table和drop_hide_db session变量,从而在slave这边也做相同的事情(隐藏或者直接删除)。记录的方法是使用binlog事件头部的一个空闲的标志位。

库表隐藏功能要求备机必须也是tdsql的percona/mariadb数据库。如果主机是tdsql的percona/mariadb数据库但是备机不是的话,那么主机上面隐藏的库表会在备机那边直接删除。然后,主执行的expose table/database语句在备那边根本无法执行。

库表隐藏与数据库恢复

每次隐藏一个库表时,这个库表名称会记录到后台删除线程所使用的一个全局对象中,然后后台线程定期扫描待删除库表,达到计时时间的就做真正的删除。当mysqld每次重启时,会扫描数据目录,找到所有已经隐藏的库表,让后台线程重新开始计时删除。

读到这里,不知道读者有没有想到一个问题:mysqld重启之后到了keep_hidden_hrs(tdsql新增的变量)个小时之后,后台删除线程要在同一个时间点删除所有启动期间扫描到的隐藏的库表。这会对机器的文件系统带来一个突增的负载,而且可能持续较长时间,可能导致机器上面所有db实例在此期间性能严重下降。 这在tdsql上面是不会发生的,为什么呢? 请看下文的“表文件慢速删除” 这部分。

与数据库恢复相关的另一个事情就是库表重命名,这与我在上文中给读者提出的两个问题有关,这里先卖个关子,欢迎大家在评论中说出自己的理解。

表数据文件慢速删除

当一个表真正要做删除时候,如果其数据量很大,比如几十GB,那么删除这么一个文件会对io设备带来突增的负载,在负载突增期间这个机器上面所有的db实例的qps会有一个深深的波谷,响应时间会有一个高耸的波峰,这对于很多业务来说是无法忍受的。所以,tdsql的percona和mariadb数据库内核中,我实现了一个慢速删除的机制,来避免这种波峰和波谷。如何做慢速删除呢? 首先要讲一下文件系统的工作原理。与本文相关的文件系统工作原理,有以下两部分:

1.空闲空间管理

主要是把空闲的不相邻的磁盘块串联为一个链表。当需要分片若干个磁盘块时候,就从空闲链表中去找;当有文件被删除或者截断而释放了磁盘块时候,需要把这个文件的磁盘块合并到空闲链表中。注意这个合并过程,不是简单地把文件的所有块串联到空闲链表末尾,而是要合并到空闲链表中,比如空闲链表是(1,2)->(4,5)->(9,10),现在要合并磁盘块3,那么合并后的磁盘块就是(1,5)->(9,10)。只有做合并才能有效地做大块空间的分配,在ssd之前的时代,连续存放的文件,读写都要快的多。合并磁盘块是truncate或者删除文件的操作中负载最高的部分。我这个慢速删除就是把这部分操作,拉长了。

2.文件空间管理

每个文件的磁盘块形成一个链表,这些磁盘块通常并不相邻。随着文件的增长,文件系统为该文件分配更多的磁盘块。当文件被truncate时候,就把磁盘块归还给文件系统。

所以,慢速删除一个文件,其实就是把归还和合并一个文件中所有的磁盘块这个高负载的操作,分批来做。通过多次调用truncate()系统调用,分多步骤每次把一个数据文件缩小16MB,直到最后它小于16MB再一次性删除,两次truncate之间等待一段时间(由file_slow_delete_rate变量控制,该变量是慢速删除的速率,单位是MB/s)。例如,本来要一次性归还并合并的一千万个磁盘块,分为1000次,每次1万个,这样文件系统的负载就不会突增了。

当执行drop table t1的时候,在innodb内部,我会把t1.ibd文件重命名为t.ibd.delayed_drop,然后放到慢速删除线程的任务队列中。慢速删除线程会按照上述做法完成该文件的缩小和最终删除。当mysqld启动时候,会扫描其数据目录中所有的*.delayed_drop文件,放入其工作队列中,把它们慢慢处理掉。

目前慢速删除只对innodb的表文件有效,不处理rocksdb和myisam等。Myisam想处理并不难,不过tdsql本来就禁止用户使用myisam表的,所以不做支持。而rocksdb由于其特殊的存储方式,数据并没有以表为单位分别存放到不同文件中,因而想做到表级别的慢速删除是不现实的。

另外,XFS文件系统已经避免了删除大文件导致的io负载突增,我没有看XFS的技术文档,但是我想它可能有两种办法避免这个问题:

1.XFS可能把合并被删除的磁盘块这个动作,做成了异步操作,在后台进程中完成。现在的文件系统也有事务日志的,完全可以做可靠的恢复;

2.XFS可能从空闲空间管理方面做了改变,不再合并相邻磁盘块。在SSD时代,磁盘块随机读写与顺序读写是同样的性能,所以就不需要合并了,直接把文件的磁盘块列表挂到空闲磁盘块链表末尾(或者头部)即可。

本文到此结束,欢迎大家留言回复讨论。

  • 发表于:
  • 原文链接:https://kuaibao.qq.com/s/20181114G1KF1Z00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券