MySQL实践——快速删除大表

背景

删除表,对于MySQL来说是一件习以为常的事情。在业务下线后,或者业务不再需要某张表时,数据库管理人员就需要清理这些废弃的数据库表等。遇到这种情况,要直接删除表吗?如果表过大,那么直接删除对MySQL实例是否存在影响?带着这些问题,将会从源码层面剖析删除可能带来的问题,以及如何快速删除表。

问题分析

一天早上接到一个需求,业务下线了,数据库中的某张表不需要了。为了减少磁盘大小的消耗,提出了将表删除的需求,面对这样的需求,DBA肯定是支持的。于是,登录到MySQL实例上执行了DROPTABLE的操作,消耗了大约35秒。这时候,发现MySQL夯住了。通过监控发现,数据库的QPS瞬间下来很多。发现了问题,就需要来排查问题。首先,确定了一下表的大概大小,发现表的数据量达到了200G。难道是因为表数据量太大导致的?从源码看起吧,看一下DROP TABLE操作、

MySQL底层干了些什么?

MySQL在删除表的时候,主要分为以下两个过程。
  • 1.Buffer Pool页面清除过程。

在删除表的时候,Innodb会将文件在Buffer Pool中对应的页面清除。对于删除表的页面清除,只需要将页面从flush队列中删除即可,而不需要去做flush操作,这样可以减小对系统的冲击。

  • 2.删除ibd磁盘文件的过程。

具体到对Buffer Pool的影响,删除线程首先会根据要删除表的space id,从Buffer Pool中每一个Buffer Pool实例的flush list中找到属于被删除表的页面。在每个实例中搜索页面时,会持有各自Buffer Pool实例的锁,然后遍历搜索这个Buffer Pool实例,如果找到了对应的页面,就会将这个页面从flush list中删除,并且将其oldest_modification设置为0,用来表示这个页面已经失效。不过,这个操作只是将其从flush list中删除了,它还会在Buffer Pool的空闲池中存在,以便重新使用。

这里的问题就是,如果Buffer Pool很大,或者是在Buffer Pool中有很多需要被flush的页面,那么此时遍历扫描页面时就会占用比较长的时间,导致其他事务在用到相应Buffer Pool实例时被阻塞,从而影响整个数据库的性能。

下面精简后的代码讲述的是每一个Buffer Pool实例在删除页面时的方法。

buf_flush_dirty_pages(
    buf_pool_t* buf_pool,     /*!< buffer pool instance */
    ulint id,                 /*!< in: space id */
    Flushobserver* observer,  /*!< in: flush observer */
    bool flush,               /*!< in:flush to disk if true otherwiseremove the pages without flushing */
    const trx_t*  trx )       /*!< to check if the operation mustbe interrupted */
{   dberr_t err;
    do {
        /*获取Buffer Pool mutex */
        buf_pool_mutex_enter(buf_pool);
        /*从当前指定的Buffer Pool实例buf_pool中,删除指定表空间的所有页面*/
        err = buf_flush_or_remove_pages(buf_pool, id, observer,flush, trx);
        /*释放buffer pool mutex */
        buf_pool_mutex_exit(buf_pool);
        /* other code */
    }
}

从代码中看出,针对每一个在Buffer Pool实例中的具体操作,调用了函数buf_flush_or_remove-pages,具体实现如下。

uf_flush_or_remove_pages(
buf_pool_t* buf_pool,     /*!< buffer pool instance */
ulint id,                 /*!< in: target space id for which to remove or flush pages */
Flushobserver* observer,  /*!< in: flush observer */
bool flush,               /*!< in: flush to disk if true but don't remove else remove without flushing to disk */
const trx_t* trx)         /*!< to check if the operation mustbe interrupted, can be 0 */
{
    buf_page_t* prev;
    buf_page_t* bpage;
    ulint processed = 0;
    /*获取buffer flush list mutex */
    buf_flush_list_mutex_enter(buf_pool);

    rescan:
        bool all_freed = true;
        for(bpage = UT_LIST_GET_LAST(buf_pool->flush_list)) { //循环遍历

            /* other code */
            /*这里通过对bpage->id.space()的判断,来确定是不是要清除的页面。如果不是,就直接跳过,继续处理下一个;如果是,就通过函数buf_flush_or_remove_page来处理*/
            if((observer != NULL && observer != bpage->flush_observer)
                || (observer == NULL && id != bpage->id.space())) {

                /* Skip this block, as it does not belong to the target space.*/
            } else if(!buf_flush_or_remove_page(buf_pool, bpage, flush)) {
                all_freed = false;
            } else if (flush) {
                goto rescan;
            }
            ++processed; /* 计数 */
            /* 比较processed与BUF_LRU_DROP_SEARCH_SIZE大小并作出相应处理 */
            if (buf_flush_try_yield(buf_pool, prev, processed)) {

                /* Reset the batch size counter if we had to yield.*/
                processed = 0;
            }
            /* other code */
        }
    buf_flush_list_mutex_exit(buf_pool);   /*释放buffer flush list mutex */
    return(all_freed ? DB_SUCCESS:DB_FAIL);
}

