如何在不挂起 MySQL 进程的情况下使用 innodb_file_per_table=1 删除 1000 多个数据库?

How to drop a 1000+ databases with innodb_file_per_table=1 without hanging the MySQL process?

我们有一个重复的过程,我们希望并且需要清理我们的数据库。每个客户或潜在客户都有自己的数据库(有 300 table 秒,并且每个月都在增加),它会在几秒钟内启动,并植入一些基本数据。

几个月后,需要清理数据库。我们只需为每个数据库调用 DROP DATABASE customer_1(在每个语句与 'rest' 之间给 MySQL 服务器 10 秒),然后是 DROP USER 'customer_1'@'127.0.0.1').

每隔一段时间,整个数据库就会挂起。 SHOW PROCESSLIST 给出

Id     User       Command    Time    State         Info
[pid]  adm-user   Query      300     System lock   DROP DATABASE `customer_1`

不会完成任何新查询。杀死相关查询 pid 将导致 Command=Killing,仅此而已。什么都没发生。 MySQL 守护程序也无法停止,因为它仍在等待完成查询。

我们关闭了整个服务器,重新启动它,并让 MySQL 执行自动崩溃恢复,效果很好。之后,我们可以删除另外 10-30 个数据库,然后这个事件会重复。

我们已经阅读了大量有关该主题的资料,包括但不限于:

似乎共识是,是的,MySQL 在 table(space) 上使用了全局互斥锁,并结合了较大的缓冲池大小。

我们的my.cnf:

innodb_file_per_table   = 1
innodb_buffer_pool_size = 9G
innodb_log_file_size    = 256M
innodb_flush_method     = O_DIRECT
table_open_cache        = 200000
table_definition_cache  = 110000
innodb_flush_log_at_trx_commit = 2

有什么方法可以让我们负责任地删除数据库——即,不让服务器因其他潜在客户而停机?

我读过,只需删除所有 table 文件即可,然后删除数据库,其中 MySQL 应该 只需删除对数据库。

您应该告诉数据库引擎不要建立独占(全局)锁。您可以通过两种方式执行此操作:

  1. 使用 LOCK clause(NONE 或共享)。
  2. 使用ALTER ONLINE TABLE(与LOCK=NONE相同)。

ALTER TABLE tbl_name ADD PRIMARY KEY (column), ALGORITHM=INPLACE, LOCK=NONE;

更改所有表后,或者如果您使用共享锁创建,您应该能够删除所有表。

您应该做的一件重要事情是为您的 MySQL 数据目录使用 XFS 文件系统。

将大文件拖放到 ext3 文件系统上会花费太多时间,正如您在所链接的 Percona 博客中所读到的那样。使用 XFS 可以更快地删除大文件,因此全局互斥锁的保留时间更短。

我还会一次删除一个 tables,以进一步减少互斥量的持有时间。然后在删除所有 table 之后,删除数据库。

MySQL 中的数据库根本不是物理对象。它是 MySQL datadir 的一个子目录,还有一个名为 db.opt 的小文件,它存储了数据库的一些属性,比如它的默认字符集(这甚至不再是 MySQL 中的一个单独文件) 8.0).在删除所有 table 之后,删除数据库本身就很简单了。

另一个建议是先把客户的MySQL用户去掉,然后让MySQL运行几个小时,直到那个客户的table的数据都没有缓存在缓冲池中的时间更长。当您删除一个大 table 时,MySQL 必须扫描缓冲池以释放属于该 table 的页面。缓冲池越大,所需的时间越长。因此,如果让该客户的 table 的页面过期并离开缓冲池,则可以最大限度地减少这种影响。这可能需要一些时间,因为它更多地是由对其他 table 的需求驱动的。除了删除 table.

之外,没有什么好的方法可以强制 table 的页面离开缓冲池

我在某些环境中这样做过。将 "DROP TABLE" 请求放入 RENAME TABLE 以将 table 移动到另一个用户无权访问的架构中。然后定期 运行 一个脚本来真正删除已经在该控制笔中超过 7 天的 tables。当来自其他 table 的数据取代它们时,这为页面逐渐从缓冲池中逐出提供了时间。此外,如果用户决定放弃他们需要的 table,它还为用户提供了改变主意的宽限期。

基于此评论链:

Is a script-based solution viable to you? E.g. Create a php script and run that? – treyBake Feb 14 at 17:33

@treyBake Yep, definitely. We've got full control over everything. – Mave 5 mins ag

您可以通过 PHP 执行此操作(例如将其命名为 rm_databases.php

$tableName = 'customer_';

for ($i = 1; $i <= 300; $++)
{
    # set up db conn
    $conn = new \PDO(
        'mysql:dbname='. $tableName .$i. ';host=localhost;',
        'user',
        'pass'
     );

     # create the SQL statement
     $sql = 'DROP DATABASE IF EXISTS '. $tableName .$i. ';';

     # exec it
     $conn->prepare($sql);
     $conn->execute();
}

echo 'done!';

然后你可以选择 运行ning 手动 运行ning:

php -f rm_databases.php

或者您可以通过 cronjob 设置为每 3 个月 运行:

0 0 12 ? 1/3 MON#1 * php -f rm_databases.php

这将 运行 每 3 个月的第一个星期一。

旁注: 如果 每个 客户数据库的前缀为 customer_$i - 但如果它更动态不仅如此,尽管它可能很累人,但创建一个数据库名称的巨型数组并循环遍历它可能是值得的。初始设置时间会更长,但完成后,添加新用户需要 2 秒:

$databases = [
    'foo', 'bar3', 'foobar', 'treyisawesome', 'wp-firesf'
    # etc etc
];

foreach ($databases as $el)
{
   # set up db conn
   $conn = new \PDO(
       'mysql:dbname='. $el .';host=localhost;',
       'user',
       'pass'
    );

    # create the SQL statement
    $sql = 'DROP DATABASE IF EXISTS '. $el .';';

    # rest of the script stays the same
}

Bill Karwin 的建议似乎是合理的(尽管 RENAME TABLE 过去曾引发过一些与 DROP TABLE 相同的问题),但大部分内容应该已修复:Bug 51325 was fixed in 2011-12-20 in 5.6.4 and Bug 64284 was fixed in 2012-08-09 in 5.6.6.

您在删除 tables/databases 时可能遇到与 MySQL bug 91977, for which one suggested workaround is to disable the Adaptive Hash Index 有关的问题。

SET GLOBAL innodb_adaptive_hash_index = OFF;
DROP TABLE ...
SET GLOBAL innodb_adaptive_hash_index = ON;

或者完全放弃自适应哈希索引。请参阅上面链接的文档,其中指出它们是否是净收益取决于工作负载,您应该进行性能测试来决定是否使用它们。

您可能想要升级到当前的 MySQL 5.7.x,即 5.7.25,如果您仍然可以重现问题,请提交错误报告。