【GaussDB】内存资源告急:深度诊断一起“memory temporarily unavailable“故障

【GaussDB】诊断一起内存临时不可用的问题


📋 背景

在客户测试环境中(GaussDB 506.0 SPC0100 集中式 ),一个重度使用存储过程的系统,频繁出现内存临时不可用的问题(ERROR: memory is temporarily unavailable)。令人困惑的是,这个环境配置的内存大小已经数十倍于迁移前的Oracle。

🔍 GaussDB提供的观测方法

GaussDB官方产品文档中有一个《内存过载》章节,详细描述了在出现内存过载情况下如何定位,但似乎这个章节并没有在官方网站上公开。不过GaussDB松鼠会倒是把这个章节的内容发成一篇文章了:《GaussDB内存过载分析》

下面按照本次分析过程进行描述。

📊 内存监控视图

GaussDB提供了几个重要的内存视图:

gs_total_memory_detail

显示当前数据库节点的内存使用情况,单位为MB。当GUC参数enable_memory_limit的值为off时,本视图不可用。

gs_session_memory_detail

显示会话的内存使用情况,以MemoryContext节点来统计。当开启线程池(enable_thread_pool = on)时,该视图包含所有线程和会话的内存使用情况。当GUC参数enable_memory_limit的值为off时,本视图不可用。

gs_shared_memory_detail

查询当前节点所有已产生的共享内存上下文的使用信息。

📈 内存使用情况示例

sql 复制代码
SELECT * FROM gs_total_memory_detail;
nodename memorytype memorymbytes
primary max_process_memory 24576
primary process_used_memory 7847
primary max_dynamic_memory 16651
primary dynamic_used_memory 1843
primary dynamic_peak_memory 2942
primary dynamic_used_shrctx 1386
primary dynamic_peak_shrctx 1387
primary max_backend_memory 1360
primary backend_used_memory 1
primary high_backend_thread_memory 20
primary high_backend_thread_max_memory 768
primary medium_backend_thread_memory 113
primary medium_backend_thread_max_memory 1536
primary low_backend_thread_memory 0
primary low_backend_thread_max_memory 768
primary max_shared_memory 6548
primary shared_used_memory 5402
primary max_cstore_memory 16
primary cstore_used_memory 0
primary max_sctpcomm_memory 0
primary sctpcomm_used_memory 0
primary sctpcomm_peak_memory 0
primary other_used_memory 527
primary gpu_max_dynamic_memory 0
primary gpu_dynamic_used_memory 0
primary gpu_dynamic_peak_memory 0
primary pooler_conn_memory 0
primary pooler_freeconn_memory 0
primary storage_compress_memory 0
primary udf_reserved_memory 0
primary llvm_used_memory 0
primary max_htap_memory 0
primary htap_used_memory 0

🧭 内存类型理解指南

要真实理解gs_total_memory_detail里这些内存类型的关系,并且理解gs_session_memory_detailgs_shared_memory_detail记录的内存对应到gs_total_memory_detail的映射关系,需要明确以下几点:

📏 内存指标含义
  • max 表示最大可以为多少,超过就会报内存不足
  • used 表示当前已经申请了多少
  • peak 表示本次数据库启动以来的历史used峰值
🔗 内存分配关系
  • max_process_memory 是通过参数设置的
  • max_shared_memory 主要为shared_buffers,但是这个值的大小可能会比shared_buffers要大
  • max_dynamic_memorymax_process_memory - max_shared_memory - max_cstore_memory - max_backend_memory - ... 得到的

dynamic_used_memory 包括了:

  • 动态会话内存(gs_total_memory_detail里没列出来)
  • dynamic_used_shrctx
  • high_backend_thread_memory
  • medium_backend_thread_memory
  • low_backend_thread_memory

💡 注意:后三个high/medium/low暂时不用关心,这是新版本中引入的特性,为后台线程专门分出来的内存。

🗂️ GaussDB内存区域划分

在GaussDB中,主要的内存区域分为:

复制代码
📁 共享内存(主要为shared_buffers)
📁 动态内存(dynamic_memory)
   ├── 📄 动态会话内存 (gs_session_memory_detail)
   ├── 📄 动态共享内存 (dynamic_used_shrctx) (gs_shared_memory_detail)
   └── 📄 动态后台内存

