注册
DMOCI编程时内存泄漏问题排查
专栏/技术分享/ 文章详情 /

DMOCI编程时内存泄漏问题排查

青空 2026/08/28 165 0 0
摘要

1. 背景介绍

客户在使用DMOCI编程时遇到了内存泄漏的问题,需要辅助排查。客户给出了pprof监控数据生成的内存图。需要排查是达梦数据库/DMOCI的问题还是因为在调用DMOCI时存在未释放的内存

2. 环境

内容 版本/类型
操作系统 Kylin-Server-V10-SP3-2403
CPU x86_64bit
达梦8数据库 8.1.4.80p32
DMOCI驱动 8.1.5.60

3. 验证过程

3.1. 先观察客户给出的pprof图

发现有许多函数都调用了OCIParamGet,图大致如此:

 a【0.0(0.0%)of 142.6(96.7%)】
/|\
bcd【略】
\|/
OCIParamGet【0.0(0.0%)of 142.6(96.7%)】
 |
dmoci_get_dtype_list_param【0.0(0.0%)of 142.6(96.7%)】
 |
di_malloc【145.6(98.7%)】

而OCIParamGet对应的cum累积内存较多,并且显示的flat反而为0.0,说明OCIParamGet本身占用的内存并不多

其调用的dmoci_get_dtype_list_param也显示了0.0(0.0%)的flat,再往下看发现di_malloc的内存占用为145.6(98.7%)

说明OCIParamGet间接调用di_malloc分配的内存较多,并且di_malloc分配的大部分堆区内存都用于OCIParamGet

联想到di_malloc也是一种malloc,合理怀疑是使用OCIParamGet过程中分配了内存但未释放,导致内存占用较多

以此作为前提尝试复现

3.2. 建表

先创建用户

SQL> CREATE USER "NQ_MON" IDENTIFIED BY "Dameng123"; 操作已执行 已用时间: 132.738(毫秒). 执行号:702. SQL> GRANT RESOURCE TO "NQ_MON"; 操作已执行 已用时间: 7.192(毫秒). 执行号:703.

然后创建表结构相同的表

SQL> CREATE TABLE "NQ_MON"."MONEVENT" 2 ( 3 "CURR_DATE" NUMBER(10,0) DEFAULT TO_NUMBER(TO_CHAR(SYSDATE, 'yyyymmdd')) NOT NULL, 4 "CURR_TIME" NUMBER(10,0) DEFAULT TO_NUMBER(TO_CHAR(SYSDATE, 'hh24miss')) NOT NULL, 5 "SERVER_ID" VARCHAR2(64) DEFAULT '' NOT NULL, 6 "SERVER_TYPE" NUMBER(10,0) DEFAULT 0 NOT NULL, 7 "EVENT_ID" NUMBER(10,0) DEFAULT 0 NOT NULL, 8 "EVENT_LEVEL" CHAR(1) DEFAULT '' NOT NULL, 9 "EVENT_NAME" VARCHAR2(255) DEFAULT '' NOT NULL, 10 "EVENT_INFO" VARCHAR2(2000) DEFAULT '' NOT NULL, 11 "DEAL_FLAG" CHAR(1) DEFAULT '0' NOT NULL 12 ) STORAGE (ON "MAIN", CLUSTERBTR); 操作已执行 已用时间: 456.134(毫秒). 执行号:704. SQL> CREATE OR REPLACE INDEX "NQ_MON"."IDX_MONEVENT" 2 ON "NQ_MON"."MONEVENT" ("CURR_DATE" ASC,"SERVER_ID" ASC,"CURR_TIME" ASC); 操作已执行 已用时间: 39.814(毫秒). 执行号:705. SQL> CREATE TABLE "NQ_MON"."MONPARAMEXTREM" 2 ( 3 "SERVER_ID" VARCHAR2(64) DEFAULT '' NOT NULL, 4 "SERVER_TYPE" NUMBER(10,0) DEFAULT 0 NOT NULL, 5 "PARAM_GROUP" VARCHAR2(32) DEFAULT '' NOT NULL, 6 "PARAM_OBJECT" VARCHAR2(32) DEFAULT '' NOT NULL, 7 "PARAM_NAME" VARCHAR2(64) DEFAULT '' NOT NULL, 8 "PARAM_VALUE" VARCHAR2(2000) DEFAULT '' NOT NULL, 9 "REMARK" VARCHAR2(2000) DEFAULT '' NOT NULL 10 ) STORAGE (ON "MAIN", CLUSTERBTR); 操作已执行 已用时间: 124.284(毫秒). 执行号:706. SQL> CREATE OR REPLACE INDEX "NQ_MON"."IDX_MONPARAMEXTREM" 2 ON "NQ_MON"."MONPARAMEXTREM" ("SERVER_ID" ASC); 操作已执行 已用时间: 17.282(毫秒). 执行号:707.

