在 MySQL InnoDB 中存储大于 max_allowed_packet 的 BLOB 的最佳方式

Optimal way to store BLOBs larger than max_allowed_packet in MySQL InnoDB

也许应该在 https://dba.stackexchange.com/ 上问这个问题,我不确定。请在评论中指教或移至那里。

对于这个项目,我使用 MySQL 5.6.19 托管在 Amazon RDS。

总结

我打算将照片存储在数据库中 InnoDB table 的 BLOB 列中,我想知道最佳的实现方式。我正在寻找官方文档或一些可以比较不同变体的方法。

在搜索该主题时,有很多关于将二进制文件存储在数据库 BLOB 中还是存储在只有文件路径和名称的数据库的文件系统中更好的讨论和问题。这样的讨论超出了这个问题的范围。对于这个项目,我需要一致性和引用完整性,因此文件将存储在 BLOB 中,问题是如何具体存储。

数据库架构

这是架构的相关部分(到目前为止)。有一个 table Contracts,其中包含有关每个合约和主 ID 密钥的一些一般信息。 对于每个合同,可以拍摄几张(~10)张照片,所以我有一个 table ContractPhotos:

CREATE TABLE `ContractPhotos` (
  `ID` int(11) NOT NULL,
  `ContractID` int(11) NOT NULL,
  `PhotoDateTime` datetime NOT NULL,
  PRIMARY KEY (`ID`),
  KEY `IX_ContractID` (`ContractID`),
  CONSTRAINT `FK_ContractPhotos_Contracts` FOREIGN KEY (`ContractID`) REFERENCES `Contracts` (`ID`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8

对于每张照片,我将存储原始全分辨率图像以及一些缩小版本,所以我有一个 table ContractPhotoVersions:

CREATE TABLE `ContractPhotoVersions` (
  `ID` int(11) NOT NULL,
  `ContractPhotoID` int(11) NOT NULL,
  `PhotoVersionTypeID` int(11) NOT NULL,
  `PhotoWidth` int(11) NOT NULL,
  `PhotoHeight` int(11) NOT NULL,
  `FileSize` int(11) NOT NULL,
  `FileMD5` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
  PRIMARY KEY (`ID`),
  KEY `IX_ContractPhotoID` (`ContractPhotoID`),
  CONSTRAINT `FK_ContractPhotoVersions_ContractPhotos` FOREIGN KEY (`ContractPhotoID`) REFERENCES `ContractPhotos` (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

最后,table 保存所有图像的实际二进制数据。我知道 MySQL 允许在 LONGBLOB columns, but during my search I came across another MySQL limitation: max_allowed_packet 中存储最多 4GB。在我的 MySQL 实例中,这个变量是 4MB。阅读文档后我对这个变量的理解是,实际上,单行不能超过 4MB。一张超过4MB的照片是很正常的,所以为了能够INSERTSELECT这样的文件我打算把文件分成小块:

CREATE TABLE `PhotoChunks` (
  `ID` int(11) NOT NULL,
  `ContractPhotoVersionID` int(11) NOT NULL,
  `ChunkNumber` int(11) NOT NULL,
  `ChunkSize` int(11) NOT NULL,
  `ChunkData` blob NOT NULL,
  PRIMARY KEY (`ID`),
  UNIQUE KEY `IX_ContractPhotoVersionID_ChunkNumber` (`ContractPhotoVersionID`,`ChunkNumber`),
  CONSTRAINT `FK_PhotoChunks_ContractPhotoVersions` FOREIGN KEY (`ContractPhotoVersionID`) REFERENCES `ContractPhotoVersions` (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

此外,我将能够一次将大照片几块上传到数据库中,并在连接断开时恢复上传。

数据量

估计数据量为 40,000 张全分辨率照片,每张约 5MB => 200GB。缩小版本很可能是 800x600,每个 ~120KB => + 额外 5GB。图片不会 UPDATEd。几年后,它们最终将被删除。

问题

有很多方法可以将文件拆分成更小的块:您可以将其拆分为 4KB、8KB、64KB 等。使用 InnoDB 存储引擎时,首先要尽量减少浪费的最佳方法是什么 space整体表现第二?

我找到了这些文档:http://dev.mysql.com/doc/refman/5.6/en/innodb-file-space.html,但是没有太多关于 BLOB 的细节。它说页面大小为 16KB。

The maximum row length, except for variable-length columns (VARBINARY, VARCHAR, BLOB and TEXT), is slightly less than half of a database page. That is, the maximum row length is about 8000 bytes.

我真的希望官方文档比大约 8000 字节更精确。下面这段话最有意思:

If a row is less than half a page long, all of it is stored locally within the page. If it exceeds half a page, variable-length columns are chosen for external off-page storage until the row fits within half a page. For a column chosen for off-page storage, InnoDB stores the first 768 bytes locally in the row, and the rest externally into overflow pages. Each such column has its own list of overflow pages. The 768-byte prefix is accompanied by a 20-byte value that stores the true length of the column and points into the overflow list where the rest of the value is stored.

考虑到以上至少可以有这些策略:

我也看到了这个文档 https://dev.mysql.com/doc/refman/5.6/en/innodb-row-format-dynamic.html 并且在这一点上我意识到我想问这个问题。现在对我来说太难了,我希望有人对此主题有实际经验。

我不想因为无意中选择了糟糕的块大小和行格式而浪费了一半的磁盘 space。我担心的是,如果我选择为每个块存储 8000 字节加上在 PhotoChunks table 的同一行中为 4 个整数存储 16 字节,它将超过页面大小的神奇一半,我最终花费对于仅 8000 字节的数据,每行 16KB。

有没有办法检查这种方式实际上浪费了多少 space?在 Amazon RDS 环境中,恐怕无法查看 InnoDB table 包含的实际文件。否则,我会简单地尝试不同的变体并查看最终文件大小。

到目前为止我可以看到有两个参数:行格式和块大小。也许还有其他需要考虑的事情。

编辑

为什么我不考虑更改 max_allowed_packet 变量。来自 doc:

Both the client and the server have their own max_allowed_packet variable, so if you want to handle big packets, you must increase this variable both in the client and in the server.

我使用 MySQL C API 来处理这个数据库,同一个 C++ 应用程序正在使用相同的 libmysql.dll 与其他 200 个 MySQL 服务器(与这个项目完全无关)通信。其中一些服务器仍然是 MySQL 3.23。所以我的应用程序必须与所有这些一起工作。坦率地说,我没有查看有关如何在 MySQL C API 的客户端更改 max_allowed_packet 变量的文档。

编辑 2

@akostadinov 指出 mysql_stmt_send_long_data() to send BLOB data to server in chunks and people said 他们已经设法 INSERT 个大于 max_allowed_packet 的 BLOB。尽管如此,即使我设法 INSERT,比方说,20MB BLOB 和 max_allowed_packet=4MB 我如何 SELECT 它回来?我不知道该怎么做。

如果你能指出正确的方向,我将不胜感激。

一种尝试方法是使用长发送,如下所述: Is there any way to insert a large value in a mysql DB without changing max_allowed_packet?

如您所建议的,另一种方法是将数据分成块。在此线程中查看一种可能的方法: http://forums.mysql.com/read.php?20,601656,601656

另一个是,假设您在用户界面上设置了一些图像最大大小限制,相应地增加数据包大小。是否允许大于 16MB 的图片?

如果你问我,我会避免实施分块,因为它看起来更像是过早的优化,而不是让 DB 自己进行优化。

我坚持我在 forums.mysql.com 2 年前的回答。一些进一步的说明:

  • 16M 可能适用于 max_allowed_packet,但我没有证据表明它适用于此之外。
  • 在我几年前开发的一个应用程序中,大约 50KB 的块大小似乎是 'optimal'。
  • max_allowed_packet可以在/etc/my.cnf中设置。但是,如果您无法访问它,您就会被它的价值所困。您可以通过 SHOW VARIABLES LIKE 'max_allowed_packet' 在任何(?)版本中获得它。 (我有理由确定回到 4.0,但不确定 3.23。)所以这可能是你的块大小的上限。
  • InnoDB 会将大 BLOB/TEXT 字段拆分为 16KB 的块。可能每个块都有一些开销,所以你不会得到恰好 16KB。
  • Antelope 与 Barracuda 以及其他设置控制是否将 767 字节的 BLOB 存储在记录中。如果 none 存储在那里,则有一个 20 字节的指针指向块外存储。
  • 今天,16MB 似乎是图片大小的合理限制;明天不会了。
  • 如果您是 运行 足够新的 MySQL 版本,innodb_page_size 可以从 16K 提高到 32K 或 64K。 (并且 ~8000 上升到 ~16000,但不是 ~32000。)
  • 如果涉及复制,分块就变得更加重要。但是对于块的 'sequence number' 可能会有一些额外的棘手问题。 (问我是否需要走这个方向。)
  • 将以上评论加在一起,我建议 MIN(64700, max_allowed_packet) 字节的块大小作为合理的折衷方案,即使您无法控制 innodb_file_format。只有 1-2% 的磁盘 space 会在这个 "photos" table 中被浪费(假设图片大约 1MB)。
  • 压缩没用; JPG 已经压缩。
  • 大部分时间都在I/O;其次是客户端和服务器之间的网络聊天。这里的重点是... C vs PHP 在性能方面不会有太大区别。
  • 每条记录约 8000 字节与本次讨论无关。这适用于具有很多列的 table —— 它们加起来不能超过 ~8K。大多数 BLOB 将离开页面,每行仅留下 60-800 字节,因此每 16KB 块有 15-200 行(平均,在其他类型的开销之后)。
  • PARTITION 不太可能有任何用处。
  • 是"chunking a premature optimization"吗?如果您因为 max_allowed_packet.
  • 而撞到砖墙,则它不是 "optimization"