⚠️ 重要提醒 :这里出现两个"共享内存",非常容易产生理解上的歧义。比如gs_shared_memory_detail这个视图,查的就不是"共享内存",而是"动态共享内存"。

可以这么理解:

  • shared_buffers主要是data buffer,即表和索引的数据
  • GaussDB实现的global plan cache/global plsql cache/unique sql hash table等全局缓存,并不属于shared_buffers,而是在dynamic_used_shrctx

📝 内存追踪配置

内存不足的报错经常是一个瞬间冲高,回头再来看这些视图时,已经找不到对应的内存上下文,因此GaussDB在出现内存不足的报错时,会自动把当时的内存上下文情况生成到单独的日志文件中。

memory_trace_level 参数

参数说明 :动态内存使用超过最大动态内存的90%后,记录内存申请信息的管控等级。该参数仅在GUC参数use_workload_managerenable_memory_limit打开时生效。

参数类型 :枚举类型
参数单位:无

取值范围

级别 说明
none 表示不记录内存申请信息
level1 动态内存使用超过最大动态内存的90%后,会记录以下信息,并将记录的内存信息保存在$GAUSSLOG/mem_log目录下: • 全局内存概况 • instance,session,thread三种类型的所有内存上下文中内存占用前20的内存上下文的内存使用情况 • 每个内存上下文的totalsize、freesize字段
level2 在level1的基础上,还会记录每个内存上下文上所有内存申请的详细信息,包含申请内存所在的文件,行号和大小

默认值:level1

⚠️ 设置建议:该参数设置为level2后,会记录每个内存上下文的内存申请详情(file,line,size字段),会对性能影响较大,需慎重设置。

相关功能

  • 记录的内存快照信息可以通过系统函数gs_get_history_memory_detail(cstring)查询
  • 记录的内存上下文是经过将同一类型所有重名的内存上下文进行汇总之后得到的

🔬 现场分析

🕵️ 初步发现

由于默认是level1,没有记录申请内存的源码文件和行号,因此后面现场调成了level2。

当时的mem_log中有记录两个比较可疑的内存上下文:

位置 上下文名称 数量 大小
shared_memory unique sql hash table 1个 ~8G
session_memory SRF multi-call context 1个 ~8G

然后回头查gs_shared_memory_detailgs_session_memory_detail这两个视图,发现的确可以查到unique sql hash table占了8G,但是查不到SRF multi-call context

📊 监控脚本

客户DBA写了个监控脚本,每隔20秒采集一下各个内存视图的数据。

但几天后,仍然没有采集到SRF multi-call context,而数据库日志中仍然会时不时报错内存临时不可用。

💡 关键发现

在一次偶然的连续查询gs_total_memory_detail时,发现已使用的动态内存上下波动幅度高达8GB

于是猜想可能是一个非常短的时间产生了SRF multi-call context这个上下文,执行完后就释放了,所以间隔20秒采一次能采到的概率非常低。

🔍 深入分析

数据库大厂的内核研发往往都只负责各自一小块领域,华为内核研发远程查看日志,并没有发现什么可疑的点,而且还拉了好几个不同领域的内核研发,都没有找到原因。

但是在远程会议中,客户快速一页一页翻日志时,我发现一个现象:

在出现内存临时不可用报错的前面一段,会频繁打印相同格式的日志,而且中间有个数字一直在涨,从1XXXXXXXXX一直涨到7XXXXXXXXX,然后就没有这个重复日志了。

直觉告诉我,很可能与这个有关。于是让客户回到这段日志前面,然后发现了类似下面的这么一行:

