Core 文件可以理解为进程异常退出时保存下来的“现场”。日志记录故障前发生了什么,Core 保存故障发生时线程停在哪里,GDB 用来查看调用栈,dmrdc 则可以尝试从达梦 Core 中提取活动 SQL。
本文所有达梦实验都只适用于测试环境。
kill -11 <dmserver_pid>会中断数据库服务,不要在生产库随意执行。
Core 能否生成,主要看三个条件:当前用户是否允许生成 Core、Core 文件写到哪里、目标目录是否有权限和空间。
ulimit -c
cat /proc/sys/kernel/core_pattern
本次环境中,ulimit -c 为 unlimited,Core 文件路径规则为:
/dmbak/dmcore/core-%e-%t-%p-%u.core
其中 %e 是进程名,%t 是时间戳,%p 是 PID,%u 是用户 UID。因此后面生成的 Core 不会在当前目录,而是在 /dmbak/dmcore 下。
先用一个普通 C 程序制造空指针异常:
#include <stdio.h>
void level3(void)
{
int *pointer = NULL;
*pointer = 100;
}
void level2(void)
{
level3();
}
void level1(void)
{
level2();
}
int main(void)
{
printf("program will crash\n");
level1();
return 0;
}
编译时加上 -g,这样 GDB 才能显示函数名和源码行号:
gcc -g -O0 null_test.c -o null_test
./null_test
程序输出 program will crash 后出现“段错误(核心已转储)”,说明程序访问非法内存并生成了 Core。根据 core_pattern,实际文件为:
/dmbak/dmcore/core-null_test-1784399982-20585-1001.core
用 GDB 打开时,要传入程序文件和 Core 文件的绝对路径:
gdb ./null_test /dmbak/dmcore/core-null_test-1784399982-20585-1001.core
GDB 里最关键的是这几行:
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x000000000040114a in level3 () at null_test.c:6
6 *pointer = 100;
SIGSEGV 表示非法内存访问,null_test.c:6 直接定位到 *pointer = 100;。继续执行 bt,可以看到完整调用链:
#0 level3 () at null_test.c:6
#1 level2 () at null_test.c:11
#2 level1 () at null_test.c:16
#3 main () at null_test.c:22
调用栈从下往上读,就是 main -> level1 -> level2 -> level3;从上往下看,则能看到程序最终在哪里崩溃。
第二个实验观察另一种退出方式:程序自己主动中止。
#include <stdlib.h>
void stop_program(void)
{
abort();
}
int main(void)
{
stop_program();
return 0;
}
编译运行后,用对应 Core 打开:
gcc -g -O0 abort_test.c -o abort_test
./abort_test
gdb ./abort_test /dmbak/dmcore/core-abort_test-1784402270-21600-1001.core
这次 GDB 显示的是:
Program terminated with signal SIGABRT, Aborted.
#0 raise () from /usr/lib64/libc.so.6
#1 abort () from /usr/lib64/libc.so.6
#2 stop_program () at abort_test.c:5
#3 main () at abort_test.c:10
SIGABRT 和 SIGSEGV 不同。前者通常表示程序主动中止,后者更多是非法内存访问。这里调用链是 main -> stop_program -> abort -> raise,说明 abort() 内部通过 raise() 触发了 SIGABRT。
这个实验对分析数据库 Core 很有帮助:如果堆栈中看到 abort、raise 或类似 halt 的函数,不应简单理解成“这里崩了”,而要回到数据库日志里找主动中止前的错误原因。halt 可以理解为程序认为继续运行不安全时主动停下来。
前两个实验解决的是“怎么看信号和调用栈”。接下来回到达梦数据库本身,目标是练习一条完整链路:
构造活动 SQL
-> kill dmserver 生成 Core
-> GDB 导出全线程堆栈
-> dmrdc 提取 SQL
-> 用 LWP 线程号关联 SQL 和堆栈
这里的重点不是证明某条 SQL 会导致达梦崩溃。Core 的直接原因是人为执行 kill -11,SQL 只是为了让 Core 现场里有可观察的活动会话。
单纯执行大 SQL 不一定够慢。本次环境里,千万级插入几十毫秒就结束了,所以改用锁等待来稳定保留活动 SQL。
会话 A 持有行锁,不提交:
DROP TABLE IF EXISTS T_CORE_LOCK;
CREATE TABLE T_CORE_LOCK(ID INT PRIMARY KEY, C1 INT);
INSERT INTO T_CORE_LOCK VALUES(1, 100);
COMMIT;
UPDATE T_CORE_LOCK SET C1 = C1 WHERE ID = 1;
会话 B 执行同一行的更新:
UPDATE T_CORE_LOCK SET C1 = C1 + 1 WHERE ID = 1;
会话 B 会一直等待会话 A 释放锁。此时另开终端查询 dmserver:
ps -ef | grep dmserver | grep -v grep
本次查到的进程号是 22976:
dmdba 22976 1 0 04:04 ? 00:00:04 /dmdbms/bin/dmserver path=/dmdata/DAMENG/dm.ini -noconsole
确认是测试环境后,执行:
kill -11 22976
ll /dmbak/dmcore/
目录中生成了新的服务端 Core:
core-dmserver-1784405790-22976-1001.core
gdb /dmdbms/bin/dmserver /dmbak/dmcore/core-dmserver-1784405790-22976-1001.core
进入 GDB 后导出线程:
set pagination off
set logging file /dmbak/dmcore/core_22976_stack.txt
set logging on
info threads
thread apply all bt
set logging off
quit
图中能看到大量 LWP 线程。很多线程停在 pthread_cond_wait、pthread_cond_timedwait,这在数据库服务端里很常见,通常表示线程正在等待任务或事件。
本次更值得关注的是后面和 SQL 对应的线程。堆栈文件中可以看到:
Thread 106 (LWP 23555):
#0 pthread_cond_timedwait ()
#1 os_event2_wait_timeout_low ()
#2 trx4_waiting_timeout ()
#3 trx4_waiting_interval ()
#4 trx4_waiting ()
#5 nupd2_exec_clu_update_check_rec_visible ()
#9 nupd2_exec_update ()
#17 uthr_db_main_for_sess ()
trx4_waiting 表示事务等待,nupd2_exec_update 表示正在执行更新。这和我们构造的锁等待现场对应上了。
cd /dmdbms/bin
./dmrdc sfile=/dmbak/dmcore/core-dmserver-1784405790-22976-1001.core
Analysing: 当前偏移/文件总大小 是扫描进度,不是报错。
未指定 dfile 时,dmrdc 在同目录生成了默认输出文件:
cat /dmbak/dmcore/core-dmserver-1784405790-22976-1001_tmp.core
输出中有两条 SQL:
!#%&*^$@[23555]:UPDATE T_CORE_LOCK SET C1 = C1 + 1 WHERE ID = 1;
!#%&*^$@[23384]:UPDATE T_CORE_LOCK SET C1 = C1 WHERE ID = 1;
方括号里的数字就是线程号。23555 对应会话 B 中被锁阻塞的更新,23384 对应会话 A 中持锁未提交的更新。再回到 GDB 堆栈里找 LWP 23555,就能把“SQL 文本”和“线程状态”关联起来。
这一步是整个实验最有价值的地方:dmrdc 告诉我们现场有哪些 SQL,GDB 告诉我们这些线程当时停在哪里。两者结合,才能把 Core 从一堆地址和线程变成能理解的故障现场。
真实遇到达梦实例异常退出时,建议至少保留这些材料:
dmserver;dmserver 运行日志、dmsql 日志;/var/log/messages、dmesg;dmrdc 输出文件;Core 分析不能只看一个点。SIGSEGV、SIGABRT、线程堆栈、SQL 文本都只是线索,最终还要和日志、业务时间线、复现结果一起判断。
几个材料和工具的定位可以这样区分:
| 材料或工具 | 主要回答的问题 | 注意点 |
|---|---|---|
| 数据库日志 | 故障前发生了什么 | 先按故障时间过滤,再看错误号、线程号和业务操作 |
| Core 文件 | 进程异常时停在哪里 | 需要和崩溃时同版本的 dmserver 一起分析 |
| GDB | 每个线程的调用栈是什么 | 重点导出 bt、info threads、thread apply all bt |
dmrdc |
Core 中能否提取到活动 SQL | SQL 是现场线索,不等于最终根因 |
通过这三个实验,可以形成一个比较清晰的认识:
dmrdc 可以从达梦 Core 中提取活动 SQL;kill -11 生成的 Core,适合学习分析流程,不适合直接推导数据库缺陷。数据库日志告诉我们故障前发生了什么,Core 告诉我们故障时停在哪里。把日志、GDB 堆栈和 dmrdc 输出放在一起看,才是达梦 Core 分析比较稳妥的方式。
文章
阅读量
获赞
