为什么 MySQL 写入磁盘的数据比提交到数据库的数据多 5 倍?

Why is MySQL writing 5 times more data to disk, than is committed to the database?

我已经在 Ubuntu 20.04 之上安装了 MySQL 8.0.25,在 C5.2xlarge 实例上安装了 运行。

然后我 运行 一个用数据填充 10 个表的脚本。测试刚好用了 2 个小时,期间创建了 123146.5MB 的数据:

这意味着平均有 17.1MB/s 写入数据库。 但是,atop 报告了一些奇怪的事情:虽然它显示磁盘 activity 大约为 18-19MB/s,但它还显示进程 mysqld 在 10 秒示例中写入了 864MB - t运行 显示为 86.4MB /s,大约是实际提交到数据库的数据量的 5 倍:

为什么会有这样的差异?

iotop 通常还显示 MySQL 正在写入 5 倍:

pidstat 也一样:

我还尝试使用 Percona 工具包中的 pt-diskstats,但它没有显示任何内容...

我也在RDS上复现了这个问题。在这两种情况下(EC2 和 RDS),Cloudwatch 统计数据还显示 5 倍的写入...

数据库有 10 个表已填充。 其中 5 个定义如下:

CREATE TABLE `shark` (
  `height` int DEFAULT NULL,
  `weight` int DEFAULT NULL,
  `name` mediumtext,
  `shark_id` bigint NOT NULL,
  `owner_id` bigint DEFAULT NULL,
  PRIMARY KEY (`shark_id`),
  KEY `owner_id` (`owner_id`),
  CONSTRAINT `shark_ibfk_1` FOREIGN KEY (`owner_id`) REFERENCES `shark_owners` (`owner_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci 

另外5个表有这样的定义:

CREATE TABLE `shark_owners` (
  `name` mediumtext,
  `owner_id` bigint NOT NULL,
  PRIMARY KEY (`owner_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

如果差异大约是 2 倍,我可以理解 - 数据首先写入 t运行saction 日志,然后提交到数据库,但是 5 倍? 这是 MySQL 的正常行为,还是我的表中的某些内容触发了此行为? 为什么有这么多“取消的写入”——大约 12%?

  • LOAD DATA 运行速度非常快,最小 I/O
  • 每个查询至少 100 行的批量 INSERT 运行速度是单行插入的 10 倍。
  • autocommit 在每个 SQL 之后导致至少一个额外的 I/O(为了事务完整性)。
  • 50 1 行插入,然后 COMMIT 是一种妥协。
  • FOREIGN KEY 需要检查另一个 table.
  • 如果innodb_buffer_pool_size太小,会出现磁盘抖动
  • owner_id 是一个“二级索引”。它以半优化的方式完成,但可能涉及读取和写入,具体取决于各种因素。
  • 如果可以使用较小的数据类型,tables 会更小。 (例如,BIGINT 占用 8 个字节,通常是矫枉过正。)更小会导致更少 I/O。
  • name有多大? ROW_FORMAT用的是什么?他们密谋导致或多或少的“非记录”存储,因此磁盘 I/O.
  • 您在执行插入时是否使用了多个线程?

换句话说,需要更多细节才能分析您的问题。

MySQL 使用 InnoDB 表时多次写入数据。大多数情况下,为了防止数据丢失或损坏,这是值得的,但如果您需要更大的吞吐量,则可能需要降低耐用性。

如果您根本不需要耐用性,另一种解决方案是使用 MEMORY storage engine。这将消除除二进制日志和查询日志之外的所有写入。

您已经提到了 InnoDB 重做日志(又名事务日志)。这无法禁用,但您可以减少文件同步操作的数量。阅读 https://dev.mysql.com/doc/refman/8.0/en/innodb-parameters.html#sysvar_innodb_flush_log_at_trx_commit 了解详情。

innodb_flush_log_at_trx_commit = 0

您可以通过增加对 InnoDB 缓冲池的 RAM 分配来减少页面刷新次数,或帮助 MySQL 整合页面刷新。不要过度分配它,因为其他进程也需要 RAM。

innodb_buffer_pool_size = XXX

二进制日志是所有已提交更改的记录。您可以减少文件同步的次数。有关这如何影响性能的说明,请参阅 https://www.percona.com/blog/2018/05/04/how-binary-logs-and-filesystems-affect-mysql-performance/

sync_binlog = 0

如果您不关心复制或时间点恢复,您也可以完全禁用二进制日志。通过注释指令关闭二进制日志:

# log_bin

或者在 MySQL 8.0 中,他们终于有了明确禁用它的指令:

skip_log_bin

disable_log_bin

详情见https://dev.mysql.com/doc/refman/8.0/en/replication-options-binary-log.html#option_mysqld_log-bin

如果您的 MySQL 服务器在页面写入期间崩溃,则双写缓冲区用于防止数据库损坏。在禁用此功能之前请三思,但如果禁用它,它可以为您带来一些性能提升。 请参阅 https://www.percona.com/blog/2006/08/04/innodb-double-write/ 进行讨论。

innodb_doublewrite = 0

MySQL也有两种查询日志:普通查询日志和慢查询日志。这些都会导致一些开销,因此如果您需要最佳性能,请禁用查询日志。 https://www.percona.com/blog/2009/02/10/impact-of-logging-on-mysql’s-performance/

有一些方法可以使慢速查询日志保持启用状态,但前提是查询时间超过 N 秒。这减少了开销,但仍然允许您保留您可能想知道的最慢查询的日志。

long_query_time = 10

另一种策略是忘记优化写入次数,让它们发生。但是使用更快的存储。在 AWS 环境中,这意味着使用实例存储而不是 EBS 存储。如果实例终止,这会带来整个数据库可能丢失的风险,因此您应该保持良好的备份或副本。