前言
前阵子一位同事在迁移 Oracle 的时候,遇到这样一个报错:
invalid memory alloc request size 1293327093 (mcxt.c:1198)
原字段是 BLOB 类型,在 PostgreSQL 中则需要使用 BYTEA 类型。乍一看这个报错,大小约 1.29 GB,看起来貌似和 1GB 的 field size limit 有关,那么是否是这个原因呢?让我们简单分析一下。
多样的限制
数据库中存在着各种各样的限制,参照:https://www.postgresql.org/docs/current/limits.html,比如字段大小最大 1GB,默认 8KB blocksize 的情况下,表最大 32TB ,函数最多可以有 32 个参数等等。上面的报错看样子和 1GB 的限制有关,让我们尝试复现一下,先看个最为直观的样例:
1 | postgres=# create table bytea_test(data bytea); |
第一条语句插入 1023 MB 的数据成功了,但第二条插入 1024 MB 的数据失败了。
让我们看下代码中出错的位置
1 | /* |
通过 AllocSizeIsValid 判断所分配的大小,而判断依据则是是否小于 MaxAllocSize,即 1GB -1
1 |
因此,这个报错就很明显了——达到了 1GB 的限制。这个问题原因很好理解,让我们看一个略微晦涩的问题,先构造一个比较大的字符串:
1 | #!/bin/bash |
此处构造了一个大约为 512MB 的字符串:
1 | [postgres@mypg ~]$ ./import.sh |
这次报错的原因比较常见,OOM了,内存不足。那么 ExecutorState 又是个什么玩意?其实这一坨是内存上下文相关的东西
比如说我们需要为用户发来的命令,例如 select * from t,开辟一个内存空间并存储它,同时在对命令进行语法解析后生成的语法解析树也需要内存保存,因此 PostgreSQL 使用 MessageContext 来存储。
当执行一个事务时,一定会伴随着内存分配,比如元组的扫描、索引的扫描或者元组的排序等等,这些内存可能需要在事务结束后才释放,因此可由 CurTransactionContext、ExecutorState、PortalHeapMemory 等内存上下文来管理。
简而言之,PostgreSQL 在开始处理语句之前需要创建一个 ExecutorState 的内存上下文,此处需要 536870924 这么多的内存,但是剩余内存不足,便会报错 (我这台小破云主机总共 1GB 的内存),各位读者要实验的话可以基于当前服务器内存构造较大一点的字符串。
看完这个问题之后,让我们再看一个例子
1 | !/bin/bash |
执行这个脚本,便会提示:ERROR: invalid memory alloc request size 1073741824
关于这个问题,各位可以参考 PgSQL · 捉虫动态 · 执行大SQL语句提示无效的内存申请大小,主要问题是在词法分析的内存申请机制上,申请的内存肯定不能少于 SQL 的长度,如果当前申请的内存不够用,那么将重新申请当前内存乘以 2 的内存大小。从源码中可以看出,每次申请原申请内存的 2 倍,即yyextra->literalalloc *= 2; 而最大申请内存限制是
1 | #define MaxAllocSize ((Size) 0x3fffffff) /* 1 gigabyte - 1 */ |
所以我们在词法分析能申请的最大内存是 2^29 = 536870912,如果词法分析 SQL 语句需要的内存大于 536870912,那么申请的内存需要再乘 2,就会得到 2^30 = 1073741824,超过 MaxAllocSize=0x3fffffff= 1073741823。
当然不仅仅是插入一个大对象才会引起这个问题,只要是 SQL 语句长度大于 512M 都是出现这个错误,我们可以使用 select 复现
有趣的 varchar(n)
另外,各位在官网上会看到这样一句话
The notations varchar(n) and char(n) are aliases for character varying(n) and character(n), respectively. If specified, the length must be greater than zero and cannot exceed 10485760. character without length specifier is equivalent to character(1). If character varying is used without length specifier, the type accepts strings of any size. The latter is a PostgreSQL extension.
这个 10485760 是怎么来的?没错,也是代码里写死的
1 | /* |
所以当你定义了一个 varchar(10485761) 便会报错:
1 | postgres=# create table tt1(info varchar(10485761)); |
至于其他限制则是一样的,不能超过 1GB。除此之外,代码当中还有很多地方都是写死的,比如
vacuum truncate 的阈值:
so funny,不晓得这是社区开发者们拍脑袋想出来的值,还是一个经验值?
小结
所以知晓了这个原理之后,解决办法也很简单,将 Oracle 中超过 512MB 的行删除,再进行迁移即可。如下是我在 Oracle 侧统计到的结果:
646663545 * 2 = 1293327093 ,也就是说 PostgreSQL 去申请了 1293327093 这么多的大小,但是始终无法超过 MaxAllocSize (1GB),所以报的错是 invalid memory alloc request size 1293327093 (mcxt.c:1198)。
参考
PgSQL · 捉虫动态 · 执行大SQL语句提示无效的内存申请大小
PostgreSQL DBA(179)-invalid memory alloc request size XXX