客户在使用DMOCI编程时遇到了内存泄漏的问题,需要辅助排查。客户给出了pprof监控数据生成的内存图。需要排查是达梦数据库/DMOCI的问题还是因为在调用DMOCI时存在未释放的内存
| 内容 | 版本/类型 |
|---|---|
| 操作系统 | Kylin-Server-V10-SP3-2403 |
| CPU | x86_64bit |
| 达梦8数据库 | 8.1.4.80p32 |
| DMOCI驱动 | 8.1.5.60 |
发现有许多函数都调用了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过程中分配了内存但未释放,导致内存占用较多
以此作为前提尝试复现
先创建用户
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.
创建/WorkField/dmoci_test.c,尽可能模仿用户的操作,包括函数命名和调用的逻辑等,并使用宏定义结合#if作为开关,方便地控制变量进行对照实验,查看释放/不释放内存时是否能复现相同问题
[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
使用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
查询会话游标是否因为无法释放导致会话游标一直上涨
这需要设置达梦的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
该脚本先启动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
发现如果在调用OCIParamGet之后不及时释放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;
}
如果在调用OCIParamGet之后不及时释放parmdpp描述符,会导致内存泄漏,导致随着时间推移,进程的内存占用缓慢、匀速地上涨,所以需要及时使用OCIDescriptiorFree()释放parmdpp描述符
pprof生成的图中通常分为两部分,就像这样:
【OCIParamGet】
【0.0(0.0%)】
【of 142.6(96.7%)】
其中文字部分是函数名;数字部分包含flat和cum,flat是指其本身占用的资源,cum则包括其调用的子函数/子方法占用的资源。并且占用的资源越多,对应的方框也越大
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()时手动释放参数描述符
直接下载:
文章
阅读量
获赞