通过上面的源码可以看出MySQL drop table的过程如下。

  • 通过buf_pool_mutex_enter(buf_pool)函数持有buffer pool mutex。
  • 通过buf_flush_list_mutex_enter(buf_pool)函数持有buffer pool中的flush list mutex。
  • 开始扫描flush list列表。
    • 如果脏页属于DROPTABLE,则直接从flush list列表中移除。
    • 如果占用CPU和mutex时间过长,则调用buf_flush_try_yield函数释放CPU资源、flush list mutex和buffer pool mutex,并调用os_thread_yield()函数强制进行contextswitch。
    • 重新持有buffer pool mutex。-重新持有flush list mutex。
  • 释放flush list mutex。
  • 释放buffer pool mutex。

在大表删除时,遍历Buffer Pool中的每一个实例,从flush list中移除页面是一个比较耗时的操作。
除此之外,在删除ibd文件时,如果文件过大也会带来大量的I/O,并且耗时。对于这样的问题,该如何解决呢?可以通过下面的方案来操作。

我们已经知道删除表分为两个过程,第一个过程涉及源码部分,优化或者解决相对困难,而
对于第二个过程(删除ibd文件)来说,其实可以在删除ibd文件之前,对ibd文件做一个硬链接来加速删除,减少对数据库造成的影响,具体操作如下。

sudo ln /data/mysql/testdb/example_table.ibd /data/mysql/testdb/example_table.ibd.hdlk
sudo ls -1h /data/mysql/testdb
-rw-rw---- 1 mysql mysql 8.4K Oct 2813:26 example_table.frm
-rw-rw---- 2 mysql mysql 100G Oct 2813:26 example_table.ibd
-rw-rw---- 2 mysql mysql 100G Oct 2813:26 example_table.ibd.hdlk

上面的命令执行完之后,发现多了一个example_table.ibd.hdlk文件,且example_table.ibd.hdlk和example_table.ibd的inode(关于操作系统文件引用数目的知识请参考Linux相关书籍)数均为2。因为我们知道,一个磁盘上的文件,可以由多个文件系统的文件引用,这多个文件是完全相同的,都指向同一个磁盘上的文件,当我们删除任何一个文件的时候,都不会影响真实的文件,只是会将其被引用数目减1,只有当被引用数目变为1的时候,再次删除文件,才会真正被删除。

删除时,这两种情况的区别很明显,一个是在减少被引用数目,一个是真正做IO来删除它。我们正是利用了这个特点,将由MySQL来删除大文件的操作转换为一个简单的操作系统级的文件删除,从而减少了对MySQL的影响。当然,在成功drop table之后,剩下的这个真正的文件,可以想办法慢慢处理,此时已经与MySQL没有关系了。

如果还是担心删除大文件对操作系统有影响,从而进一步影响到了MySQL,还有其他方法可以用来处理,比如可以循环分块删除,慢慢地清理文件,通过一个脚本即可搞定,方法是操作系统级的文件truncate,这个可以自行了解。

发散思维

在普通的MySQL实例上,可以通过文件的硬链接方式来解决。但是Galera Cluster在删除表的时候,进行的验证、复制等操作都会导致集群节点出现问题,相当于一个大事务(关于DDL和大事务在Galera Cluster上执行的问题,请参考第29章),这时就不能直接删除了。

该如何解决呢?众所周知,在Galera Cluster中有一个参数可以控制本地操作是否要复制到其他节点,那就是wsrep_on参数。该参数是会话级别的,默认值为ON,如果设置为OFF,则当前会话的操作不会复制到其他节点,并会像独立的MySQL服务器一样运行。具体操作如下。

#集群中的一个节点
mysql> set @@session.wsrep_on = OFF;
Query OK,0 rows affected(0.00 sec)
#这时可以通过上面描述的删除大表的方法,使用硬链接的方式来删除。建立硬链接的操作#此处不做赘述。
mysql> drop table t2;
Query OK,0 rows affected (0.01 sec)

可以在集群中的每一个节点开启一个新会话,设置参数wsrep_on为OFF,然后分别通过上面描述的删除大表的方法来删除集群中的大表,线上数据库几乎无影响。

总结

在删除大表的时候,一定要注意对线上数据库的影响。对于大表,可以通过建立硬链接,或者先清理数据,最后再删除的方式,达到预期效果。总之,线上数据库最重要,要让所有操作尽可能小地影响数据库。

你可能感兴趣的:(数据库,删除大表,数据库删除表,快速删除大表)