在一个巨大的 INSERT 完成后,我怎样才能弄清楚 InnoDB 取得了什么进展?

How can I figure out what progress InnoDB is making after a huge INSERT completes?

我正在使用 MariaDB:mysql Ver 15.1 Distrib 10.3.32-MariaDB。我从 Ubuntu 的默认值更改的唯一配置选项是我在删除大量行之前遇到一些问题后更改了 innodb_buffer_pool_size = 1G。我没有设置复制。

我刚刚将大约 6 亿行插入 table。我的所有 tables 都是 InnoDB,当然,系统 tables 除外。 table (edges) 非常简单;两个整数列组成一个复合主键。插入过程大约用了 4 天并成功完成(INSERT INTO edges SELECT some, columns FROM a INNER JOIN b ON a.column = b.foreign_key)。

在 INSERT 期间,我知道将插入多少行,因此为了大致了解进度,我将启动一个单独的 mysql 查询提示和 运行 SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED 后跟 SELECT COUNT(*) FROM the_table 并找出到目前为止插入了多少行。我也 运行 SHOW PROCESSLIST 这只是显示了 运行 已经多长时间了。通过做一些数学运算,我能够粗略地估计出事情需要多长时间:从 SHOW PROCESSLIST 中获取查询 运行ning 的秒数,并根据粗略的行数进行推断到目前为止插入了多少行,我认为会产生多少行。

这是我有点迷失的地方:成功返回我插入行的提示后,平均负载飙升到比 INSERT 期间更高的水平。我看到这都是因为来自 MySQL.

的 IO

SHOW PROCESSLIST 并没有真正说明什么,尽管很明显一些进程 运行 在几个不同的线程上运行,如 htop.

所示

我最接近弄清楚发生了什么的是 SHOW ENGINE INNODB STATUS。这是一些与日志相关的数据:

好像大数字一直在增加,但是我很困惑,因为它说0 pending log flushes,但是Log flushed up to一直在增加,根本没有查询运行ning。

这里是INSERT BUFFER AND ADAPTIVE HASH INDEX,但是

这是要点形式的输出:https://gist.github.com/hut8/3508c901e6ebfaab65656b8ae3c32ae3

CREATE TABLE 对于正在插入的 table:

 CREATE TABLE `edges` (                                                         
    `source_page_id` int(8) unsigned NOT NULL,                                             
    `dest_page_id` int(8) unsigned NOT NULL,                                               
    PRIMARY KEY (`source_page_id`,`dest_page_id`))

插入语句是:

INSERT INTO edges (source_page_id, dest_page_id)
     SELECT pl.pl_from, v.page_id
       FROM pagelinks pl
 INNER JOIN vertexes v
         ON v.page_title = pl.pl_title;

作为该联接中数据源的两个 table 如下所示:

 CREATE TABLE `pagelinks` ( 
`pl_from` int(8) unsigned NOT NULL DEFAULT 0,      
`pl_namespace` int(11) NOT NULL DEFAULT 0,                 
`pl_title` varbinary(255) NOT NULL DEFAULT '',         
`pl_from_namespace` int(11) NOT NULL DEFAULT 0  
 )ENGINE=InnoDB DEFAULT CHARSET=binary ROW_FORMAT=COMPRESSED;