查询一下,检验表是否存在

SQL> select server_id, 2 server_type, 3 param_group, 4 param_object, 5 param_name, 6 param_value 7 from NQ_MON.MONPARAMEXTREM 8 where server_id = 'arb1' 9 and server_type = 6 10 and param_group = 'PROCRES'; 未选定行 已用时间: 2.779(毫秒). 执行号:708.

3.3. 写C程序

创建/WorkField/dmoci_test.c,尽可能模仿用户的操作,包括函数命名和调用的逻辑等,并使用宏定义结合#if作为开关,方便地控制变量进行对照实验,查看释放/不释放内存时是否能复现相同问题

3.4. 需要监控:

3.4.1. 需要安装pprof相应工具查看进程内存图

[root@localhost WorkField]# sudo yum install -y gperftools gperftools-devel graphviz
【略】

这要求编译时需要连接profiler等相关库

gcc -o /WorkField/dmoci_test2 /WorkField/dmoci_test2.c -I/WorkField/dmoci/include -I/home/dmdba/dmdbms/include -L/WorkField/dmoci -L/usr/lib64 -ldmoci -lpthread -lprofiler -ltcmalloc -g -O0

运行前需要设置相应的profile变量

# 设置环境变量,指定性能数据输出profile文件 # 设置内存 profile # 先查看当前设置的环境变量,并清理和profile相关的环境变量 env | grep -E "PROFILE|HEAP|CPUPROFILE" unset CPUPROFILE unset CPUPROFILE_FREQUENCY unset HEAPPROFILE unset HEAP_PROFILE_ALLOCATION_INTERVAL unset HEAP_PROFILE_MMAP # 然后设置内存相关的profile变量 export HEAPPROFILE=/tmp/dmoci_heap.prof export HEAP_PROFILE_ALLOCATION_INTERVAL=1048576 # 每 1MB 采样一次 export HEAP_PROFILE_MMAP=1 # 也跟踪 mmap 分配 # 生成内存图 pprof --svg /WorkField/dmoci_test2 /tmp/dmoci_heap.prof.*.heap > heap_customer.svg

3.4.2. 需要监控内存情况

使用sar监控系统内存情况,使用pidstat监控进程内存情况,使用方法如下

# 每5秒采集一次,共采集 720 次(1小时)
sar -r 5 720 > /tmp/sar_memory.log 2>&1 &
# 查看 sar 进程是否在运行
ps aux | grep sar
# 查看最新采集数据
tail -f /tmp/sar_memory.log

# 用 sar 实时查看
sar -r 5

3.4.3. 需要统计会话游标

查询会话游标是否因为无法释放导致会话游标一直上涨

3.4.4. 需要监控dmoci的函数调用过程

这需要设置达梦的dm_svc.conf启用dmoci日志

[root@localhost WorkField]# ls -la /etc/dm_svc.conf -rw-r--r-- 1 dmdba dinstall 30 8月 11 10:02 /etc/dm_svc.conf [root@localhost WorkField]# vim /etc/dm_svc.conf

写入如下内容

OCI_TRACE=(1)

如果没有配置路径,日志dmoci_trace.log会生成在调用数据库的程序所在的目录

这要求编译时需要链接dmoci库,pthread库

gcc -o /WorkField/dmoci_test /WorkField/dmoci_test.c -I/WorkField/dmoci/include -I/home/dmdba/dmdbms/include -L/WorkField/dmoci -ldmoci -lpthread

3.5. 使用一个脚本运行程序并进行监控

该脚本先启动sar,从进程启动前10秒开始监控,然后启动进程,同时使用pidstat监控该进程运行过程中占用的内存情况,并不断轮询检查会话游标情况,等到程序结束后,统计oci日志运行情况、会话游标情况,绘制sar和pidstat全程监控到的内存趋势,绘制pprof内存图。这里为了避免记录oci日志的行为影响内存图,分两次执行,一次不绘制pprof内存图,一次不统计oci日志。

先编译程序:

gcc -o /WorkField/dmoci_test3 /WorkField/dmoci_test3.c \ -I/WorkField/dmoci/include \ -I/home/dmdba/dmdbms/include \ -L/WorkField/dmoci \ -L/usr/lib64 \ -ldmoci -lpthread -lprofiler -ltcmalloc -g -O0

