背景

我们执行一个大SQL时(长度大于512M),会返回如下错误:

  1. ERROR: invalid memory alloc request size 1073741824

复现

我们首先复现出来这个问题

  1. 创建表

    1. create table byteatable(id int, obj bytea);
  2. 插入512M大对象

    1. #!/bin/bash
    2. data='a'
    3. for ((i=1;i<=29;i++));
    4. do
    5. data=$data$data
    6. done
    7. echo 'build ok'
    8. psql -U postgres -d postgres << EOF
    9. insert into byteatable(id,obj) values (1,"$data");
    10. EOF
    11. echo 'OK'

执行脚本后就能复现出来了

  1. ERROR: invalid memory alloc request size 1073741824

BUG分析

我们先找到出现这个错误的位置。

源码位置:

  1. void *
  2. MemoryContextAlloc(MemoryContext context, Size size)
  3. {
  4. void *ret;
  5. AssertArg(MemoryContextIsValid(context));
  6. if (!AllocSizeIsValid(size))
  7. elog(ERROR, "invalid memory alloc request size %zu", size);
  8. context->isReset = false;
  9. ret = (*context->methods->alloc) (context, size);
  10. VALGRIND_MEMPOOL_ALLOC(context, ret, size);
  11. return ret;
  12. }
  13. #define AllocSizeIsValid(size) ((Size) (size) <= MaxAllocSize)
  14. #define MaxAllocSize ((Size) 0x3fffffff) /* 1 gigabyte - 1 */

这里限制的内存是1G - 1,而我们插入的大SQL需要的内存没有1G,那么为什么还提示非法的申请内存大小呢?通过调试跟踪,我们发现是在词法分析的时刻出错的。主要问题是在词法分析的内存申请机制上,申请的内存肯定不能少于sql的长度,如果当前申请的内存不够用,那么将重新申请当前内存乘以2的内存大小。

  1. int literallen; /* actual current string length */
  2. int literalalloc; /* current allocated buffer size */
  3. //当前申请内存初始化
  4. yyext->literalalloc = 1024;
  1. static void
  2. addlit(char *ytext, int yleng, core_yyscan_t yyscanner)
  3. {
  4. /* enlarge buffer if needed */
  5. if ((yyextra->literallen + yleng) >= yyextra->literalalloc)
  6. {
  7. do {
  8. yyextra->literalalloc *= 2;
  9. } while ((yyextra->literallen + yleng) >= yyextra->literalalloc);
  10. yyextra->literalbuf = (char *) repalloc(yyextra->literalbuf,
  11. yyextra->literalalloc);
  12. }
  13. /* append new data */
  14. memcpy(yyextra->literalbuf + yyextra->literallen, ytext, yleng);
  15. yyextra->literallen += yleng;
  16. }
  17. static void
  18. addlitchar(unsigned char ychar, core_yyscan_t yyscanner)
  19. {
  20. /* enlarge buffer if needed */
  21. if ((yyextra->literallen + 1) >= yyextra->literalalloc)
  22. {
  23. yyextra->literalalloc *= 2;
  24. yyextra->literalbuf = (char *) repalloc(yyextra->literalbuf,
  25. yyextra->literalalloc);
  26. }
  27. }

从源码中可以看出,每次申请原申请内存的2倍,即yyextra->literalalloc *= 2; 而最大申请内存限制是

  1. #define MaxAllocSize ((Size) 0x3fffffff) /* 1 gigabyte - 1 */

所以我们在词法分析能申请的最大内存是2^29 = 536870912,如果词法分析SQL语句需要的内存大于536870912,那么申请的内存需要再乘2,就会得到2^30 = 1073741824,超过MaxAllocSize=0x3fffffff= 1073741823。所以会提示错误:

  1. ERROR: invalid memory alloc request size 1073741824

当然不仅仅是插入一个大对象才会引起这个问题,只要是SQL语句长度大于512M都是出现这个错误,我们可以使用select复现:

  1. do language plpgsql $$
  2. declare
  3. v_text text := 'a';
  4. begin
  5. for i in 1..29 loop
  6. v_text:=v_text||v_text;
  7. end loop;
  8. execute $_$select '$_$||v_text||$_$'$_$;
  9. raise notice 'execute a sql large than 512MB success.';
  10. exception when others then
  11. raise notice 'execute a sql large than 512MB failed.';
  12. end;
  13. $$;

BUG修复

其实申请MaxAllocSize是可行的,通过修改源码实现,每当申请的内存大于MaxAllocSize并且SQL需要的长度小于MaxAllocSize时,我们就申请MaxAllocSize大小的内存。

  1. static void
  2. addlit(char *ytext, int yleng, core_yyscan_t yyscanner)
  3. {
  4. /* enlarge buffer if needed */
  5. if ((yyextra->literallen + yleng) >= yyextra->literalalloc)
  6. {
  7. do {
  8. yyextra->literalalloc *= 2;
  9. } while ((yyextra->literallen + yleng) >= yyextra->literalalloc);
  10. /* we can not alloc more than MaxAllocSize */
  11. if (yyextra->literalalloc > MaxAllocSize && (yyextra->literallen + yleng) < MaxAllocSize)
  12. yyextra->literalalloc = MaxAllocSize;
  13. yyextra->literalbuf = (char *) repalloc(yyextra->literalbuf,
  14. yyextra->literalalloc);
  15. }
  16. /* append new data */
  17. memcpy(yyextra->literalbuf + yyextra->literallen, ytext, yleng);
  18. yyextra->literallen += yleng;
  19. }
  1. static void
  2. addlitchar(unsigned char ychar, core_yyscan_t yyscanner)
  3. {
  4. /* enlarge buffer if needed */
  5. if ((yyextra->literallen + 1) >= yyextra->literalalloc)
  6. {
  7. yyextra->literalalloc *= 2;
  8. /* we can not alloc more than MaxAllocSize */
  9. if (yyextra->literalalloc > MaxAllocSize && (yyextra->literallen + 1) < MaxAllocSize)
  10. yyextra->literalalloc = MaxAllocSize;
  11. yyextra->literalbuf = (char *) repalloc(yyextra->literalbuf,
  12. yyextra->literalalloc);
  13. }
  14. /* append new data */
  15. yyextra->literalbuf[yyextra->literallen] = ychar;
  16. yyextra->literallen += 1;
  17. }

修复之后可以正常插入了

  1. $ sh pgtest.sh
  2. build ok
  3. INSERT 0 1
  4. OK