CREATE TABLE `vertexes` ( 
`page_id` int(8) unsigned NOT NULL DEFAULT 0,                              
`page_title` varbinary(255) NOT NULL DEFAULT '',                         
PRIMARY KEY (`page_id`),  
KEY `page_title_index` (`page_title`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

如您所见,pagelinks 上没有索引。

MariaDB [wiki]> explain SELECT pl.pl_from, v.page_id
FROM pagelinks pl
INNER JOIN vertexes v
ON v.page_title = pl.pl_title\G
*************************** 1. row *************************** 
id: 1
select_type: SIMPLE
table: pl
type: ALL
possible_keys: NULL
key: NULL 
key_len: NULL
ref: NULL
rows: 676208507 
Extra: 
*************************** 2. row *************************** 
id: 1
select_type: SIMPLE
table: v
type: ref
possible_keys: page_title_index 
key: page_title_index
key_len: 257
ref: wiki.pl.pl_title
rows: 1
Extra: Using index

基本上,我很困惑服务器实际上在做什么,以及需要多长时间才能完成。谁能指出我正确的方向?同样,我只是想知道 INSERT 似乎已完成后会发生什么。非常感谢。

运行 SHOW PROCESSLIST with suitable permissions -- 你只能看到允许你看到的命令。

平均负载和 I/O 可能因多种原因而有所不同。 UNIQUE 索引被即时检查和更新; non-unique 索引稍后插入(参见“更改缓冲”)。这可能 解释了插入返回后的尖峰。不用担心。

您的服务器(或 VM)有多小?缓冲池 1G 意味着 600M 行会有很多 I/O。

“进度条”仅部分可行。

您会经常重新加载 600M 行吗?如果是这样,我们可以讨论更好的方法。这是 table 的完全替代品吗?还是大多数行都一样?或者什么?

一次插入 600M 行的一大成本是需要读取 ROLLBACK 整个事务。一旦是 COMMITted,所有“回滚”的东西都需要清理。这可能是您 post-commit 峰值的一部分。

如果其他连接正在接触 table,这个大插入可能 运行 比没有接触它时慢。

I will only periodically replace all the rows and will never update them. I am getting the data directly from Wikipedia's dumps and don't need the most up to date data. I could insert with no primary key and then build it later, or just add an index on the first column because that's all I will query by.

假设您正在获取 CSV 文件,请按以下方式执行:

  1. CREATE TABLE new_tbl ... -- 有了 PRIMARY KEY
  2. 使用 OS 的排序以 PK 顺序对 csv 文件进行排序。
  3. LOAD DATA INFILE ... 变成 new_tbl.
  4. RENAME TABLE tbl TO old_tbl, new_tbl TO tbl;
  5. DROP TABLE tbl;

备注:

  • 交换对性能来说很糟糕;由于您只有 2GB 的 RAM,请将 innodb_buffer_pool_size 降低到仅 500M。
  • 您可能需要在任何地方都使用 utf8mb4。 (但我想这并不重要,因为我看到 VARBINARY.
  • ROW_FORMAT=COMPRESSED可能不值得。
  • 步骤 1、4、5 几乎是瞬间完成的。
  • 排序可能是最快的排序方式。
  • 有了数据 pre-sorted,填充 PK 几乎是免费的。
  • 基本上不会有停机时间,因为第 4 步非常快。
  • 不需要“进度条”
  • 由于加载本质上是“顺序的”,因此 buffer_pool 远小于 table 大小并不重要。
  • 通过构建一个新的 table,需要处理的“Rollback/undo/redo/etc”东西更少了。
  • “如您所见,页面链接上没有索引。” -- 不需要索引,因为它将完全扫描 table。

分块 INSERT...SELECT:

假设 JOIN 确实减慢了 4 天的插入速度,让我们分块查询。也就是说,让我们一次遍历 pagelinks 1000 行,然后 COMMIT。这将涉及 vertexes 1000 次中的 1000 次查找(提取的近 1000 个 16KB 块 = 16MB)。这种定期提交有助于减轻 Bill 讨论的开销。有关分块的提示,请参阅:http://mysql.rjweb.org/doc.php/deletebig#deleting_in_chunks

InnoDB 旨在以各种方式推迟昂贵的 I/O 工作。当您将更改写入事务中的行时,它只会修改 RAM 中的页面——在缓冲池中。但它也会在磁盘上的 InnoDB 重做日志中记录更改,因此如果发生崩溃,它可以恢复。然后缓冲池中的页面在一段时间后刷新到磁盘。

但是如果您要写入 6 亿行,您肯定会一遍又一遍地填满重做日志。发生这种情况时,InnoDB 被迫立即将修改后的页面从 RAM 刷新到磁盘。

除此之外,二级索引的构建进一步推迟。这是 innodb 状态中记录的“插入缓冲区”。因此提交事务,稍后刷新数据页,稍后“合并”二级索引页。

基本上,在你做了很多 inserts/updates 之后,还有很多清理工作要做。这就是为什么您在认为插入完成后继续看到 I/O 加载一段时间的原因。

如果高写入率是零星的,InnoDB 的设计是明智的。它试图非常快速地做出反应,并推迟一些工作。希望它能在你有另一波写入之前“赶上”。但是你正试图做一个 four-day 长期激增,所以 InnoDB 最终会越来越落后。一旦事情平静下来,它必须在真正完成之前进行大量的刷新和索引合并。

您如何监控进度?这样做的方法还没有得到很好的发展。通常您不必监视它。您可以通过这种方式查询 innodb 指标:

SELECT * FROM INFORMATION_SCHEMA.INNODB_METRICS WHERE NAME LIKE '%ibuf%';

但插入缓冲区监视器似乎默认情况下被禁用(所有 ibuf-related 指标在我的 MySQL 8.0 实例中显示 status: disabled)。阅读 https://dev.mysql.com/doc/refman/8.0/en/innodb-information-schema-metrics-table.html 以了解如何启用特定的 innodb 监视器。

解释这些 innodb 指标的结果有点神秘。您可以阅读 https://dev.mysql.com/doc/refman/8.0/en/innodb-change-buffer.html 但您必须“仔细阅读字里行间”才能理解数字代表的含义。您甚至可能需要阅读一些 InnoDB 存储引擎的源代码。

我怀疑 Select 的 JOIN 的 98% 是由于 I/O 无法缓存造成的。提供足够大的 buffer_pool 到 'solve' 这个问题需要更多的 RAM。

所以,这是 B 计划

vertexes 添加另一列(加载后):

CREATE TABLE `vertexes` ( 
`page_id` int(8) unsigned NOT NULL DEFAULT 0,                              
`page_title` varbinary(255) NOT NULL DEFAULT '',   
title_hash BINARY(16) NOT NULL,  -- MD5 of the title                      
PRIMARY KEY (`page_id`),  
KEY `page_title_index` (`page_title`),
INDEX(title_hash, page_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

同样,在另一个table中有title_hash。现在,将 JOIN 更改为作用于 title_hash 而不是 page_title —— 一个小得多的值。因此,它缓存得更好。

PS。 MariaDB 10.3 可能 有一种自动进行散列的机制。

但是,这可能还不够好,所以这里是 C 计划:

让我们安排JOIN以“相同的顺序”作用于两个table。 (这从原来的table开始,没有散列。)即:

SELECT pl.pl_from, v.page_id FROM pagelinks pl INNER JOIN 顶点 v ON v.page_title = pl.pl_title;

如果我们有这两对 sorted,JOIN 将像拉链一样工作:

(pl_title, pl_from)
(page_title, page_id)

这可以通过

轻松完成
INDEX(pl_title, pl_from)
INDEX(page_title, page_id)  -- This already exists.

现在,JOIN 将(应该)使用这两个索引,它们都按字母顺序排列,从而使工作非常高效(wrt I/O)。

C 计划总结:

  • ALTER TABLE pagelinks ADD INDEX(page_title, page_id);

  • ORDER BY pl_title 添加到 SELECT 使它们按相同的顺序排列。

  • 插入时在 edges 上保留 PK;而是在加载 table.

    后执行 ALTER TABLE edges ADD PRIMARY KEY(source_page_id, dest_page_id)