运行时需要指定库,并指定线程数和循环数的参数

[root@localhost WorkField]# LD_LIBRARY_PATH=/WorkField/dmoci:/usr/lib64 /WorkField/dmoci_test3 10 10000 【略】

或者也可以使用监控脚本启动,在脚本里指定了线程数和循环数,为了效果比较明显,在运行时指定了10个线程观察内存情况

./monitor_all.sh

3.5.1. 运行结果

3.5.1.1. 不释放parmdpp描述符的情况

发现如果在调用OCIParamGet之后不及时释放parmdpp描述符,会导致内存泄漏,随着时间推移进程的内存占用缓慢、匀速地上涨

file20260821_161038_2.png

file20260821_161038_1.png

3.5.1.2. 释放parmdpp描述符的情况

如果及时释放parmdpp描述符(对应OCIDescriptorFree((dvoid*)mypard, OCI_DTYPE_PARAM);)则不会导致内存泄漏,进程占用的内存会保持稳定

int HSQLColIsNull(STMT_HANDLE *pStmt, int indexCol) { int type = 0; OCIParam *mypard = NULL; g_oci_param_get++; OCIParamGet(pStmt->stmt, OCI_HTYPE_STMT, pStmt->errhp, (void**)&mypard, indexCol); g_oci_attr_get++; if (OCI_SUCCESS != OCIAttrGet((dvoid*)mypard, (ub4)OCI_DTYPE_PARAM, (dvoid*)&type, (ub4*)0, (ub4)OCI_ATTR_IS_NULL, (OCIError*)pStmt->errhp)) { g_failed++; Err_handler(pStmt->errhp); #if FREE_OCI_PARAM OCIDescriptorFree((dvoid*)mypard, OCI_DTYPE_PARAM); #endif return -1; } g_success++; #if FREE_OCI_PARAM OCIDescriptorFree((dvoid*)mypard, OCI_DTYPE_PARAM); #endif return type; }

file20260821_161038.png

file20260821_161038_3.png

4. 结论

如果在调用OCIParamGet之后不及时释放parmdpp描述符,会导致内存泄漏,导致随着时间推移,进程的内存占用缓慢、匀速地上涨,所以需要及时使用OCIDescriptiorFree()释放parmdpp描述符

5. 附录

5.1. 参考

  1. 《C语言DCI(OCI)方式连接DM数据库》
  2. 《oci的方式获取字段长度》

5.2. 背景知识

5.2.1. pprof数据怎么看

pprof生成的图中通常分为两部分,就像这样:

【OCIParamGet】

【0.0(0.0%)】

【of 142.6(96.7%)】

其中文字部分是函数名;数字部分包含flat和cum,flat是指其本身占用的资源,cum则包括其调用的子函数/子方法占用的资源。并且占用的资源越多,对应的方框也越大

5.2.2. 什么是OCIParamGet

DM8 - DCI Program.pdf手册上是这样说的:

OCIParamGet()和OCIAttrGet()函数是用来获取指定字段描述信息的函数

用法通常是先调用OCIParamGet()获取指定语句上指定位置的描述信息,将其绑定到一个参数句柄上,之后调用OCIAttrGet()获得该参数句柄上描述信息相关的参数值

sword OCIParamGet( const void* hndlp,// 语句句柄,该函数会返回该句柄的参数描述符 ub4 htype,// 句柄类型 OCIError* errhp,// 错误句柄,发送错误时用于获得诊断信息 void** parmdpp,// 是指针,用于接受函数返回结果,记录给定位置的参数描述符 ub4 pos// 语句句柄的位置号,该函数将返回该位置的参数描述符 ) // 返回值是sword类型,成功返回OCI_SUCCESS,失败返回OCI_ERROR // 某种意义上,parmdpp也是该函数的返回值,其作为指针接受返回值

参数描述符由DCI库在内部分配,可以使用OCIDescriptiorFree()释放。

如果每次执行语句时获取相同的列元数据,则会导致内存泄漏,需要每次调用OCIParamGet()时手动释放参数描述符

5.3. 测试用例

直接下载:

  • 链接: https://pan.baidu.com/s/1IxiCJt5Ep61OILyV9qUViA?pwd=qebx 提取码: qebx
  • 链接: https://pan.quark.cn/s/306a4d11b7f2?pwd=6PLE 提取码:6PLE
评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服