【问题标题】:Optimal way to store BLOBs larger than max_allowed_packet in MySQL InnoDB在 MySQL InnoDB 中存储大于 max_allowed_pa​​cket 的 BLOB 的最佳方法
【发布时间】:2015-06-18 11:49:32
【问题描述】:

也许这个问题应该在https://dba.stackexchange.com/ 上提问,我不确定。请在 cmets 中告知或移到那里。

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

总结

我将在InnoDB 表的BLOB 列中将照片存储在数据库中,我想知道执行此操作的最佳方法。我正在寻找可以比较不同变体的官方文档或一些方法。

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

数据库架构

这是架构的相关部分(到目前为止)。有一个表Contracts,其中包含有关每个合同和主要ID 键的一些一般信息。 每个合同可以拍摄几张(~10)张照片,所以我有一张桌子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

对于每张照片,我将存储原始全分辨率图像以及一些缩小版本,所以我有一张桌子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

最后,有一个表格保存了所有图像的实际二进制数据。我知道 MySQL 允许在 LONGBLOB 列中存储多达 4GB,但在搜索过程中,我遇到了另一个 MySQL 限制:max_allowed_packet。在我的 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 存储引擎首先最大限度地减少浪费的空间,其次是整体性能,最佳方式是什么?

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

最大行长度,除了可变长度列(VARBINARY, VARCHAR、BLOB 和 TEXT),略小于数据库的一半 页。即最大行长度约为 8000 字节。

我真的希望官方文档比 大约 8000 字节更精确。以下段落最有趣:

如果一行小于半页长,则全部存储在本地 页面内。如果超过半页,可变长度列 被选为外部页外存储,直到行适合 半页。对于选择用于页外存储的列,InnoDB 存储 行中本地的前 768 个字节,其余的外部进入 溢出页面。每个这样的列都有自己的溢出页面列表。 768 字节的前缀伴随着一个 20 字节的值,用于存储 列的真实长度并指向溢出列表,其中 其余的值被存储。

考虑到上述情况,至少可以有以下策略:

  • 选择这样的块大小,它将存储在页面本地而不涉及页外存储。
  • 选择这样的块大小,以使整个 BLOB 存储在页外。
  • 我不喜欢将 BLOB 部分存储在页面内,部分存储在页面外的想法。但是,嘿,也许我错了。

我也遇到了这个文档https://dev.mysql.com/doc/refman/5.6/en/innodb-row-format-dynamic.html,此时我意识到我想问这个问题。现在对我来说太难了,我希望有人对这个话题有过实践经验。

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

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

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

编辑

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

客户端和服务器都有自己的 max_allowed_pa​​cket 变量,所以如果你想处理大数据包,你必须增加这个 客户端和服务器中的变量。

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

编辑 2

@akostadinov 指出有mysql_stmt_send_long_data() 将BLOB 数据以块的形式发送到服务器,而said 的人已经设法将大于max_allowed_packet 的BLOB 发送到INSERT。尽管如此,即使我设法INSERT,比如说,20MB BLOB 和max_allowed_packet=4MB 我如何SELECT 它回来?我不知道我该怎么做。

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

【问题讨论】:

  • 您知道可以在 Amazon RDS 上使用 DB Parameter Groups 修改 max_allowed_packet 吗?
  • 据我所知不是重复的,抱歉标记
  • 我不确定你为什么说你“最终每行花费 16KB”。我只能想象您认为 InnoDB 仅限于每个索引页一条记录?那是绝对不正确的:每个索引页将被多条记录填充,直到它无法容纳更多。实际上,记录的内联存储数据长度小于半页的要求是确保每个页面包含至少两条记录!
  • 我也不确定为什么你“不喜欢将 BLOB 部分存储在页面内和部分离页的想法”,因为这是一种性能增强(MySQL 必须始终考虑首先页面是为了找到对该字段的页外存储的引用,因此同时遇到前 768 个字节的数据会节省一点 I/O)。但是,如果您真的不喜欢它,COMPRESSEDDYNAMIC 格式都不会在索引页面中存储前缀。
  • 我已经在上面解释过这些假设是错误的。如果您有大小为 8200 字节的块,则 BLOB 将始终存储在索引页之外——只有内部行开销、剩余列和 20 字节 BLOB 引用将被内联存储(也是如果您使用其中一种行格式,则为 768 字节前缀);因此 许多 记录仍将适合每个页面。如果您的块的大小足够小,两个记录可以放在一个索引页面中,那么根据定义,您将在每个页面中实现多个记录,但您在此过程中浪费了大量开销。

