昨天Oceanbase V4.3 发布会,因工作原因未能亲临现场学习,颇为遗憾。近年来,国产数据库快速迭代,无论是功能引入还是问题修复,Oceanbase 的版本更新速度与客户发展态势都值得肯定。尤其在信创走得最快的金融领域,OB 正沿着从分布式到集中式、行列混合存储引擎、混合负载支持、Oracle/MySQL 多模兼容,再到今年推出的向量功能,逐步朝着“All in One(OB)”的方向演进——这似乎也与 Oracle 从集中式走向分布式的技术路线不谋而合,只是起点恰好相反。
然而,就在 OB 发布会同一天,我们另一批同事却因新上线的 Oceanbase 集群突发故障而焦头烂额:一个租户内存耗尽,进而引发活动会话激增,最终导致 CPU 满载并触发重启。目前根因尚未定位,应对方式仍停留在“重启大法”和靠人工紧盯的层面。这暴露出我们当前面临的几重困境:首先,现场支持能力有限,数据库诊断手段不足,加之各方——无论是应用、数据库团队还是部门领导——似乎都不愿主动承担责任,导致根因分析缺乏牵头推进;其次,公开的深入分析文档极为匮乏,全网仅有一份官方在线文档中寥寥数语的描述,真正解决问题仍需研发介入,周期漫长;再者,若如该文档所言,此问题是数据库设计缺陷所致,且在新版本中已修复,那也反映出在版本快速迭代、尚未完全成熟的阶段,生产环境版本选择是否合理——上线周期长达两年,继续沿用旧版本是否还具备可持续性?
“如果只允许一种声音存在,那么,唯一存在的那个声音就是谎言。” –柏拉图
环境 oceanbase for oracle V 3.2.4 BP5
现象Oceabase集群中的某一个节点的CPU使用率高告警,事务响应时间变长,部分应用提示ORA-4013(No memory or reach tenant memory limit)、Transaction rolled back错误, 查询该节点的内存使用率DTL占用高达300G。

CPU和ACTIVE session暴增,应急先尝试SQL 限流

SQL限流失败,提示ORA-00600: internal error code, arguments: -4013, No memory or reach tenant memory limit, 查看内存使用率。

