故障诊断 Oceanbase “ORA-00600: internal error code, arguments: -4013, No memory or reach tenant memory limit” 特烦恼

昨天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;
通过view 的trace_id再结合gv$sql_audit定位对应的SQL_ID, 但是当该view数据量过大时,可能会查询无响应。可以根据一些可疑算子,筛选一部分SQL.
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_ 中的内存没有得到复用,因此导致使用的内存越来越多。该案例与我们的案例现象较接近.

 解决方法

1,可以使用 hint /*+ parallel(2) */ 开启并行,并行度大于 1 时是流式执行,不会写中间结果。
create or replace outline OLT_DCCAxxxx on 'DCCAxxxxx' using hint /*+parallel(2) */ ;
2,问题SQL发送的数据过大,判断合理性
3,   如果DTL持续放大,可以对SQL限流
create or replace outline OLT_DCCAxxxx on 'DCCAxxxxx' using HINT /*+max_concurrent(10)*/;
4,升级到V3.2.4 BP6以上。
5,   应急方法,可以手动切主操作(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