我的理解是,新的 RDS 实例将根据需要从快照中“分页”块,如 EC2 卷所述这里。
这目前让我很头疼:我正在一个新的测试实例上运行一个大型查询;它应该需要 10-15 分钟才能运行,但它已经运行了一个小时。这个实例有 1,000GB 的存储空间,所以有 3,000 IOPS,但我在控制台中看到的总 IOPS 不到 100(有时读取 IOPS 不到 20)。
通常我会mysqldump
进行完整的数据库备份并将其发送给/dev/null
-- 但这需要 12-18 小时。过去我曾一次对多个表进行表扫描查询,希望这些 IO 能够并行发生。
有谁知道更好的方法来预热音量?
答案1
由于看起来没有比我已经使用的方法更简单的方法,所以我决定评估这些方法。
对于测试环境,我为每种方法启动了一个实例:r3.large(2 VCPU,15 GB RAM),每个实例使用相同的快照。这些实例有 1,000 GB 的磁盘,因此应该能够维持 3,000 IOPS。
整个数据库包含数百个表,从几百行到几亿行不等(几个表主要用于日志记录,但可能涉及一些报告查询)。
我选择了两个表进行评估:我们的“用户”表,它包含 2000 万行并且非常宽;以及一个“链接”表,它也包含 20MM 行但只有两列。
加载后,我对用户表运行了两个查询:一个通过对非索引数字字段求和来强制进行表扫描,另一个对索引列执行聚合操作(应遍历整个索引)。我没有对链接表运行查询,因为它似乎没有提供更多信息。
所有计时均采用 H:MM:SS(小时:分钟:秒)格式,并且来自单次运行。我还使用 Cloudwatch 指标跟踪了读取和写入 IOPS(通常平均为 5-15 分钟)。
我们的数据库使用 MySQL,但我相信通用方法适用于任何 DBMS。
方法
将表转储到 /dev/null
mysqldump CONNECTION_OPTIONS --compress DATABASE TABLES > /dev/null
该mysqldump
程序用于备份数据库或单个表。它检索所有表数据并将其写入 StdOut,同时使用 DDL 重新创建表及其索引。
由于我并不关心是否真正备份表,因此我将输出重定向到/dev/null
。由于我不想被网络所阻碍,因此我使用了选项--compress
。即便如此,我还是在同一可用区的 EC2 实例上运行,以将所有网络流量保持在 Amazon 数据中心内。
这种方法的主要缺点是它不会触及索引块。
Users Linkage
---------------------------------------------
| time to touch blocks | 00:53:38 | 00:03:02 |
| read IOPS | < 150 | 150+ |
| table-scan | 00:01:29 | |
| index aggregate | 00:00:15 | |
强制表扫描查询
与测试查询非常相似,此方法是一种简单的选择,可从非索引字段聚合数据。我为“touch”查询和“test”查询选择了不同的字段。
与转储操作一样,这仅访问表数据块。我可以将其扩展为通过某种形式的索引聚合来访问索引块,但我认为这与我的需求不太相关。
Users Linkage
---------------------------------------------
| time to touch blocks | 00:59:12 | 00:03:31 |
| read IOPS | 150 | 150 |
| table-scan | 00:02:04 | |
| index aggregate | 00:00:19 | |
优化
这优化表命令将重建 InnoDB 表和索引,并在此过程中释放空间。它是 MySQL 特有的,但我认为 PostgresVACUUM
命令是类似的,而且我相信其他数据库系统也有等效的命令。
对于我们的大型表来说,这可能是一个不公平的测试,因为它需要进行多次更新,而且毫无疑问在其多年的生命周期中从未进行过优化。如果我们定期进行优化,也许数字会更低。
Users Linkage
----------------------------------------------
| time to touch blocks | 02:01:36 | 00:03:44 |
| read IOPS | 100 | 150 |
| write IOPS | 500+ | 1000+ |
| table-scan | 00:00:05 | |
| index aggregate | 00:00:01 | |
您会注意到我添加了一行用于写入 IOPS。此外,这些查询时间并非印刷错误:它们比其他查询快一个数量级以上。我怀疑这是因为这些块被缓存在内存中(我可能应该在接触块和执行查询之间重新启动实例)。
概括
OPTIMIZE
对于大型表来说速度会显著变慢,并且会使用过多的写入 IOPS。但是,如果我使用的是 Postgres,那么这VACUUM
可能是一个有效的选择,前提是源数据库定期进行清理。
全行选择和 之间的差异mysqldump
很小,可能是由于网络或 VM 负载。但是,mysqldump
执行起来要容易得多,因为全行选择需要一些思考来选择合适的查询。
运行完这些测试后,我启动了一个新实例并启动了 10 个并发mysqldump
会话(随机在它们之间划分表)。一些观察结果:
- 大多数会议都在 6 小时内结束。有几场会议持续了 7 个小时,还有一场(有几张大桌子)持续了 8 个小时以上。
- CPU 一直保持在 40% 左右。我怀疑这完全是由于压缩造成的。
- 读取 IOPS 缓慢从~400 增加到~600(使用 15 分钟的平均周期),并在接近尾声时飙升至 > 1000。
随着我对这个问题的进一步思考,我开始相信我的痛苦主要是因为我正在使用此实例来测试报告负载。如果它是一个 OLTP 实例,我怀疑我可以以最小的痛苦(尽管性能较慢)将其转换为服务。然而,同样的痛苦会影响读取副本,也许更严重,因为你只会在系统负载过重时启动另一个副本。
从长远来看,我只能希望亚马逊能够添加一个并行触及卷块的“快速初始化”操作。
答案2
不幸的是,强制将块调入页的唯一方法是获取它们,并且您已经找到了多种方法来做到这一点。
答案3
我一直在使用一个非常简单的脚本,它将生成一个查询,我很确定它应该触及数据库上的所有数据和索引块:
SCHEMA_NAME=your_schema_name
MYSQL_CMD="mysql -sN $SCHEMA_NAME"
for TBL in $($MYSQL_CMD -e "show tables;" | sort); do
ALL_IDX=$($MYSQL_CMD -e "SHOW INDEX FROM $TBL;")
while IFS= read -r IDX_DATA; do
Key_name=$(echo $IDX_DATA | cut -d " " -f3)
Seq_in_index=$(echo $IDX_DATA | cut -d " " -f4)
Column_name=$(echo $IDX_DATA | cut -d " " -f5)
if [[ $Seq_in_index -ne 1 ]]; then
continue
fi
printf 'SELECT COUNT(*) FROM `%s`.`%s` WHERE CRC32(`%s`) = 1;\n' "$SCHEMA_NAME" "$TBL" "$Column_name"
done <<< "$ALL_IDX"
done
这将生成如下查询:
SELECT COUNT(*) FROM `db`.`tb1` WHERE CRC32(`id`) = 1;
SELECT COUNT(*) FROM `db`.`tb2` WHERE CRC32(`col1`) = 1;
SELECT COUNT(*) FROM `db`.`tb2` WHERE CRC32(`col2`) = 1;
为了加快进程,您可以并行运行查询。这样会更快,OPTIMIZE
因为您不会在进程中发生写入,并且应该触及所有块,这与mysqldump
仅触及数据块的方法相反。