发现当前一般租户使用最高的是SQL_EXEC_CTX_ID, label和mod_name为DtlIntermRes, hold内存区近340GB。
SELECT tenant_id,svr_ip,mod_name,sum(hold/1024/1024/1024) module_sum_gb
FROM __all_virtual_memory_info
WHERE tenant_id >1000 and tenant_id <2000 AND hold <> 0
and mod_name='DtlIntermRes' AND
mod_name not in('OB_KVSTORE_CACHE_MB','OB_MEMSTORE','OB_KVSTORE_CACHE')
GROUP BY tenant_id,svr_ip,mod_name
ORDER BY module_sum_gb desc;
内存相关内部表
| 视图 | 描述 | |
|---|---|---|
| __all_virtual_tenant_ctx_memory_info | context 级别的统计信息(context 是面向开发者的概念),它是 2M 粒度的统计。 | |
| __all_virtual_memory_info | OBServer 内存标签的统计信息。 | |
| __all_virtual_tenant_memstore_info | memstore 统计信息。 | |
| __all_virtual_kvcache_info | kvcache 统计信息。 | |
分析日志关键字
而分布式数据库分析最麻烦的就是分析日志,一个集群一个小时900G的日志,如何识别关键字才能减少分析工作量,接下来查看observer.log中关键字“ret=-4013”.
像案例1日志中的模块是“ERROR [SQL.REWRITE]”阶段的ob_query_range.cpp ,文档说是SqlExecutor 或 CostBasedRewrite模块(可以通过__all_virtual_memory_info.mod_name确认),解决是修改了配置项 range_optimizer_max_mem_size 来限制 range 抽取时的内存使用,或增加 NO_REWRITE Hint 禁止 SQL 重写来进行规避。
而案例2是一段memleak的信息因软件bug. 不过除了重启应急外,看不明白“hint 走主表”是什么意思 。不过一般OB会定期在日志中记录内存状态,可以 egrep “^[MMORY]” observer.log关键字过滤或 egrep “^[MMORY]%SQL_EXEC_CTX_ID” observer.log, 再根据事务号“tid”值,为关键字继续查找对应的trace_id ,再继续查看SQL 文本。 其它一些日志关键字如 fail to alloc memory 或 allocate memory fail . 也可以通过关键词 OOPS 确定内存爆的类型。
而本案例的DtlIntermRes 的SQL_EXEC_CTX_ID占用这么大的内存,势必会挤占MEMSTORE的内存区, 《Oceanbase Memstore 使用分析》记录过统计memstore的方法, 而memstore减少,为了防止出现ERROR 4030 (HY000): OB-4030:Over tenant memory limitsmemstore内存超限,不确认是不是会触发memstore限流,如参数writing_throttling_trigger_percentage、writing_throttling_maximum_duration,增加 Query 的运行时间(response time),从而降低外部的写入速度,而影响cpu使用率。 这些需要在问题发生时确认,当然也可以当时从长事务活动会话查找当时的SQL关连 gv$sql_audit,查当内存使用使用较大的SQL(REQUEST_MEMORY_USED), kill session并通知应用整改。
grep “clear dtl” observer.log来查询dtl的释放情况,expire_keys表示当时超时了的中间结果,数量如果不为0,也能说明可能有内存不释放问题。
memleak分析
可以使用oceanbase的leak mode event开启捕捉DtlIntermRes内存堆栈。
登录sys租户 -- 开启捕捉 obclient [oceanbase]> alter system set leak_mod_to_check='SqlExecutor'; --查看内存使用较高的堆栈地址 obclient [oceanbase]> SELECT * FROM oceanbase.__all_virtual_mem_leak_checker_info ORDER BY alloc_count DESC limit 10; -- 转换地址为函数 [root@ob1 ~]# addr2line -pCfe /home/admin/oceanbase/bin/observer [__all_virtual_mem_leak_checker_info.back_trace地址] oceanbase::common::lbt(char*, int) at ./build_rpm/deps/oblib/src/lib/./deps/oblib/src/lib/utility/ob_backtrace.cpp:136 oceanbase::common::ObMemLeakChecker::on_alloc(oceanbase::lib::AObject&) at ??:? oceanbase::lib::ObMallocAllocator::alloc(long, oceanbase::lib::ObMemAttr const&) at ./build_rpm/deps/oblib/src/lib/./deps/oblib/src/lib/alloc/ob_malloc_allocator.cpp:125 oceanbase::common::ob_malloc(long, oceanbase::lib::ObMemAttr const&) at ./build_rpm/src/sql/./deps/oblib/src/lib/allocator/ob_malloc.h:53 oceanbase::sql::dtl::ObDTLIntermResultManager::process_interm_result_inner(oceanbase::sql::dtl::ObDtlLinkedBuffer&, oceanbase::sql::dtl::ObDTLIntermResultKey&, long, long, long, bool, bool, long&) at ./build_rpm/src/sql/./src/sql/dtl/ob_dtl_interm_result_manager.cpp:495 oceanbase::sql::dtl::ObDTLIntermResultManager::process_interm_result(oceanbase::sql::dtl::ObDtlLinkedBuffer*, long) at ??:? oceanbase::sql::dtl::ObDtlSendMessageP::process_msg(oceanbase::sql::dtl::ObDtlRpcDataResponse&, oceanbase::sql::dtl::ObDtlSendArgs&) at ./build_rpm/src/sql/./src/sql/dtl/ob_dtl_rpc_processor.cpp:50 oceanbase::sql::dtl::ObDtlSendMessageP::process() at ./build_rpm/src/sql/./src/sql/dtl/ob_dtl_rpc_processor.cpp:41 oceanbase::obrpc::ObRpcProcessorBase::run() at ./build_rpm/deps/oblib/src/rpc/./deps/oblib/src/rpc/obrpc/ob_rpc_processor_base.cpp:82 oceanbase::omt::ObWorkerProcessor::process_one(oceanbase::rpc::ObRequest&) at ./build_rpm/src/observer/./src/observer/omt/ob_worker_processor.cpp:76 oceanbase::omt::ObWorkerProcessor::process(oceanbase::rpc::ObRequest&) at ./build_rpm/src/observer/./src/observer/omt/ob_worker_processor.cpp:147 oceanbase::omt::ObThWorker::process_request(oceanbase::rpc::ObRequest&) at ./build_rpm/src/observer/./src/observer/omt/ob_th_worker.cpp:311 oceanbase::omt::ObThWorker::worker(long&, long&, int&) at ??:? non-virtual thunk to oceanbase::omt::ObThWorker::run(long) at ??:? operator() at ./build_rpm/deps/oblib/src/lib/./deps/oblib/src/lib/coro/co_user_thread.cpp:234 eeentry(boost::context::detail::transfer_t) at ./build_rpm/deps/oblib/src/lib/./deps/oblib/src/lib/coro/co_user_thread.cpp:44
什么是DTL
OceanBase 数据库将数据传输封装成了叫做 DTL(Data Transfer layer)的模块,可以通过 SHOW PARAMETERS 来查找相关参数的
obclient> SHOW PARAMETERS LIKE '%dtl%'\G
*************************** 1. row ***************************
zone: zone1
svr_type: observer
svr_ip: 172.xx.xxx.xxx
svr_port: 2882
name: dtl_buffer_size
data_type: CAPACITY
value: 64K
info: to be removed
section: OBSERVER
scope: CLUSTER
source: DEFAULT
edit_level: DYNAMIC_EFFECTIVE
1 row in set
似乎只有一个参数 dtl_buffer_size, 但是它是控制 EXCHANGE 算子之间(即Transmit 和 Receive 之间)发送数据时,每次发送数据的 Buffer 的大小(用于设置 SQL 数据传输模块使用的缓存大小)。
可以查询 V$OB_DTL_INTERM_RESULT_MONITOR展示 DTL 中间结果管理器的监控信息,查看哪个SQL占用内存较高(HOLD_MEMORY)。注意当如果DTL内存泄露后,可能会导致该视图的数据量暴增,再频繁的查询该VIEW,可能会导致SYS租户内存耗尽,而查询业务租户时,部分查询需要SYS租户获取metadata,因为SYS内存耗尽问题,业务租 户同样会报错4013的内存失败错误。
Select trace_id,hold_memory/1024/1024/1024 hold_memory_G From __all_virtual_dtl_interm_result_monitor Where svr_ip = ‘xxx’ Order by hold_memory desc Limit 20;
SELECT SQL_ID, COUNT(*) FROM gv$sql_audit WHERE tenant_id = 1999 AND USER_NAME = 'XXXX' AND TRACE_ID IN ( SELECT DISTINCT a.TRACE_ID FROM ( SELECT TRACE_ID, PLAN_OPERATION FROM gv$sql_plan_monitor WHERE PLAN_OPERATION = 'PHY_SUBPLAN_FILTER' AND FIRST_REFRESH_TIME >= NOW() - INTERVAL '600' SECOND ) a INNER JOIN ( SELECT TRACE_ID, PLAN_OPERATION FROM gv$sql_plan_monitor WHERE PLAN_OPERATION LIKE '%PX_FIFO_COORD%' AND FIRST_REFRESH_TIME >= NOW() - INTERVAL '600' SECOND ) b ON a.TRACE_ID = b.TRACE_ID UNION SELECT DISTINCT c.TRACE_ID FROM ( SELECT TRACE_ID, PLAN_OPERATION FROM gv$sql_plan_monitor WHERE PLAN_OPERATION = 'PHY_NESTED_LOOP_JOIN' AND FIRST_REFRESH_TIME >= NOW() - INTERVAL '600' SECOND ) c INNER JOIN ( SELECT TRACE_ID, PLAN_OPERATION FROM gv$sql_plan_monitor WHERE PLAN_OPERATION LIKE '%PX_FIFO_COORD%' AND FIRST_REFRESH_TIME >= NOW() - INTERVAL '600' SECOND ) d ON c.TRACE_ID = d.TRACE_ID ) AND request_time > TIME_TO_USEC(NOW() - INTERVAL 600 SECOND) AND sql_id <> '' GROUP BY SQL_ID ORDER BY 2 DESC LIMIT 20;
什么是“DtlIntermRes”
以DtlIntermRes为关键字全网好像就《分布式计划执行时中间结果占用内存过大》提到了一下,分布式计划执行时中间结果全文应该是DTL interm results ,没有文档只能去githup上从开源版找找关键字. 也没找到具体的描述,但中间结果不同于sort area,hash area, 也可能包含节点间SQL执行过程中分布式执行计划在不同节点的数据缓存?我不确认。
class ObDTLMemProfileInfo : public ObSqlMemoryCallback
{
public:
ObDTLMemProfileInfo(const uint64_t tenant_id)
: allocator_(tenant_id),
profile_(ObSqlWorkAreaType::HASH_WORK_AREA),
sql_mem_processor_(profile_),
ref_count_(0), row_count_(0),
mutex_(common::ObLatchIds::SQL_MEMORY_MGR_MUTEX_LOCK) {}
~ObDTLMemProfileInfo() {}
// The local channel and the rpc channel may modify the interm results concurrently,
// and these interme results may be linked to the same profile.
// Therefore, access to the profile needs to be protected by locks
// to prevent concurrent modification issues.
限制max_dtl_memory_size内存使用?
从上面看好像是max_tenant_memory_limit_size* max_mem_percent_ / 100, 没有独立的限制DTL的大小, 这内存区我无法确认对应oracle这种集中式数据库是什么内存区,朋友说应该是PGA,但它有没有像oracle pga_max_size,和tidb mem_quota_query limit参数限制, 发现只有ob_sql_work_area_percentage参数, 如果是PGA 那应该对应的是 ora-4030 ,但错误信息也没有什么提示,限制 tenant memory limit那应该是max_tenant_memory_limit_size(租户内存大小)
OB_INLINE int64_t ObDtlChannelMemManager::get_max_dtl_memory_size() { if (0 == max_mem_percent_) { get_max_mem_percent(); } return get_max_tenant_memory_limit_size() * max_mem_percent_ / 100; } OB_INLINE int64_t ObDtlChannelMemManager::get_max_tenant_memory_limit_size() { int ret = OB_SUCCESS; if (0 == memstore_limit_percent_) { get_memstore_limit_percentage_(); } int64_t percent_execpt_memstore = 100 - memstore_limit_percent_; return lib::get_tenant_memory_limit(tenant_id_) * percent_execpt_memstore / 100; } OB_INLINE bool ObDtlChannelMemManager::out_of_memory() { bool oom = false; int64_t used = get_used_memory_size(); int64_t max_dtl_memory_size = get_max_dtl_memory_size(); if (used > max_dtl_memory_size) { oom = true; } return oom; }
如当在一台 100 GB 的机器上启动一个 OceanBase 数据库实例时,memory_limit_percentage 和 memory_limit 参数的值是如何影响 OceanBase 数据库的内存上限的。
| 示例 | memory_limit_percentage | memory_limit | OceanBase 数据库内存上限 |
|---|---|---|---|
| 示例 1 | 80 | 0 | 80 GB |
| 示例 2 | 80 | 90 GB | 90 GB |
应急方法,如果有可用内存,可以临时加大memory_limit_percentage和system_memory,使用更多的内存,但治标不治本。
错误编码
static const _error _error_OB_ALLOCATE_MEMORY_FAILED = {
.error_name = "OB_ALLOCATE_MEMORY_FAILED",
.error_cause = "Internal Error",
.error_solution = "Contact OceanBase Support",
.mysql_errno = -1,
.sqlstate = "HY001",
.str_error = "No memory or reach tenant memory limit",
.str_user_error = "No memory or reach tenant memory limit",
.oracle_errno = 600,
.oracle_str_error = "ORA-00600: internal error code, arguments: -4013, No memory or reach tenant memory limit",
.oracle_str_user_error = "ORA-00600: internal error code, arguments: -4013, No memory or reach tenant memory limit",
.ob_str_error = "OBE-00600: internal error code, arguments: -4013, No memory or reach tenant memory limit",
.ob_str_user_error = "OBE-00600: internal error code, arguments: -4013, No memory or reach tenant memory limit"
};
而在Oceanbase是当MemStore 就被用完,才返回用户报错 4030 (Over tenant memory limits)。看来还不是很一致。
分布式计划执行时中间结果占用内存过大案例描述DtlIntermRes内存大的问题原因是OB设计问题。所有中间结果的落盘是由一个后台线程完成的,写入速度较大时落盘跟不上,会导致占用内存越来越大。在读落盘的中间结果时,会申请内存存放数据,读完以后会释放内存释。为了避免频繁申请释放内存,这里做了优化,不会真正把内存释放掉,而是放入 free_list_ 中等待后续复用。导致 free_list_ 中的内存没有得到复用,因此导致使用的内存越来越多。该案例与我们的案例现象较接近.
解决方法
hint /*+ parallel(2) */ 开启并行,并行度大于 1 时是流式执行,不会写中间结果。create or replace outline OLT_DCCAxxxx on 'DCCAxxxxx' using hint /*+parallel(2) */ ;
create or replace outline OLT_DCCAxxxx on 'DCCAxxxxx' using HINT /*+max_concurrent(10)*/;
Ora-600 4013 和4030的区别
错误码 4013 的错误信息如下。
ERROR 4013 ( HY001 ): No memory or reach tenant memory limit
该错误码表示各个模块内存不足,例如 working area 不足。出现该问题的可能原因是编译和执行模块内存不足。此时 MemStore 并没有占满,仅仅是各个模块内存不足导致的报错。
错误码 4030 的错误信息如下。
ERROR 4030 ( HY000 ): Over tenant memory limits
该错误码表示 MemStore 内存不足,该错误通常发生在 INSERT、UPDATE、DELETE 语句及 table_scan 动作等 MemStore 操作上,此时错误的日志信息不是 ERROR 级别,而是 WARN 级别。
小结
因为没有日志和精力去定位该问题,更多也是没有太多的资料,当前的知识库或者是相比Oracle MOS 问题的描述还不是很透明或详细,不过我整理了好像个人能力有限也只到这个地步,这是所有国产库都存在知识库生态不足的问题,不过OB的文档相比已经是较全面的,更多可参考官方在线文档4013 内存爆问题的排查.
— update —
根因因为在数据库给某SQL使用outline绑定执行计划,触发了Oceanbase 内存组件的bug,导致内存泄露,引发该问题。 该BUG为OBServe DtlintermRes(简称DTL)模块内存泄漏, SQL特征为查询条件中使用了OR或AND+子查询且执行计划中使用了subplan filter+px batch rescan算子才有概率触发.
- Rescan:在关联时,使用驱动表的每一行数据去扫描被驱动表,每一次扫描称为一次 rescan。
原因是batch rescan优化的过程中,如果上一个batch的数据没读完,比如上方有limit,导致上一个batch尾部有一些中间结果没释放。
https://www.oceanbase.com/knowledge-base/oceanbase-database-1000000000217882
PX batch rescan 优化
在 NLJ(nested-loop join)和 SPF(subplan filter)中,需要针对驱动表中的每一行重新扫描被驱动表中的数据,如果被驱动表是分区表且分布在不同的机器上,在分布式重新扫描(Rescan)的过程中会包括资源释放、调度重启等过程,这些操作常常伴随着消息等待、同步以及网络传输操作,影响执行效率
对于 PX rescan 带来的性能问题,OceanBase 数据库从 V3.1.2 版本开始提供了 batch rescan 的优化,通过对驱动表中的记录进行分批,每一批数据 rescan 一次被驱动表,从而减小 rescan 的次数,提升性能。Batch rescan 的功能由隐藏参数控制,默认为开启。
| 参数名称 | 描述 | 默认值 |
|---|---|---|
_enable_px_batch_rescan |
控制在 NLJ 生成分布式 PX RESCAN 计划执行时是否使用BATCH RESCAN,可以获得更好的性能。 | True |
在 EXPLAN 结果中,PX batch rescan 的使用可以通过 px_batch_rescan=true 来识别,示例如下。
Outputs & filters:
-------------------------------------
0 - output ...
1 - output ... batch_join=false, px_batch_rescan=true
需要注意的是,到目前为止 PX batch rescan 对于使用 anti/semi Join 的 NLJ 是不支持的,也就是说对于 EXISTS、NOT EXISTS 子查询转换为 Join 的 NLJ 处理,可能无法使用该优化,需要把子查询改写成 LEFT JOIN 来使用 PX batch rescan 提升性能。
Reference
https://www.modb.pro/db/1851164362112851968