PostgreSQL DBA(179)-invalid memory alloc request size XXX

问题描述

客户反馈在查询bytea字段时,报错:

atlasdb=# select * from t_bytea;
2020-12-25 15:28:18.804 CST [1884] ERROR:  invalid memory alloc request size XXX
2020-12-25 15:28:18.804 CST [1884] STATEMENT:  select * from t_bytea;
ERROR:  invalid memory alloc request size XXX

测试脚本

构造测试脚本

drop table t_bytea;
create table t_bytea(id int,c1 bytea);
truncate table t_bytea;
insert into t_bytea values(1,E'\\000');
select * from t_bytea;
-- 插入256M特殊字符(ASCII 0)
do
$$
declare
begin
    for i in 1..28 loop
        update t_bytea set c1 = c1||(select b.c1 from t_bytea b);
    end loop;
end
$$;
select length(c1) from t_bytea;
-- bytea_output='escape',出错!
set bytea_output='escape';
select * from t_bytea;
-- bytea_output='hex',不出错
set bytea_output='hex';
select * from t_bytea;
-- 加大到512M特殊字符
update t_bytea set c1 = c1||(select b.c1 from t_bytea b);
-- bytea_output='hex',出错!
set bytea_output='hex';
select * from t_bytea;

原因分析

bytea_output有两种输出方式,一种是hex,即十六进制输出,一种是escape即转义字符输出.
1.hex格式
内存申请的上限是1G - 1,理论上字段最大值为(512M-2)个字节,其中需预留2个字节用于输出(因为”\”是转义字符,因此需2个字节存储”\“),1个字节用于输出”x”
即:

atlasdb=# truncate table t_bytea;
TRUNCATE TABLE
atlasdb=# insert into t_bytea values(1,E'\\000');
INSERT 0 1
atlasdb=#
atlasdb=# select * from t_bytea;
 id |  c1
----+------
  1 | \x00
(1 row)

2.escape输出
这种格式输出涉及3种类型的字符:
A.”\”,由于该字符是转义字符,因此需要前面加”\”,需要2个字节的空间,输出为”\“
B.特殊编码字符,所谓的特殊编码是指单位小于0x20和大于0x7E的十六进制值,输出格式为”\nnn”,需要4个字节的空间,如ASCII 0,输出为”\000”
C.普通的ASCII字符,如a,b,A,+,-等等,直接输出,只需要1个字节的空间,输出为a,b,A,+,-等
理论上,如果bytea字段中存储的全部都是特殊编码十六进制值,那么该字段最大只能是256MB.

atlasdb=# set bytea_output='escape';
SET
atlasdb=# select * from t_bytea;
 id |  c1
----+------
  1 | \000
(1 row)

源码解读

Datum
byteaout(PG_FUNCTION_ARGS)
{
    bytea       *vlena = PG_GETARG_BYTEA_PP(0);
    char       *result;
    char       *rp;
    if (bytea_output == BYTEA_OUTPUT_HEX)//hex格式
    {
        /* Print hex format */
        rp = result = palloc(VARSIZE_ANY_EXHDR(vlena) * 2 + 2 + 1);
        *rp++ = '\\';//上面分配空间时额外的2个字节,即2+1中的2
        *rp++ = 'x';///上面分配空间时额外的1个字节,即2+1中的1
        rp += hex_encode(VARDATA_ANY(vlena), VARSIZE_ANY_EXHDR(vlena), rp);
    }
    else if (bytea_output == BYTEA_OUTPUT_ESCAPE)//escape格式
    {
        /* Print traditional escaped format */
        char       *vp;
        uint64        len;
        int            i;
        len = 1;                /* empty string has 1 char */
        vp = VARDATA_ANY(vlena);
        for (i = VARSIZE_ANY_EXHDR(vlena); i != 0; i--, vp++)
        {
            if (*vp == '\\')//字符"\",申请2个字节,即"\\"
                len += 2;
            else if ((unsigned char) *vp < 0x20 || (unsigned char) *vp > 0x7e)
                len += 4;//小于0x20 和 大于0x7E,申请4个字节,输出格式为:\XXX,如\000
            else
                len++;//常规的ASCII字符,如a,b,A等字符,只需要1个字节
        }
    ......
请使用浏览器的分享功能分享到微信等