log 复制代码
2025-07-25 10:00:01.523 primary RdsAdmin postgres xxx.xxx.xxx.xxx 140216907527936 12[0:0#0] 0 gsql 00000 31806672368306287 [BACKEND] LOG:  00000: unique_sql_id=3740540431, Current SQL is consuming about 1099 MB, allocating 102921000 bytes, memctx:SRF multi-call context, appname:, user_id: 16616, current query is: select usename,count(*) from dbe_perf.statement group by usename

🎯 问题串联

很明显这里就出现了SRF multi-call context,然后这段日志下面就是不断地在申请内存,直到申请了8G,后面很快就有一个业务调用存储过程报错内存不足了。

也就是说,在这个环境中,查询一次dbe_perf.statement这个视图,就会申请8G内存。这个查询,是TPOPS端定时采集的SQL。

dbe_perf.statement这个视图里实际上是一个查询特定内存数据的返回多行的函数,查询的就是unique sql hash table里的内容。

📝 问题链条分析

分析到这里,感觉大部分疑点都串起来了:

  1. ⚡ 由于某种原因,归一化SQL的内存达到了8G
  2. 🔄 然后由于TPOPS定时查询dbe_perf.statement,导致内存瞬间又消耗了8GB
  3. 📈 光这两个动态内存上下文就占了16GB
  4. 💾 由于这是个重度存储过程的系统,GLOBAL PLSQL CACHE又占了几个GB
  5. ❌ 动态内存全用完了,导致业务会话报错

❓ 进一步探究

归一化内存为什么会达到8GB?

我在很早就问过华为内核研发:track_activity_query_size=40960这么设置,如果SQL没达到这个长度,占用的内存是SQL实际的长度还是40960?

我一直记得在openGauss系数据库中,这个内存一定是按设置的这个大小申请,不会根据SQL实际长度变化,但是华为内核研发说在GaussDB中内存占用只会按实际SQL长度来,除非SQL长度达到或者超过这个数,才会申请这么多内存。

由于GaussDB并没有开源,所以当时我只能相信华为说的,可能GaussDB做了优化。

🧮 巧合的乘积

可是客户现场出现这个现象时,我直接计算了下:

复制代码
40,960 × 200,000 = 8,192,000,000

刚好差不多8G,这么巧合?

然后客户也查了,实际在dbe_perf.statement里query的汇总字节数,也就只有一百多MB。

但是华为内核研发说他看了那段代码,说的确是按实际长度来的,但是他又无法解释这8GB到哪里去了,因为算上其他属性的长度,每行最多就再加几百KB。


🤖 问问万能的AI

GaussDB没开源,但是openGauss是开源的,本来想用Cursor来分析一下,奈何被断供了,只能先用vscode的Copilot了。

打开openGauss源码目录,切换Copilot到agent模式,开问:

第一轮提问

Dark-Athena: 看下SRF multi-call context 这个上下文在什么时候会产生
GitHub Copilot :我来帮你分析 "SRF multi-call context" 这个上下文在什么时候会产生。让我先搜索相关代码来了解这个上下文的创建和使用场景。 现在让我搜索更多关于 SRF(Set-Returning Functions)和多次调用上下文的信息: 让我查看一些使用这些函数的实际例子: 现在让我查看一个具体的SRF函数实现例子来理解上下文的产生时机: 现在让我查看头文件中的宏定义和更多的注释说明: 现在让我查看一下具体的内存上下文创建代码: 让我再查看一些具体的SRF函数使用例子: 让我查找更多的SRF函数实现例子: 让我查找一些实际的SRF函数实现: 现在让我查看一个具体的pg_stat函数来理解SRF上下文的产生: 根据对代码的分析,我现在可以详细回答你的问题:

SRF multi-call context 产生的时机和原因

"SRF multi-call context" 是 Set-Returning Functions(集合返回函数)的多次调用上下文,它在以下情况下产生:

1. 什么是SRF?

SRF(Set-Returning Functions)是能够返回多行数据的函数,比如:

  • pg_stat_get_activity() - 返回所有活动会话信息
  • unnest() - 展开数组为多行
  • generate_series() - 生成数字序列
  • 各种系统统计函数

2. 上下文产生的具体时机

SRF multi-call context 在以下时机被创建:

  1. 首次调用时 :当SRF函数第一次被调用时,通过 SRF_FIRSTCALL_INIT() 宏(实际调用 init_MultiFuncCall())创建:
cpp 复制代码
if (SRF_IS_FIRSTCALL()) {
   funcctx = SRF_FIRSTCALL_INIT();  // 这里创建 "SRF multi-call context"
   // 初始化操作...
}
  1. 内存上下文创建位置 :在 init_MultiFuncCall() 函数中:
cpp 复制代码
multi_call_ctx = AllocSetContextCreate(fcinfo->flinfo->fn_mcxt,
   "SRF multi-call context",    // 这就是你看到的上下文名称
   ALLOCSET_SMALL_MINSIZE,
   ALLOCSET_SMALL_INITSIZE,
   ALLOCSET_SMALL_MAXSIZE);

3. 为什么需要这个上下文?

  1. 跨调用数据保存:SRF函数需要在多次调用之间保持状态信息
  2. 内存管理:为跨调用的数据分配长期有效的内存
  3. 性能优化:避免每次调用都重新计算所有结果

4. 使用场景举例

pg_stat_get_activity() 为例:

  • 第1次调用:创建上下文,获取所有会话信息,返回第1行
  • 第2次调用:使用已有上下文,返回第2行
  • 第N次调用:返回第N行
  • 最后一次:清理上下文,结束返回

5. 生命周期

cpp 复制代码
// 第一次调用 - 创建上下文
funcctx = SRF_FIRSTCALL_INIT();

// 后续调用 - 使用现有上下文  
funcctx = SRF_PERCALL_SETUP();

// 最后 - 清理上下文
SRF_RETURN_DONE(funcctx);  // 调用 end_MultiFuncCall()

6. 典型的SRF函数模式

cpp 复制代码
Datum my_srf_function(PG_FUNCTION_ARGS) {
   FuncCallContext *funcctx;
   
   if (SRF_IS_FIRSTCALL()) {
       // 创建 "SRF multi-call context"
       funcctx = SRF_FIRSTCALL_INIT();
       
       // 在这个上下文中分配跨调用的数据
       MemoryContext oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
       // ... 初始化操作
       MemoryContextSwitchTo(oldcontext);
   }
   
   funcctx = SRF_PERCALL_SETUP();
   
   if (funcctx->call_cntr < funcctx->max_calls) {
       // 返回下一行数据
       SRF_RETURN_NEXT(funcctx, result);
   } else {
       // 清理上下文并结束
       SRF_RETURN_DONE(funcctx);
   }
}

总结:SRF multi-call context 在任何集合返回函数第一次被调用时产生,用于在函数的多次调用之间保持状态和数据,直到函数返回完所有结果后被清理。

第二轮提问

Dark-Athena: 分析下执行get_instr_unique_sql这个函数时,申请的"SRF multi-call context"占用大小与什么有关
GitHub Copilot : 我来分析 get_instr_unique_sql 函数执行时 "SRF multi-call context" 内存占用的情况。首先让我搜索这个函数的实现。 让我搜索这个函数的具体实现: 现在让我找到这个函数的具体实现: 让我搜索这个函数的具体实现文件: 现在让我查看这个函数的具体实现: 现在让我查看 GetUniqueSQLStat 函数,这是关键的数据获取函数: 现在让我查看内存分配的关键部分和数据结构定义: 现在让我查找相关的宏定义和常量: 现在让我查看 UniqueSQL 数据结构定义: 让我查找 UniqueSQL 结构体的定义: 基于对代码的深入分析,我现在可以详细回答你关于 get_instr_unique_sql 函数执行时 "SRF multi-call context" 内存占用与什么相关的问题:

SRF multi-call context 在 get_instr_unique_sql 中的内存占用分析

1. 内存占用的核心影响因素

SRF multi-call context 的内存占用主要与以下几个因素密切相关:

1.1 Unique SQL 条目数量
cpp 复制代码
*num = hash_get_num_entries(g_instance.stat_cxt.UniqueSQLHashtbl);
  • 哈希表中存储的 unique SQL 条目总数是最主要的影响因素
  • 条目数量越多,需要分配的内存越大
1.2 每个条目的大小
cpp 复制代码
int malloc_size = (int)MAX_MEM_UNIQUE_SQL_ENTRY_COUNT * (sizeof(UniqueSQL) + unique_sql_str_len);

单个条目大小包括:

  • UniqueSQL 结构体:约 200+ 字节(包含统计信息、时间信息、网络信息等)
  • SQL 字符串长度UNIQUE_SQL_MAX_LEN = g_instance.attr.attr_common.pgstat_track_activity_query_size + 1
1.3 节点类型和配置
cpp 复制代码
int unique_sql_str_len = (IS_PGXC_COORDINATOR || IS_SINGLE_NODE) ? UNIQUE_SQL_MAX_LEN : 0;
  • CN 节点或单机:需要存储完整的 SQL 字符串,内存占用更大
  • DN 节点:不存储 SQL 字符串,内存占用相对较小

2. 内存分配策略

2.1 批次分配机制
cpp 复制代码
#define MAX_MEM_UNIQUE_SQL_ENTRY_COUNT 1000.0

for (int j = 0; j < ceil(*num / MAX_MEM_UNIQUE_SQL_ENTRY_COUNT); j++) {
   int malloc_size = (int)MAX_MEM_UNIQUE_SQL_ENTRY_COUNT * (sizeof(UniqueSQL) + unique_sql_str_len);
   unique_sql_array = (UniqueSQL*)palloc0_noexcept(malloc_size);
}
  • 分批处理:每批最多处理 1000 个条目
  • 避免大块内存分配:防止因条目过多导致的内存分配失败
2.2 内存布局
cpp 复制代码
/* memory format: [entry_1] [entry_2] [entry_3] ... | [sql_1] [sql_2] [sql_3] ... */
  • 结构体数组 + SQL 字符串数组的连续内存布局
  • 提高内存访问效率

3. 具体内存计算公式

3.1 CN 节点或单机环境
复制代码
总内存 = 批次数 × 每批内存大小
每批内存大小 = 1000 × (sizeof(UniqueSQL) + pgstat_track_activity_query_size + 1)
3.2 DN 节点环境
复制代码
总内存 = 批次数 × 每批内存大小
每批内存大小 = 1000 × sizeof(UniqueSQL)

4. 主要配置参数影响

4.1 instr_unique_sql_count
  • 直接影响:控制系统中最多能存储多少个 unique SQL 条目
  • 内存影响:条目数越多,SRF 上下文需要的内存越大
4.2 pgstat_track_activity_query_size
  • 直接影响:控制每个 SQL 字符串的最大长度
  • 在 CN 节点上影响巨大:该值越大,每个条目占用的内存越多

5. 内存占用估算示例

假设配置如下:

  • instr_unique_sql_count = 5000 (5000个条目)
  • pgstat_track_activity_query_size = 4096 (4KB SQL字符串)
  • sizeof(UniqueSQL) ≈ 200 字节

CN 节点内存占用:

复制代码
批次数 = ceil(5000 / 1000) = 5 批
每批内存 = 1000 × (200 + 4096 + 1) = 4,297,000 字节 ≈ 4.3 MB
总内存 ≈ 5 × 4.3 MB = 21.5 MB

DN 节点内存占用:

复制代码
批次数 = 5 批
每批内存 = 1000 × 200 = 200,000 字节 ≈ 200 KB
总内存 ≈ 5 × 200 KB = 1 MB

6. 内存优化和保护机制

6.1 错误处理
cpp 复制代码
if (unique_sql_array == NULL) {
   ereport(ERROR, (errmsg("[UniqueSQL] palloc0 error when querying unique sql stat!")));
}
6.2 内存清理
cpp 复制代码
if (funcctx->user_fctx) {
   list_free_deep(((UniqueSQLResults *)(funcctx->user_fctx))->batch_list);
   pfree_ext(funcctx->user_fctx);
}

总结

SRF multi-call contextget_instr_unique_sql 函数中的内存占用主要取决于:

  1. 系统中 unique SQL 条目的总数量(最关键因素)
  2. 节点类型(CN/单机 vs DN)
  3. SQL 字符串的最大长度配置
  4. UniqueSQL 结构体的大小

在生产环境中,建议根据实际的 SQL 多样性和系统内存容量合理配置 instr_unique_sql_countpgstat_track_activity_query_size 参数,以平衡监控精度和内存占用。

第三轮关键提问

Dark-Athena: 如果我一条SQL的长度没达到pgstat_track_activity_query_size,也会申请pgstat_track_activity_query_size这个值大小的内存么?
GitHub Copilot : Summarized conversation history我来帮你分析一下在openGauss中,当SQL长度没达到pgstat_track_activity_query_size时的内存申请情况。 让我再搜索更具体的信息,关于SQL字符串的内存分配机制。 让我重新搜索更直接相关的代码: 现在让我搜索关于st_activity缓冲区分配的代码: 基于对代码的分析,我可以回答你的问题了:

回答:是的,即使SQL的实际长度没有达到pgstat_track_activity_query_size,系统也会为每个后端进程预先分配pgstat_track_activity_query_size大小的内存空间。

详细分析:

1. 内存预分配机制

在pgstat.cpp的BackendStatusShmemInit函数中,可以看到:

cpp 复制代码
/* Create or attach to the shared activity buffer */
t_thrd.shemem_ptr_cxt.BackendActivityBufferSize =
   mul_size(g_instance.attr.attr_common.pgstat_track_activity_query_size, BackendStatusArray_size);
t_thrd.shemem_ptr_cxt.BackendActivityBuffer =
   (char*)ShmemInitStruct("Backend Activity Buffer", t_thrd.shemem_ptr_cxt.BackendActivityBufferSize, &found);

if (!found) {
   /* Initialize st_activity pointers. */
   buffer = t_thrd.shemem_ptr_cxt.BackendActivityBuffer;
   for (i = 0; i < BackendStatusArray_size; i++) {
       t_thrd.shemem_ptr_cxt.BackendStatusArray[i].st_activity = buffer;
       buffer += g_instance.attr.attr_common.pgstat_track_activity_query_size;  // 每个后端进程分配固定大小
   }
}

2. SQL写入机制

pgstat_report_activity函数中,当记录SQL活动时:

cpp 复制代码
if (cmd_str != NULL) {
   len = pg_mbcliplen(cmd_str, strlen(cmd_str), g_instance.attr.attr_common.pgstat_track_activity_query_size - 1);
}

// ...
rc = memcpy_s((char*)beentry->st_activity, g_instance.attr.attr_common.pgstat_track_activity_query_size,
   cmd_str, len);
beentry->st_activity[len] = '\0';

这里的关键点是:

  • len是实际要复制的SQL长度(经过截断处理)
  • 但是memcpy_s的目标缓冲区大小始终是pgstat_track_activity_query_size
  • 只有实际使用的长度是变化的,但预分配的空间是固定的

3. 内存占用情况

  • 预分配空间 :每个后端进程都会分配pgstat_track_activity_query_size字节的空间
  • 实际使用:只使用SQL实际长度 + 1(null终止符)的空间
  • 浪费空间pgstat_track_activity_query_size - (实际SQL长度 + 1)字节

4. 设计原因

这种预分配设计的原因:

  1. 性能考虑:避免动态内存分配/释放的开销
  2. 并发安全:共享内存中的固定布局,避免竞争条件
  3. 简化管理:统一的内存布局便于管理和访问

所以答案是肯定的:即使你的SQL很短(比如SELECT 1;),系统仍然会为每个后端进程的st_activity字段分配完整的pgstat_track_activity_query_size大小的内存空间。


🎯 结论分析

结合AI的分析,可以得出以下结论:

📌 核心发现

  1. 📏 固定内存分配

    • track_activity_query_size是固定会申请的内存大小
    • 占用的内存大小不会由于SQL比这个值小而减小
    • 实际单个unique sql所占用的空间,会比track_activity_query_size还要大一些
    • instr_unique_sql_count为GaussDB默认的200000,track_activity_query_size为40960时,unique sql hash table能达到8GB
  2. 🔄 SRF函数特性

    • SRF函数就是返回多行的函数
    • dbe_perf.statement里就是一个SRF函数
    • 为了在返回多行时保持数据稳定性,就会把查询结果全存到内存里,直到查询完才释放
  3. 💾 内存复制机制

    • dbe_perf.statement查的就是unique sql hash table的内容
    • SRF为了数据稳定性,几乎把这8GB的内存复制了一份,放到了SRF multi-call context里

🔍 深层问题探究

看上去这就是个非常低级的问题?但是问题还没完:

为什么unique sql会那么快达到200000个?

这个环境才刚部署没几天,而且都是跑的存储过程,没有什么拼接SQL,理论上SQL数是稳定有限的。

通过截取statement.query前200个字符进行group by计数排序,发现很大一部分SQL都是一个declare开头的自治事务匿名块。

🔧 根本原因

由于我之前和openGauss社区的研发一起分析过plsql编译依赖这个功能的问题,当时为了解决编译时数据字典表锁释放的问题,我提出了可以起个自治事务去编译,这样能避免长时间持有元数据锁的问题。而GaussDB也是用的这个方案(巧合?)。

所以我一眼就看出来GaussDB里记录的这些SQL是在做存储过程编译,但是GaussDB把整个存储过程的内容都放到这个匿名块里了,而且还有一些oid信息,因此必然是不同的SQL。

这个匿名块还会做一些数据字典的DML操作,这些DML的SQL是固定不变的,大概有6~7条。再来就是dbe_perf.statement里,对于parent_unique_sql_id不同的同一个unique_sql_id,会视为不同的记录。

⚡ 问题链条完整梳理

  1. 存储过程编译 :每次改一个存储过程,就在dbe_perf.statement里生成了约10条的记录
  2. 重复生成:反复改就反复生成,有时候全量存储过程重建,数字唰唰唰就涨上去了
  3. 资源耗尽:200000也经不起这么耗,真正的业务SQL可能还会计不进去了
  4. 内存占用:全是存储过程编译时自动产生的这些SQL

目前客户这边只能先调小track_activity_query_size到4K,让dbe_perf.statement的内存占得小些,至少让业务测试能继续进行下去。


🧪 复现方法

诊断了这么多天的问题,弄清楚原理后,非常容易复现unique sql hash tableSRF multi-call context申请大量内存的现象:

🔧 环境配置

sql 复制代码
-- 这个是单个会话的unique sql上限,由于模拟测试都在一个会话里,所以要调大
gs_guc reload -c "track_stmt_session_slot=200000"

-- 直接设置到最大值,内存涨得最快
gs_guc reload -c "track_activity_query_size=102400"

-- 重启,释放 unique sql 内存
gs_ctl restart

📝 测试脚本

sql 复制代码
-- 构造不同的SQL
-- 注意GaussDB对于表别名或字段别名不同、常量不同的也会进行归一化
-- 所以这里直接用不同的对象名
DECLARE
    x_sql1 text;
    x_sql2 text;
    i int;
BEGIN
    FOR i IN 1..5000 
    LOOP
        x_sql1 := 'CREATE TABLE t_abcd'||i||'(a int);';
        x_sql2 := 'DROP TABLE t_abcd'||i;
        EXECUTE IMMEDIATE x_sql1;
        EXECUTE IMMEDIATE x_sql2;
    END LOOP;
    RAISE NOTICE '%', x_sql1;
END;
/

📊 验证结果

sql 复制代码
-- 查询动态共享内存里unique sql hash table的大小
SELECT 
    contextname,
    count(1),
    ROUND(sum(totalsize)/1024/1024,2) AS TOTALSIZE_MB,
    ROUND(Sum(usedsize)/1024/1024) AS usedsize_MB 
FROM GS_shared_MEMORY_DETAIL  
GROUP BY contextname 
ORDER BY 3 DESC 
LIMIT 10;
contextname count totalsize_mb usedsize_mb
unique sql hash table 1 1468.75 1469
DefaultTopMemoryContext 1 172.57 60
ASP unique sql hash table 1 82.11 82
... ... ... ...
  • unique sql hash table占1个多GB
sql 复制代码
-- 查询SQL实际长度所占的空间,和按track_activity_query_size的长度计算的空间
SELECT 
    sum(lengthb(query))/1024/1024 AS actual_size_mb,
    (count(1)*current_setting('track_activity_query_size')::int)/1024/1024 AS allocated_size_mb
FROM dbe_perf.statement;
actual_size_mb allocated_size_mb
0.38 1401.07
  • 按track_activity_query_size来算,也是1个多GB。

📋 日志验证

执行查询后,日志中出现SRF multi-call context

log 复制代码
2025-07-25 10:00:01.523 primary admin postgres 172.17.0.1 140216907527936 12[0:0#0] 0 Mogeaver 23.3.1 - SQLEditor <Script-22.sql> 00000 31806672368306287 [BACKEND] LOG:  00000: unique_sql_id=3740540431, Current SQL is consuming about 1099 MB, allocating 102921000 bytes, memctx:SRF multi-call context, appname:, user_id: 16616, current query is: select sum( lengthb(query) )/?/?,(count(?)*current_setting(?)::int)/?/? from dbe_perf.statement
2025-07-25 10:00:01.523 primary admin postgres 172.17.0.1 140216907527936 12[0:0#0] 0 Mogeaver 23.3.1 - SQLEditor <Script-22.sql> 00000 31806672368306287 [BACKEND] LOCATION:  mmgr_memory_check_reach_one_gb, framework_memory_memctrl_util.cpp:241
2025-07-25 10:00:01.523 primary admin postgres 172.17.0.1 140216907527936 12[0:0#0] 0 Mogeaver 23.3.1 - SQLEditor <Script-22.sql> 00000 31806672368306287 [INSTR] LOG:  00000: [UniqueSQL] idx[10] - new memory allocated: 102921000
2025-07-25 10:00:01.523 primary admin postgres 172.17.0.1 140216907527936 12[0:0#0] 0 Mogeaver 23.3.1 - SQLEditor <Script-22.sql> 00000 31806672368306287 [INSTR] LOCATION:  unique_sql_log_result_mem, instr_unique_sql.cpp:1407
2025-07-25 10:00:01.524 primary admin postgres 172.17.0.1 140216907527936 12[0:0#0] 0 Mogeaver 23.3.1 - SQLEditor <Script-22.sql> 00000 31806672368306287 [INSTR] LOG:  00000: [UniqueSQL] total memory allocated: 1132131000
2025-07-25 10:00:01.524 primary admin postgres 172.17.0.1 140216907527936 12[0:0#0] 0 Mogeaver 23.3.1 - SQLEditor <Script-22.sql> 00000 31806672368306287 [INSTR] LOCATION:  unique_sql_log_result_mem, instr_unique_sql.cpp:1408

💡 总结

🎯 经验教训

  1. 📚 经验的积累非常重要

    • 国产数据库使用过程中肯定会发现很多新问题,其中有一些综合性的问题,原厂的内核研发都不一定能找到原因,具备综合技能的DBA能从现场的蛛丝马迹结合长期的工作经验所产生的直觉,来发现问题症结所在
  2. 🧠 知识的积累非常重要

    • 如果连内存区域都分不清,就算数据库已经提供了很多方法来进行诊断,面对本文这个问题,脑子里只能是一团浆糊
  3. 🤖 需要学会借助AI

    • 现在的AI有了agent,能自动且快速完成很多事,本文中就让AI自动翻源码分析,对问题进行了快速定位
相关推荐
小陈工1 小时前
Python Web开发入门(十七):Vue.js与Python后端集成——让前后端真正“握手言和“
开发语言·前端·javascript·数据库·vue.js·人工智能·python
科技小花6 小时前
数据治理平台架构演进观察:AI原生设计如何重构企业数据管理范式
数据库·重构·架构·数据治理·ai-native·ai原生
一江寒逸6 小时前
零基础从入门到精通MySQL(中篇):进阶篇——吃透多表查询、事务核心与高级特性,搞定复杂业务SQL
数据库·sql·mysql
D4c-lovetrain6 小时前
linux个人心得22 (mysql)
数据库·mysql
阿里小阿希6 小时前
CentOS7 PostgreSQL 9.2 升级到 15 完整教程
数据库·postgresql
荒川之神6 小时前
Oracle 数据仓库雪花模型设计(完整实战方案)
数据库·数据仓库·oracle
做个文艺程序员7 小时前
MySQL安全加固十大硬核操作
数据库·mysql·安全
不吃香菜学java7 小时前
Redis简单应用
数据库·spring boot·tomcat·maven
一个天蝎座 白勺 程序猿7 小时前
Apache IoTDB(15):IoTDB查询写回(INTO子句)深度解析——从语法到实战的ETL全链路指南
数据库·apache·etl·iotdb
不知名的老吴7 小时前
Redis的延迟瓶颈:TCP栈开销无法避免
数据库·redis·缓存