Debugging OceanBase: GDB, Debug-Info Packages, Logging, SQL Trace and Debug Sync
【免费下载链接】oceanbaseOceanBase is the unified distributed database for the AI era — open-source, multi-model, one engine for your most demanding workloads.项目地址: https://gitcode.com/GitHub_Trending/oc/oceanbase
OceanBase 是一个 C++ 实现的高性能分布式数据库,进程内部线程众多、状态机复杂,调试方式也与普通单机程序有显著差异。本文基于 docs/docs/en/debug.md 整理出完整的调试方法论:从 GDB 附加进程与 debug-info 符号包的使用,到日志埋点与检索、SQL 全链路 Trace,再到专为分布式场景设计的 Debug Sync 同步点机制。读完本文,你将掌握针对 observer 进程从"定位线程"到"在代码任意位置挂起并观察"的完整实战技能,并了解每类手段的适用场景与限制。
调试前的准备:优先使用 Debug 模式构建
官方文档强烈建议以 debug 模式构建 OceanBase。Debug 模式保留了完整符号与断言信息,gdb 附加后可以直接看到函数名、参数与源码行号;而 Release 模式(RelWithDebInfo)构建产物通常只保留行号级别的信息,需要额外配合 debug-info 包才能获得良好调试体验。
无论采用哪种方式,都请先确认你已经基于源码构建并部署了 observer(进程名为observer),后续所有调试手段都围绕该进程展开。
GDB:附加到运行中的 observer 进程
GDB 功能强大,但官方文档明确指出:GDB 调试 OceanBase 难度较大且场景受限。原因是 GDB 会挂起整个进程,而 OceanBase 依赖心跳(heartbeat)与各副本协同才能正常工作,进程被长时间挂起会引发选主、超时等一系列连锁反应。因此 GDB 仅推荐用于"单个 observer 进程、单个线程"的低风险调试,多线程/多副本场景更推荐日志手段。
查找进程 ID
ps -ef | grep observer或:
pidof observer附加进程
gdb observer <pid>附加成功后可正常设置断点、打印变量。GDB 本身的使用方式与调试其他 C++ 程序一致,不再赘述。
使用 debug-info 包调试 RPM 部署的 OceanBase
如果你的 observer 是通过 RPM 包部署的(例如生产环境),二进制中不包含调试符号,直接 gdb 附加后无法获得源码与参数信息。此时需要先获取并加载对应的 debug-info 包。
官方建议优先采用"加载(load)"而非"安装(install)"debug-info 包:系统里可能同时存在多个版本的 debug-info 包,安装后难以清理。
第一步:获取版本 revision
在 observer 运行目录下执行:
clusters/local/bin [83] $ ./observer -V ./observer -V observer (OceanBase_CE 4.1.0.1) REVISION: 102000042023061314-43bca414d5065272a730c92a645c3e25768c1d05 BUILD_BRANCH: HEAD BUILD_TIME: Jun 13 2023 14:26:23 BUILD_FLAGS: RelWithDebInfo BUILD_INFO: Copyright (c) 2011-2022 OceanBase Inc.如果直接执行报动态库缺失错误:
./observer: error while loading shared libraries: libmariadb.so.3: cannot open shared object file: No such file or directory说明需要手动指定依赖库路径,改用如下命令:
LD_LIBRARY_PATH=../lib:$LD_LIBRARY_PATH ./observer -V输出中REVISION的第一段102000042023061314就是后续在 rpm 站点搜索 debug-info 包的关键标识。
第二步:下载 debug-info 包
在官方社区版 rpm 镜像站点上,按照发行版与 CPU 架构选择对应目录(分别提供 el7/el8 的 x86_64 与 aarch64 架构目录),用 revision 值搜索包名形如oceanbase-ce-debuginfo-4.1.0.1-102000042023061314.<distro>.<arch>.rpm的安装包:
第三步:从 rpm 中解压 debug-info 文件
rpm2cpio oceanbase-ce-debuginfo-4.1.0.1-102000042023061314.el7.x86_64.rpm | cpio -div解压后得到如下目录结构:
~/tmp/debug-info [83] $ tree -a . └── usr └── lib └── debug ├── .build-id │ └── ee │ ├── f87ee72d228069aab083d8e6d2fa2fcb5c03f2 -> ../../../../../home/admin/oceanbase/bin/observer │ └── f87ee72d228069aab083d8e6d2fa2fcb5c03f2.debug -> ../../home/admin/oceanbase/bin/observer.debug └── home └── admin └── oceanbase └── bin └── observer.debug其中usr/lib/debug/home/admin/oceanbase/bin/observer.debug就是我们需要的符号文件,.build-id目录下则是用于 gdb 自动查找符号的 build-id 软链接。
第四步:附加进程或打开 core 文件
# 附加进程 gdb ./observer `pidof observer`或:
# 打开 coredump 文件 gdb ./observer <coredump file name>未加载符号时,gdb 会提示(No debugging symbols found ...),此时执行bt只能看到地址与函数名、拿不到源码行号和参数:
(gdb) bt #0 0x00007fb6e9c36d62 in pthread_cond_timedwait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0 #1 0x00007fb6f9f44862 in ob_pthread_cond_timedwait () #2 0x00007fb6eee8d206 in oceanbase::common::ObThreadCond::wait_us(unsigned long) () #3 0x00007fb6f34b21c8 in oceanbase::observer::ObUniqTaskQueue<oceanbase::observer::ObServerSchemaTask, oceanbase::observer::ObServerSchemaUpdater>::run1() () #4 0x00007fb6f9f44259 in oceanbase::lib::Threads::run(long) () #5 0x00007fb6f9f40aca in oceanbase::lib::Thread::__th_start(void*) ()第五步:加载 debug-info 符号并重新调试
在 gdb 内加载符号文件:
(gdb) symbol-file usr/lib/debug/home/admin/oceanbase/bin/observer.debug Reading symbols from usr/lib/debug/home/admin/oceanbase/bin/observer.debug...建议使用 debug-info 文件的完整路径,避免相对路径导致加载失败。
再次执行bt,即可看到完整的源码位置、函数参数和模板实例化信息:
(gdb) bt #0 0x00007fb6e9c36d62 in pthread_cond_timedwait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0 #1 0x00007fb6f9f44862 in ob_pthread_cond_timedwait (__cond=0x7fb6fb1d5340, __mutex=0x7fb6fb1d5318, __abstime=0x7fb6b3ed41d0) at deps/oblib/src/lib/thread/ob_tenant_hook.cpp:124 #2 0x00007fb6eee8d206 in oceanbase::common::ObThreadCond::wait_us (this=<optimized out>, time_us=140422679606016) at deps/oblib/src/lib/lock/ob_thread_cond.cpp:106 #3 0x00007fb6f34b21c8 in oceanbase::common::ObThreadCond::wait (this=0x7fb6fb1d5310, time_ms=200) at deps/oblib/src/lib/lock/ob_thread_cond.h:69 #4 oceanbase::observer::ObUniqTaskQueue<oceanbase::observer::ObServerSchemaTask, oceanbase::observer::ObServerSchemaUpdater>::run1 ( this=<optimized out>) at src/observer/ob_uniq_task_queue.h:417从堆栈中可以清晰地看到:ObThreadCond::wait等待 200ms 的调用发生在 ob_uniq_task_queue.h,这正是ObUniqTaskQueue线程池run1()中ObServerSchemaUpdater处理 schema 任务的阻塞点。符号加载让排查从"猜测"变成了"定位"。
Logging:最常用、覆盖面最广的调试手段
官方文档明确推荐:Logging 是调试 OceanBase 最常用的方式,易用且适用于绝大多数场景。常规做法是:在代码中埋点打印变量 → 重新构建并部署 → 在日志中观察输出。
如何在代码中添加日志
在源码中直接使用日志宏,例如:
LOG_DEBUG("insert sql generated", K(insert_sql));LOG_DEBUG是打印 DEBUG 级别日志的宏。与printf/fprintf风格不同,OceanBase 日志宏的第一个参数是描述信息字符串,后续参数通常是K(变量名)。K宏会自动展开为"变量名", 变量值的键值对,例如K(insert_sql)展开为"insert_sql", insert_sql,最终在日志中输出形如insert_sql=...的内容,避免手写格式串出错。
日志宏与级别速查
结合 docs/docs/en/logging.md,日志级别与对应宏如下:
| 级别 | 宏 | 说明 |
|---|---|---|
| DEBUG | LOG_DEBUG | 开发调试日志 |
| TRACE | LOG_TRACE | 链路追踪日志 |
| INFO | LOG_INFO | 系统状态变更日志 |
| WARN | LOG_DBA_WARN | 面向 DBA:服务可用但行为不符合预期 |
| ERROR | LOG_DBA_ERROR | 面向 DBA:服务不可用,需 DBA 介入恢复 |
| WDIAG | LOG_WARN | 告警诊断:辅助故障排查、预期内失败 |
| EDIAG | LOG_ERROR | 错误诊断:非预期逻辑错误,通常是程序缺陷 |
除K外还常用以下参数宏(定义见deps/oblib/src/lib/utility/ob_log_module.h):
| 宏 | 示例 | 说明 |
|---|---|---|
K_ | K_(consistency_level) | 打印成员变量,自动在变量名后补_ |
KR | KR(ret) | 同时打印错误码与错误码名称 |
KP | KP(plan) | 以十六进制打印指针值 |
KPC | KPC(session) | 指针为空输出 NULL,否则调用其to_string |
KTIME | KTIME(cur_time) | 微秒时间戳转字符串 |
KPHEX | KPHEX(buf, 20) | 以十六进制打印缓冲区内容 |
KERRMSG | KERRMSG | 输出系统错误码信息 |
如何检索日志
日志存放在home_path目录下的 log 子目录中(即 observer 安装路径下的 log 目录),可用grep检索。典型的一行日志如下:
[2023-07-05 16:40:42.635136] INFO [SQL.EXE] explicit_start_trans (ob_sql_trans_control.cpp:194) [88022][T1003_ArbSer][T1003][YD9F97F000001-0005FFB71FCF95C7-0-0] [lt=42] start_trans(ret=0, tx_id={txid:2118151}, session={this:0x7ff2663d6188, id:1, tenant:"sys", tenant_id:1, effective_tenant:"sys", effective_tenant_id:1003, database:"oceanbase", user:"root@%", consistency_level:3, session_state:0, autocommit:true, tx:0x7ff26b0e4300}, read_only=false, ctx.get_execution_id()=18446744073709551615)日志字段从左到右依次为:
| 字段 | 示例 | 含义 |
|---|---|---|
| 时间戳 | [2023-07-05 16:40:42.635136] | 日志打印时间(微秒精度) |
| 日志级别 | INFO | 日志级别 |
| 模块名 | [SQL.EXE] | 所属模块(主模块.子模块) |
| 函数名 | explicit_start_trans | 打印日志的函数 |
| 代码位置 | (ob_sql_trans_control.cpp:194) | 文件名与行号 |
| 线程标识 | [88022][T1003_ArbSer] | 线程 ID 与线程名 |
| 租户 ID | [T1003] | 租户 ID |
| Trace ID | [YD9F97F000001-0005FFB71FCF95C7-0-0] | 单条 SQL 请求的全局唯一 ID |
| 打印开销 | [lt=42] | 上一条日志打印耗时(微秒) |
其中Trace ID 是每条 SQL 请求的唯一标识,按 Trace ID 检索即可拿到某条 SQL 从进入到返回的全链路日志,是排查单请求问题的利器。
日志调试实用技巧
获取上一条 SQL 的 Trace ID
select last_trace_id();动态调整日志级别
set ob_log_level=debug;放开日志流量控制
如果找不到自己的日志,很可能是被日志流量控制(限流)丢弃了,可执行:
alter system set syslog_io_bandwidth_limit='1G'; alter system set diag_syslog_per_error_limit=1000;其中syslog_io_bandwidth_limit控制日志磁盘 IO 带宽上限(默认 "30MB",限流日志含REACH SYSLOG RATE LIMIT关键字),diag_syslog_per_error_limit控制每种错误码每秒 WDIAG 日志条数上限(默认 200,超限日志含Throttled WDIAG logs in last second关键字),实现细节可参考ObSyslogPerErrLimiter::do_acquire。
同步打印日志
异步日志(默认开启)可能导致日志延迟落盘,调试时可强制同步:
alter system set enable_async_syslog='False';在日志中打印调用栈
在日志宏中追加K(lbt())即可输出当前调用栈地址序列:
LOG_DEBUG("insert sql generated", K(insert_sql), K(lbt()));输出形如:
lbt()="0x14371609 0xe4ce783 0x54fd9b6 0x54ebb1b 0x905e62e 0x92a4dc8 0x905df11 0x905dc94 0x13d2278e 0x13d22be3 0x6b10b81 0x6b0f0f7 0x62e2491 0x10ff6409 0x1475f87a 0x10ff6428 0x1475f1c2 0x1476ba83 0x14767fb5 0x14767ae8 0x7ff340250e25 0x7ff33fd0ff1d"lbt()是 OceanBase 在 ob_backtrace.cpp 中提供的取栈函数(内部通过ob_backtrace采集当前栈地址)。拿到地址后可用addr2line解析为符号:
addr2line -pCfe ./bin/observer 0x14371609 0xe4ce783 0x54fd9b6 0x54ebb1b 0x905e62e 0x92a4dc8 0x905df11 0x905dc94 0x13d2278e 0x13d22be3 0x6b10b81 0x6b0f0f7 0x62e2491 0x10ff6409 0x1475f87a 0x10ff6428 0x1475f1c2 0x1476ba83 0x14767fb5 0x14767ae8 0x7ff340250e25 0x7ff33fd0ff1d解析结果示例:
oceanbase::common::lbt() at /home/distcc/tmp/./deps/oblib/src/lib/utility/ob_backtrace.cpp:130 (discriminator 2) operator() at /home/distcc/tmp/./src/sql/session/ob_basic_session_info.cpp:599 (discriminator 2) oceanbase::sql::ObBasicSessionInfo::switch_tenant(unsigned long) at /home/distcc/tmp/./src/sql/session/ob_basic_session_info.cpp:604 oceanbase::observer::ObInnerSQLConnection::switch_tenant(unsigned long) at /home/distcc/tmp/./src/observer/ob_inner_sql_connection.cpp:1813 (discriminator 2) ... oceanbase::lib::Thread::run() at /home/distcc/tmp/./deps/oblib/src/lib/thread/thread.cpp:162 oceanbase::lib::Thread::__th_start(void*) at /home/distcc/tmp/./deps/oblib/src/lib/thread/thread.cpp:312 ?? ??:0这样就能在不打断进程的前提下,把热点路径的调用链完整还原出来。
SQL Trace:一键诊断慢查询的执行阶段耗时
对慢 SQL,无需改代码即可通过 SQL Trace 查看每个执行阶段的耗时分布。
首先开启 trace 开关(4.x 版本):
set ob_enable_show_trace=1;然后执行待诊断的 SQL:
select * from t, t1 where t.id=t1.id;执行完毕后用show trace查看:
obclient> show trace; +-------------------------------------------+----------------------------+------------+ | Operation | StartTime | ElapseTime | +-------------------------------------------+----------------------------+------------+ | com_query_process | 2023-07-06 15:30:49.907532 | 9.547 ms | | └── mpquery_single_stmt | 2023-07-06 15:30:49.907552 | 9.506 ms | | ├── sql_compile | 2023-07-06 15:30:49.907615 | 6.605 ms | | │ ├── pc_get_plan | 2023-07-06 15:30:49.907658 | 0.024 ms | | │ └── hard_parse | 2023-07-06 15:30:49.907763 | 6.421 ms | | │ ├── parse | 2023-07-06 15:30:49.907773 | 0.119 ms | | │ ├── resolve | 2023-07-06 15:30:49.907952 | 0.780 ms | | │ ├── rewrite | 2023-07-06 15:30:49.908857 | 1.320 ms | | │ ├── optimize | 2023-07-06 15:30:49.910209 | 3.002 ms | | │ ├── code_generate | 2023-07-06 15:30:49.913243 | 0.459 ms | | │ └── pc_add_plan | 2023-07-06 15:30:49.914016 | 0.140 ms | | └── sql_execute | 2023-07-06 15:30:49.914239 | 2.675 ms | | ├── open | 2023-07-06 15:30:49.914246 | 0.217 ms | | ├── response_result | 2023-07-06 15:30:49.914496 | 1.956 ms | | │ └── do_local_das_task | 2023-07-06 15:30:49.914584 | 0.862 ms | | └── close | 2023-07-06 15:30:49.916474 | 0.415 ms | | ├── close_das_task | 2023-07-06 15:30:49.916486 | 0.037 ms | | └── end_transaction | 2023-07-06 15:30:49.916796 | 0.064 ms | +-------------------------------------------+----------------------------+------------+ 18 rows in set (0.01 sec)如上例所示,optimize阶段耗时 3ms 是编译期的明显瓶颈(占比接近一半),而hard_parse(6.421ms)说明未命中计划缓存,可据此针对性优化(如开启 plan cache 或使用绑定变量)。Trace 结果以树形结构呈现 SQL 从解析、优化到执行、提交的完整时间线,无需阅读源码即可快速定位慢在哪个环节。
Debug Sync:在代码任意位置安全地挂起线程
为什么需要 Debug Sync
gdb 附加会挂起整个进程,而 OceanBase 依赖心跳机制维持集群正常运转,进程长时间挂起会导致租户无主、选举超时。为此 OceanBase 提供了Debug Sync 同步点机制:在代码中埋入同步点后,只有命中该点的特定线程会挂起等待,进程其余部分照常运行,此时你可以安全地用 gdb 附加进程,或执行 SQL 获取现场信息,调试完成后发送信号放行该线程。
Debug Sync 在 Release 模式同样生效,因此也可用于生产环境。
第一步:在代码中定义调试同步点
打开 ob_debug_sync_point.h,在宏OB_DEBUG_SYNC_POINT_DEF中追加你的同步点定义:
#define OB_DEBUG_SYNC_POINT_DEF(ACT) \ ACT(INVALID_DEBUG_SYNC_POINT, = 0) \ ACT(NOW,) \ ACT(MAJOR_FREEZE_BEFORE_SYS_COORDINATE_COMMIT,) \ ACT(BEFORE_REBALANCE_TASK_EXECUTE,) \ ACT(REBALANCE_TASK_MGR_BEFORE_EXECUTE_OVER,) \ ACT(UNIT_BALANCE_BEFORE_PARTITION_BALANCE,) \ ACT(BEFORE_UNIT_MANAGER_LOAD,) \ ...该宏通过DECLARE_ENUM/DEFINE_ENUM_FUNC自动生成ObDebugSyncPoint枚举及名称映射(见 ob_debug_sync_point.cpp),当前仓库已内置数百个覆盖合并、迁移、备份恢复、DDL、负载均衡等流程的同步点,可直接复用。
第二步:在目标函数中埋入同步点
在需要调试的函数中调用DEBUG_SYNC(...)宏,例如:
int ObRootService::do_restart() { int ret = OB_SUCCESS; const int64_t tenant_id = OB_SYS_TENANT_ID; SpinWLockGuard rs_list_guard(broadcast_rs_list_lock_); ... DEBUG_SYNC(BEFORE_UNIT_MANAGER_LOAD); ... }同一同步点可放置在任意多个位置。仓库中大量业务代码已埋点,例如 ob_archive_checkpoint.cpp 中的DEBUG_SYNC(BEFORE_UPDATE_PIECE_TO_ACTIVE)、ob_kv_storecache.cpp 中的DEBUG_SYNC(BEFORE_BACKGROUND_WASH),可作为埋点范式参考。
第三步:开启 Debug Sync 总开关
Debug Sync 默认关闭,通过debug_sync_timeout配置项开启(该值为 0 时关闭,大于 0 时启用):
alter system set debug_sync_timeout='100000s';注意:
debug_sync_timeout的单位是微秒(microsecond)。
第四步:激活指定的同步点
用会话级变量ob_global_debug_sync激活目标同步点:
set ob_global_debug_sync = 'BEFORE_UNIT_MANAGER_LOAD wait_for signal_name execute 10000';语法说明:
wait_for signal_name:命中该同步点的线程将等待名为signal_name的信号;execute 10000:该动作最多执行 10000 次后自动失效(execute控制生效次数)。
此后,当目标线程执行到该同步点时便会挂起等待,此时即可用 gdb 附加进程,或执行 SQL 查询现场状态。
第五步:发送信号放行线程
set ob_global_debug_sync = 'now signal signal_name'; -- 或 set ob_global_debug_sync = 'now broadcast signal_name';signal唤醒单个等待线程,broadcast唤醒所有等待该信号的线程。收到信号后,挂起的线程继续执行。
第六步:清理并关闭
调试结束后,务必清理同步点并关闭总开关:
-- 清理指定同步点 set ob_global_debug_sync = 'BEFORE_UNIT_MANAGER_LOAD clear'; -- 关闭 Debug Sync 总开关 alter system set debug_sync_timeout=0;Debug Sync 的底层原理
从源码结构看(ob_debug_sync.h、ob_debug_sync.cpp),Debug Sync 由三部分组成:
ObDebugSyncAction:描述一个同步点动作,包含sync_point_(同步点枚举)、timeout_(等待超时)、execute_(剩余生效次数)、signal_/broadcast_/wait_(事件名)等字段,并实现了is_valid()校验与序列化;ObDSActionArray/ObDSSessionActions:动作的存储容器,fetch_action每次命中后execute_自减,归零即自动清除该动作(这正是execute 10000生效次数限制的实现);ObDSEventControl:基于condition_variable(ObThreadCond)实现的事件控制,维护signal_cnt_/waiter_cnt_计数,提供signal/broadcast/wait原语——线程执行到同步点时在此等待,收到信号后继续执行。
也就是说,整个机制可以概括为:DEBUG_SYNC宏在埋点处通过条件变量等待事件,ob_global_debug_sync会话变量负责注册动作与发送信号,debug_sync_timeout控制开关。理解这三层关系后,你完全可以为任意新流程设计自己的同步点,实现"定点挂起、按需放行"的精细化调试。
小结:如何选择调试手段
| 手段 | 适用场景 | 限制 |
|---|---|---|
| GDB | 单进程单线程问题、core 文件分析 | 挂起整个进程,受心跳机制约束 |
| debug-info 包 | RPM 部署场景的符号还原 | 需按 revision 匹配符号包 |
| Logging | 绝大多数场景,多线程/多副本问题 | 需要重新编译部署后观察 |
| SQL Trace | 慢查询、执行阶段耗时分析 | 仅覆盖 SQL 执行路径 |
| Debug Sync | 定点挂起特定线程,配合 gdb/SQL 观察 | 需改代码定义同步点并重新编译 |
推荐的组合拳是:先用 SQL Trace 与日志缩小问题范围,再用 Debug Sync 精确定位到线程挂起点,最后用 GDB(配合 debug-info 包)做符号级分析。这套方法论同样适用于基于源码自行构建的调试场景,相关同步点定义与埋点示例可直接在 src/share/ob_debug_sync_point.h 与src/share/ob_debug_sync.cpp中查阅。
【免费下载链接】oceanbaseOceanBase is the unified distributed database for the AI era — open-source, multi-model, one engine for your most demanding workloads.项目地址: https://gitcode.com/GitHub_Trending/oc/oceanbase
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考