标签: mysql blob innodb


【解决方案1】:

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

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

【讨论】:

  • 感谢您的清单。我会尽量让我的问题更简洁,并突出我的特别担忧。
  • “两年前的答案”实际上是在此之前两年发布的。它是论坛中 Ricks 先前答案的链接集合。在forums.mysql.com/read.php?52,584053,584159#msg-584159 找到它也可以查看 Ricks 页面mysql.rjweb.org 有大量的 mysql 东西可以详述。他的 forums.mysql.com 最好的令人印象深刻:mysql.rjweb.org/bestof.html 干得好,瑞克!
  • 谢谢@rein。几年前,我离开了 mysql.forum。在这样做的过程中,我不再保持“最好的”。 MySQL 变化不是很快,所以 most 的“best of”仍然有效。但是,8.0 中没有任何实质性的变化。我维护 mysql.rjweb.com 的其余部分。当我发现错误和过期信息时,我还会在 stackoverflow 和 dba.stackexchange 上编辑我的答案。
【解决方案2】:

尝试的一种方法是使用长发送,如下所述: 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 自己进行优化。

【讨论】:

  • 谢谢。尽管答案是php,但听起来很有希望。我使用的是 C API,而且我之前从未在 MySQL 中使用过准备好的语句(我的项目从来不需要),所以我没有查看它们可用的函数集。我一定会检查出来的。 stmt->send_long_data 可能是关键。我还在关于max_allowed_packet 的问题中添加了注释。如果可能的话,我宁愿不要对用户添加人为限制(最大 16mb 图像大小)。
  • @VladimirBaranov,限制不是人为的。例如如果你期望一个头像,那么提供 16MB 是不合理的大。如果用户发送更多数据,那么他正在做一些错误的事情或试图滥用系统。所以这取决于用例。
  • 我的案例是照片。现在它们是 ~5MB JPEG,这意味着根据场景和压缩质量,文件大小可能会增加。下一代相机将拥有更多的百万像素,因此几年后 16MB 可能还不够。此外,如果他们想在系统投入生产并取得成功后除了 JPEG 之外还存储 RAW 怎么办?很难预测这样的事情。此外,还会有第三方应用程序从该数据库中读取数据。这就是为什么我更愿意使用带有块的通用解决方案,以保证可以与任何服务器和任何客户端一起使用。
  • 如果我不能/不想/太难改变max_allowed_packet,我有什么选择?我看了mysql_stmt_send_long_data()。 1) 从 MySQL 5.6.3 开始,max_allowed_packet 系统变量控制可以使用mysql_stmt_send_long_data() 发送的参数值的最大大小。哎呀。看起来他们堵住了漏洞。 2)即使我设法INSERT,比如说,20MB BLOB 和max_allowed_packet=4MB 我如何SELECT 它回来?我不知道我该怎么做。
  • @VladimirBaranov,这看起来像您正在寻找的东西(除非您使用分块):stackoverflow.com/questions/8489338/… 这也可能对您有所帮助:stackoverflow.com/questions/8257941/… ...如果您进行分块,那么您不会'不需要任何特别的东西。只需读取所有块并将它们流式传输到目标。
猜你喜欢
  • 2017-01-15
  • 2015-08-31
  • 2011-09-07
  • 1970-01-01
  • 2010-09-10
  • 2016-11-01
  • 1970-01-01
  • 2010-09-29
相关资源
最近更新 更多