节流 ALTER TABLE 磁盘利用率
Throttle ALTER TABLE disk utilization
我将从 MySQL Online DDL Limitations 页面的内容开始:
There is no mechanism to pause an online DDL operation or to throttle I/O or CPU usage for an online DDL operation.
但是,我仍然对我可能错过的解决方案感兴趣。
情况:索引越来越大,越来越大以至于没有足够的内存用于使用的查询,导致磁盘I/O暴涨,一切都陷入彻底的混乱。已创建较小的新复合索引,但问题是 运行 ALTER TABLE
没有破坏任何东西。
事实如下:
- 这是一个 InnoDB table。
- table 没有主键或唯一索引。
- 没有适合作为主键或唯一索引的列组合。
- table没有外键。
- table 每月分区(当前为 50)。
- table 必须始终接受写入。
- 最新的 3-6 个分区必须接受读取。
- 有一个
id
列,但这不是唯一的。
- table 包含大约 20 亿行。
- 当月的分区是唯一接收写入的分区。
- 提前1个月分区;总有一个空分区。
SHOW CREATE TABLE
(我没有包括所有分区):
CREATE TABLE `my_wonky_table` (
`id` bigint(20) unsigned NOT NULL,
`login` varchar(127) DEFAULT NULL,
`timestamp` int(10) unsigned NOT NULL,
`ip` varchar(32) CHARACTER SET ascii DEFAULT NULL,
`val_1` int(10) unsigned DEFAULT NULL,
`val_2` varchar(127) DEFAULT NULL,
`val_3` varchar(255) DEFAULT NULL,
`val_4` varchar(127) DEFAULT NULL,
`val_5` int(10) unsigned DEFAULT NULL,
KEY `my_wonky_table_id_idx` (`id`),
KEY `my_wonky_table_timestamp_idx` (`timestamp`),
KEY `my_wonky_table_val_1_idx` (`val_1`,`id`),
KEY `my_wonky_table_val_2_idx` (`val_2`,`id`),
KEY `my_wonky_table_val_4_idx` (`val_4`,`id`),
KEY `my_wonky_table_val_5_idx` (`val_5`,`id`),
KEY `my_wonky_table_ip_idx` (`ip`,`id`),
KEY `my_wonky_table_login_idx` (`login`,`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
/*!50100 PARTITION BY RANGE (`id`)
(PARTITION pdefault VALUES LESS THAN MAXVALUE ENGINE = InnoDB) */
关于查询:它始终是 id
上的 SELECT
,其他所有内容都用于过滤。
我想避免的事情:
- 正在关闭数据库实例。
- 磁盘 I/O 的 100%
想过用pt-online-schema-change
工具来节流,但是运行进了无主键墙。一个不同的解决方案是在代码中执行此操作,有效地将触发器移动到代码库,并使用有点奇怪的块(例如,使用时间戳列的一个小时的数据块)慢慢复制数据,因为没有唯一索引。
是否有其他可用的解决方案 and/or 工具?
- 创建一个类似于
real
table 的 new
table,但具有修改后的索引。包括一个 PRIMARY KEY
这样你就不会再被困住了。 -- 这是 ALTER
,但还不是 "populate"。
- 在新的table中,对旧内容使用季度或年度分区;当前和(以后)未来的分区每月一次。 -- 这是为了减少分区总数。我的经验法则是 "no more than 50 partitions"。 (如果您对此计划有疑问,请告诉我。)
- 编写一个脚本,慢慢地将所有数据从旧分区复制到
new
table。我对 chunking 的建议在这里可能会有用。
- 就在你被赶上之前,创建一个新分区。但是不要从中复制。在前一个分区的末尾停止 "copy" 脚本。
- 当赶上除了这个新分区,停止写入。
- 复制最后一个分区。 -- 这是第 4 步得到回报的地方。
- 原子交换:
RENAME TABLE real TO old, new TO real;
。然后再次打开写入。
强烈建议编写所有脚本并在另一台机器上练习。实践可以在总数的一小部分上,但至少需要有几个分区。
我将其作为单独的答案提出,因为最里面的部分完全不同。
与我的其他答案一样,您需要具有新索引的 new
table,以及用于复制所有数据的脚本。然而,主要是在你的应用.
中模拟触发器
幸运的是,你有 id
,尽管它不是 PRIMARY KEY
。而且,即使它不是 UNIQUE
,也可以使用(假设您没有数千行具有相同的 id——如果有,我们可以进一步讨论)。
"copy script" 和应用程序相互对话。
复制脚本在一个长循环中:
SELECT GET_LOCK('copy', 5), high_water_mark FROM tbl;
--(或其他超时)
- 复制带有
id BETWEEN high_water_mark AND high_water_mark + 999
的行。
UPDATE tbl SET high_water_mark = high_water_mark + 1000;
- 暂停一下(1 秒?)
- 循环直到停止
ids
应用程序在读取时继续从旧 table 读取。但是写的时候,确实是:
SELECT GET_LOCK('copy', 5), high_water_mark FROM tbl;
--(或其他超时)
- 如果超时,则需要修复。
- 写入旧 table --(因此,读取继续工作)
- 如果
id
<= high_water_mark
,也写入新的 table。
SELECT RELEASE_LOCK('copy');
监控进度。在某些时候,您需要停止所有操作,复制最后几行并执行 RENAME TABLE
.
我不知道您的超时、睡眠或块大小的最佳值。但是我认为块大小大于1K是不明智的。
此技术对于您将来可能需要进行的各种更改具有优势,因此请保持胆量。
这将归结为您使用的 MySQL 变体和版本,但是如果每个连接一个线程(my.cnf thread_handling=one-thread-per-connection
,这可能是您的默认设置build),你可以把你的ALTER TABLE
工作负载放在一个新的连接中,那么工作负载就是一个唯一的PID,你可以在上面使用ionice
/renice
。
我的答案有点蹩脚,但它比其他选项更具侵略性。
如果你查看 ps -eLf |grep mysql
你可以看到 threads/lightweight-processes,只需要弄清楚哪个 PID 属于你的特定连接。如果您通过 TCP 连接,您可以匹配您的本地连接端口并将其映射到 lsof 以查找特定线程。其他方法也可以使用 strace、systemtap 等,或者 运行 您可以查看的初始查询。
之后,您可以使用ionice
/renice
来影响系统上的PID。您真的很想确保捕获它是什么 PID,然后重置 nice 和优先级,以免影响其他任何东西。
与其他人一样,从长远来看,您确实需要重塑这个 table。分区很有用,但不是最终目的,因为您有 运行 1.3TiB 的在线数据,并且您声明您只需要从最近的 3-6 个分区中读取。来自添加本机分区之前的 MySQL,我认为这对于 VIEW 和单独的 table 来说是一个很好的例子(当您需要翻转时自动更新 VIEW)。它还可以让您轻松地将一些较旧的 table 移动到离线存储。
我将从 MySQL Online DDL Limitations 页面的内容开始:
There is no mechanism to pause an online DDL operation or to throttle I/O or CPU usage for an online DDL operation.
但是,我仍然对我可能错过的解决方案感兴趣。
情况:索引越来越大,越来越大以至于没有足够的内存用于使用的查询,导致磁盘I/O暴涨,一切都陷入彻底的混乱。已创建较小的新复合索引,但问题是 运行 ALTER TABLE
没有破坏任何东西。
事实如下:
- 这是一个 InnoDB table。
- table 没有主键或唯一索引。
- 没有适合作为主键或唯一索引的列组合。
- table没有外键。
- table 每月分区(当前为 50)。
- table 必须始终接受写入。
- 最新的 3-6 个分区必须接受读取。
- 有一个
id
列,但这不是唯一的。 - table 包含大约 20 亿行。
- 当月的分区是唯一接收写入的分区。
- 提前1个月分区;总有一个空分区。
SHOW CREATE TABLE
(我没有包括所有分区):
CREATE TABLE `my_wonky_table` (
`id` bigint(20) unsigned NOT NULL,
`login` varchar(127) DEFAULT NULL,
`timestamp` int(10) unsigned NOT NULL,
`ip` varchar(32) CHARACTER SET ascii DEFAULT NULL,
`val_1` int(10) unsigned DEFAULT NULL,
`val_2` varchar(127) DEFAULT NULL,
`val_3` varchar(255) DEFAULT NULL,
`val_4` varchar(127) DEFAULT NULL,
`val_5` int(10) unsigned DEFAULT NULL,
KEY `my_wonky_table_id_idx` (`id`),
KEY `my_wonky_table_timestamp_idx` (`timestamp`),
KEY `my_wonky_table_val_1_idx` (`val_1`,`id`),
KEY `my_wonky_table_val_2_idx` (`val_2`,`id`),
KEY `my_wonky_table_val_4_idx` (`val_4`,`id`),
KEY `my_wonky_table_val_5_idx` (`val_5`,`id`),
KEY `my_wonky_table_ip_idx` (`ip`,`id`),
KEY `my_wonky_table_login_idx` (`login`,`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
/*!50100 PARTITION BY RANGE (`id`)
(PARTITION pdefault VALUES LESS THAN MAXVALUE ENGINE = InnoDB) */
关于查询:它始终是 id
上的 SELECT
,其他所有内容都用于过滤。
我想避免的事情:
- 正在关闭数据库实例。
- 磁盘 I/O 的 100%
想过用pt-online-schema-change
工具来节流,但是运行进了无主键墙。一个不同的解决方案是在代码中执行此操作,有效地将触发器移动到代码库,并使用有点奇怪的块(例如,使用时间戳列的一个小时的数据块)慢慢复制数据,因为没有唯一索引。
是否有其他可用的解决方案 and/or 工具?
- 创建一个类似于
real
table 的new
table,但具有修改后的索引。包括一个PRIMARY KEY
这样你就不会再被困住了。 -- 这是ALTER
,但还不是 "populate"。 - 在新的table中,对旧内容使用季度或年度分区;当前和(以后)未来的分区每月一次。 -- 这是为了减少分区总数。我的经验法则是 "no more than 50 partitions"。 (如果您对此计划有疑问,请告诉我。)
- 编写一个脚本,慢慢地将所有数据从旧分区复制到
new
table。我对 chunking 的建议在这里可能会有用。 - 就在你被赶上之前,创建一个新分区。但是不要从中复制。在前一个分区的末尾停止 "copy" 脚本。
- 当赶上除了这个新分区,停止写入。
- 复制最后一个分区。 -- 这是第 4 步得到回报的地方。
- 原子交换:
RENAME TABLE real TO old, new TO real;
。然后再次打开写入。
强烈建议编写所有脚本并在另一台机器上练习。实践可以在总数的一小部分上,但至少需要有几个分区。
我将其作为单独的答案提出,因为最里面的部分完全不同。
与我的其他答案一样,您需要具有新索引的 new
table,以及用于复制所有数据的脚本。然而,主要是在你的应用.
幸运的是,你有 id
,尽管它不是 PRIMARY KEY
。而且,即使它不是 UNIQUE
,也可以使用(假设您没有数千行具有相同的 id——如果有,我们可以进一步讨论)。
"copy script" 和应用程序相互对话。
复制脚本在一个长循环中:
SELECT GET_LOCK('copy', 5), high_water_mark FROM tbl;
--(或其他超时)- 复制带有
id BETWEEN high_water_mark AND high_water_mark + 999
的行。 UPDATE tbl SET high_water_mark = high_water_mark + 1000;
- 暂停一下(1 秒?)
- 循环直到停止
ids
应用程序在读取时继续从旧 table 读取。但是写的时候,确实是:
SELECT GET_LOCK('copy', 5), high_water_mark FROM tbl;
--(或其他超时)- 如果超时,则需要修复。
- 写入旧 table --(因此,读取继续工作)
- 如果
id
<=high_water_mark
,也写入新的 table。 SELECT RELEASE_LOCK('copy');
监控进度。在某些时候,您需要停止所有操作,复制最后几行并执行 RENAME TABLE
.
我不知道您的超时、睡眠或块大小的最佳值。但是我认为块大小大于1K是不明智的。
此技术对于您将来可能需要进行的各种更改具有优势,因此请保持胆量。
这将归结为您使用的 MySQL 变体和版本,但是如果每个连接一个线程(my.cnf thread_handling=one-thread-per-connection
,这可能是您的默认设置build),你可以把你的ALTER TABLE
工作负载放在一个新的连接中,那么工作负载就是一个唯一的PID,你可以在上面使用ionice
/renice
。
我的答案有点蹩脚,但它比其他选项更具侵略性。
如果你查看 ps -eLf |grep mysql
你可以看到 threads/lightweight-processes,只需要弄清楚哪个 PID 属于你的特定连接。如果您通过 TCP 连接,您可以匹配您的本地连接端口并将其映射到 lsof 以查找特定线程。其他方法也可以使用 strace、systemtap 等,或者 运行 您可以查看的初始查询。
之后,您可以使用ionice
/renice
来影响系统上的PID。您真的很想确保捕获它是什么 PID,然后重置 nice 和优先级,以免影响其他任何东西。
与其他人一样,从长远来看,您确实需要重塑这个 table。分区很有用,但不是最终目的,因为您有 运行 1.3TiB 的在线数据,并且您声明您只需要从最近的 3-6 个分区中读取。来自添加本机分区之前的 MySQL,我认为这对于 VIEW 和单独的 table 来说是一个很好的例子(当您需要翻转时自动更新 VIEW)。它还可以让您轻松地将一些较旧的 table 移动到离线存储。