本文从源码角度分析了 OceanBase 压测中冻结阈值动态变化的原因,并给出运维建议。
作者:张乾
外星人2号,兼任五位喵星人的铲屎官。
本文来源:原创投稿
- 爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。
背景
测试在用小租户进行压测时,注意到监控中触发内存冻结的阈值是动态变化的,现象如下:
- 非压测时: 冻结阈值表现为 OceanBase 官方文档说明的
memstore_limit_percentage * freeze_trigger_percentage
所计算的值,约为 322MB。 - 压测时: 冻结阈值表现出不规律的降低,但没有出现升高的情况,即始终低于非压测时的 322MB。
为什么压测时该阈值会变化?接下来分析一下现象的原因。
环境信息
版本信息 | OceanBase_CE 4.1.0.0 |
租户规格 | unit 8C4G |
memstore_limit_percentage | 50(默认值) |
freeze_trigger_percentage | 20(默认值) |
sysbench 命令
sysbench /usr/share/sysbench/oltp_read_only.lua --mysql-host=x.x.x.x --mysql-port=42881 --mysql-db=sysbenchdb --mysql-user="sysbench@sysbench_tenant2" --mysql-password=sysbench --tables=10 --table_size=1000000 --threads=1024 --time=120 --report-interval=10 --db-driver=mysql --db-ps-mode=disable --skip-trx=on --mysql-ignore-errors=6002,6004,4012,2013,4016,1062 run
排查过程
确认监控采集的正确性
根据监控指标,在官方文档中找到采集 SQL 如下:
select /* MONITOR_AGENT */ con_id tenant_id, stat_id, value from v$sysstat, DBA_OB_TENANTS where stat_id IN (130002) and (con_id > 1000 or con_id = 1) and class < 1000
经过手动执行该 SQL 语句,观察到在压测过程中冻结阈值确实发生了变化,由此可以确认监控数据采集和展示并无错误。
通过源码查看冻结阈值的计算方式
上一步确认了 OceanBase 冻结阈值的确存在变化,接下来需要进一步查看源码,了解该阈值是如何计算出来的。
静态分析视图的数据来源
在源码 ${path_to_oceanbase}/src/share/inner_table/ob_inner_table_schema_def.py
中检索视图名称,找到该视图的定义如下:
可以看到查询视图 v$sysstat
实际上是查询虚拟表 __all_virtual_sysstat
。
注意:在 OceanBase 中,虚拟表并不是实际的表,是内存数据结构映射成可以用 SQL 语句直接查询的“表”。
虚拟表有对应的类来进行处理。当查询视虚拟表时,会调用相应类的方法来获取所需的值,然后将其返回给前端。
在 ${path_to_oceanbase}/src/observer/virtual_table
路径下,可以找到和虚拟表同名的头文件和对应的代码实现:
- ob_all_virtual_sys_stat.h
- ob_all_virtual_sys_stat.cpp
其中定义了虚拟表 __all_virtual_sysstat
对应的类 ObAllVirtualSysStat
。
通常,从方法 inner_get_next_row()
开始进行调用,沿着该方法的调用静态分析下去,最终找到获取冻结阈值的方法是 ObTenantFreezer::get_freeze_trigger_()
。
动态调试验证数据来源
我这里使用 VSCode + gdb 来调试,在 ObTenantFreezer::get_freeze_trigger_()
方法设置一个断点,先起一个连接到 OBServer,再启动调试模式,然后手动执行上面的采集 SQL。
注意:在 OceanBase 中,有多个方法会调用
get_freeze_trigger_()
方法,因此断点未必会命中手动执行的 SQL。需要通过显示的堆栈判断是否是采集 SQL 命中了断点;如果不是,继续点击 Continue,直至 SQL 命中断点。
另一种方式是在对应类 ObAllVirtualSysStat
下的方法里设置断点,这样在执行 SQL 后,将直接命中断点,然后可以逐步进行调试,直至达到 get_freeze_trigger_()
方法。这种方式也适用于静态分析代码时找不到所需方法的情况。
在堆栈信息中,可以通过 do_process()
方法下的 sql
参数看到执行的 SQL 内容,如下:
全部堆栈如下:
oceanbase::storage::ObTenantFreezer::get_freeze_trigger_(oceanbase::storage::ObTenantFreezeCtx & ctx) (\opt\oceanbase\src\storage\tx_storage\ob_tenant_freezer.cpp:838)
oceanbase::storage::ObTenantFreezer::get_tenant_memstore_cond(oceanbase::storage::ObTenantFreezer * this, int64_t & active_memstore_used, int64_t & total_memstore_used, int64_t & memstore_freeze_trigger, int64_t & memstore_limit, int64_t & freeze_cnt, const bool force_refresh) (\opt\oceanbase\src\storage\tx_storage\ob_tenant_freezer.cpp:758)
oceanbase::observer::ObAllVirtualSysStat::update_all_stats_(const int64_t tenant_id, oceanbase::common::ObStatEventSetStatArray & stat_events) (\opt\oceanbase\src\observer\virtual_table\ob_all_virtual_sys_stat.cpp:108)
oceanbase::observer::ObAllVirtualSysStat::process_curr_tenant(oceanbase::observer::ObAllVirtualSysStat * this, oceanbase::common::ObNewRow *& row) (\opt\oceanbase\src\observer\virtual_table\ob_all_virtual_sys_stat.cpp:288)
oceanbase::omt::ObMultiTenantOperator::execute(oceanbase::omt::ObMultiTenantOperator * this, oceanbase::common::ObNewRow *& row) (\opt\oceanbase\src\observer\omt\ob_multi_tenant_operator.cpp:125)
oceanbase::observer::ObAllVirtualSysStat::inner_get_next_row(oceanbase::observer::ObAllVirtualSysStat * this, oceanbase::common::ObNewRow *& row) (\opt\oceanbase\src\observer\virtual_table\ob_all_virtual_sys_stat.cpp:263)
oceanbase::common::ObVirtualTableIterator::get_next_row(oceanbase::common::ObVirtualTableIterator * this, oceanbase::common::ObNewRow *& row) (\opt\oceanbase\src\share\ob_virtual_table_iterator.cpp:370)
oceanbase::common::ObVirtualTableIterator::get_next_row(oceanbase::common::ObVirtualTableIterator * this) (\opt\oceanbase\src\share\ob_virtual_table_iterator.cpp:470)
oceanbase::sql::DASOpResultIter::get_next_row(oceanbase::sql::DASOpResultIter * this) (\opt\oceanbase\src\sql\das\ob_das_task.cpp:517)
oceanbase::sql::ObTableScanOp::get_next_row_with_das(oceanbase::sql::ObTableScanOp * this) (\opt\oceanbase\src\sql\engine\table\ob_table_scan_op.cpp:1690)
oceanbase::sql::ObTableScanOp::inner_get_next_row_for_tsc(oceanbase::sql::ObTableScanOp * this) (\opt\oceanbase\src\sql\engine\table\ob_table_scan_op.cpp:1885)
oceanbase::sql::ObTableScanOp::inner_get_next_row_implement(oceanbase::sql::ObTableScanOp * this) (\opt\oceanbase\src\sql\engine\table\ob_table_scan_op.cpp:1862)
oceanbase::sql::ObTableScanOp::inner_get_next_row(oceanbase::sql::ObTableScanOp * this) (\opt\oceanbase\src\sql\engine\table\ob_table_scan_op.cpp:2842)
oceanbase::sql::ObOperator::get_next_row(oceanbase::sql::ObOperator * this) (\opt\oceanbase\src\sql\engine\ob_operator.cpp:1063)
oceanbase::sql::ObSubPlanScanOp::inner_get_next_row(oceanbase::sql::ObSubPlanScanOp * this) (\opt\oceanbase\src\sql\engine\subquery\ob_subplan_scan_op.cpp:63)
oceanbase::sql::ObOperator::get_next_row(oceanbase::sql::ObOperator * this) (\opt\oceanbase\src\sql\engine\ob_operator.cpp:1063)
oceanbase::sql::ObJoinOp::get_next_left_row(oceanbase::sql::ObJoinOp * this) (\opt\oceanbase\src\sql\engine\join\ob_join_op.cpp:98)
oceanbase::sql::ObBasicNestedLoopJoinOp::get_next_left_row(oceanbase::sql::ObBasicNestedLoopJoinOp * this) (\opt\oceanbase\src\sql\engine\join\ob_basic_nested_loop_join_op.cpp:70)
oceanbase::sql::ObNestedLoopJoinOp::read_left_operate(oceanbase::sql::ObNestedLoopJoinOp * this) (\opt\oceanbase\src\sql\engine\join\ob_nested_loop_join_op.cpp:343)
oceanbase::sql::ObNestedLoopJoinOp::inner_get_next_row(oceanbase::sql::ObNestedLoopJoinOp * this) (\opt\oceanbase\src\sql\engine\join\ob_nested_loop_join_op.cpp:203)
oceanbase::sql::ObOperator::get_next_row(oceanbase::sql::ObOperator * this) (\opt\oceanbase\src\sql\engine\ob_operator.cpp:1063)
oceanbase::sql::ObExecuteResult::get_next_row(const oceanbase::sql::ObExecuteResult * this) (\opt\oceanbase\src\sql\executor\ob_execute_result.cpp:120)
oceanbase::sql::ObExecuteResult::get_next_row(oceanbase::sql::ObExecuteResult * this, oceanbase::sql::ObExecContext & ctx, const oceanbase::common::ObNewRow *& row) (\opt\oceanbase\src\sql\executor\ob_execute_result.cpp:55)
oceanbase::sql::ObResultSet::inner_get_next_row(oceanbase::sql::ObResultSet * this, const oceanbase::common::ObNewRow *& row) (\opt\oceanbase\src\sql\ob_result_set.cpp:372)
oceanbase::sql::ObResultSet::get_next_row(oceanbase::sql::ObResultSet * this, const oceanbase::common::ObNewRow *& row) (\opt\oceanbase\src\sql\ob_result_set.cpp:360)
oceanbase::observer::ObQueryDriver::response_query_result(oceanbase::observer::ObQueryDriver * this, oceanbase::sql::ObResultSet & result, bool is_ps_protocol, bool has_more_result, bool & can_retry, int64_t fetch_limit) (\opt\oceanbase\src\observer\mysql\ob_query_driver.cpp:144)
oceanbase::observer::ObSyncPlanDriver::response_result(oceanbase::observer::ObSyncPlanDriver * this, oceanbase::observer::ObMySQLResultSet & result) (\opt\oceanbase\src\observer\mysql\ob_sync_plan_driver.cpp:95)
oceanbase::observer::ObMPQuery::response_result(oceanbase::observer::ObMPQuery * this, oceanbase::observer::ObMySQLResultSet & result, bool force_sync_resp, bool & async_resp_used) (\opt\oceanbase\src\observer\mysql\obmp_query.cpp:1297)
oceanbase::observer::ObMPQuery::do_process(oceanbase::observer::ObMPQuery * this, oceanbase::sql::ObSQLSessionInfo & session, bool has_more_result, bool force_sync_resp, bool & async_resp_used, bool & need_disconnect) (\opt\oceanbase\src\observer\mysql\obmp_query.cpp:836)
oceanbase::observer::ObMPQuery::process_single_stmt(oceanbase::observer::ObMPQuery * this, const oceanbase::sql::ObMultiStmtItem & multi_stmt_item, oceanbase::sql::ObSQLSessionInfo & session, bool has_more_result, bool force_sync_resp, bool & async_resp_used, bool & need_disconnect) (\opt\oceanbase\src\observer\mysql\obmp_query.cpp:554)
oceanbase::observer::ObMPQuery::process(oceanbase::observer::ObMPQuery * this) (\opt\oceanbase\src\observer\mysql\obmp_query.cpp:361)
oceanbase::rpc::frame::ObSqlProcessor::run(oceanbase::rpc::frame::ObSqlProcessor * this) (\opt\oceanbase\deps\oblib\src\rpc\frame\ob_sql_processor.cpp:41)
oceanbase::omt::ObWorkerProcessor::process_one(oceanbase::omt::ObWorkerProcessor * this, oceanbase::rpc::ObRequest & req) (\opt\oceanbase\src\observer\omt\ob_worker_processor.cpp:67)
oceanbase::omt::ObWorkerProcessor::process(oceanbase::omt::ObWorkerProcessor * this, oceanbase::rpc::ObRequest & req) (\opt\oceanbase\src\observer\omt\ob_worker_processor.cpp:131)
oceanbase::omt::ObThWorker::process_request(oceanbase::omt::ObThWorker * this, oceanbase::rpc::ObRequest & req) (\opt\oceanbase\src\observer\omt\ob_th_worker.cpp:248)
oceanbase::omt::ObThWorker::worker(oceanbase::omt::ObThWorker * this, int64_t & tenant_id, int64_t & req_recv_timestamp, int32_t & worker_level) (\opt\oceanbase\src\observer\omt\ob_th_worker.cpp:407)
oceanbase::omt::ObThWorker::run(oceanbase::omt::ObThWorker * this, int64_t idx) (\opt\oceanbase\src\observer\omt\ob_th_worker.cpp:446)
oceanbase::lib::Threads::start()::$_156::operator()() const(const class {...} * this) (\opt\oceanbase\deps\oblib\src\lib\thread\threads.cpp:188)
std::_Function_handler<void (), oceanbase::lib::Threads::start()::$_156>::_M_invoke(std::_Any_data const&)(const std::_Any_data & __functor) (\opt\oceanbase\deps\3rd\usr\local\oceanbase\devtools\include\c++\9\bits\std_function.h:300)
std::function<void ()>::operator()() const(const std::function<void ()> * this) (\opt\oceanbase\deps\3rd\usr\local\oceanbase\devtools\include\c++\9\bits\std_function.h:688)
oceanbase::lib::Thread::__th_start(void * arg) (\opt\oceanbase\deps\oblib\src\lib\thread\thread.cpp:300)
libpthread.so.0!start_thread (Unknown Source:0)
libc.so.6!clone (Unknown Source:0)
从下往上看堆栈,前面的大部分是处理 SQL 的通用流程,后面进入到类 ObVirtualTableIterator
开始进行虚拟表的处理。
最终获取冻结阈值是调用方法 ObTenantFreezer::get_freeze_trigger_()
实现的,证明上一步的静态分析是正确的。
从源码理解冻结阈值的计算方式
get_freeze_trigger_()
源码注释如下:
(可跳过,直接阅读下方的小结)
int ObTenantFreezer::get_freeze_trigger_(ObTenantFreezeCtx &ctx)
{
int ret = OB_SUCCESS;
ObTenantResourceMgrHandle resource_handle;
// 获取租户ID
const uint64_t tenant_id = MTL_ID();
// 获取memstore上限
const int64_t mem_memstore_limit = ctx.mem_memstore_limit_;
int64_t kv_cache_mem = 0;
int64_t memstore_freeze_trigger = 0;
int64_t max_mem_memstore_can_get_now = 0;
// 获取租户资源管理器句柄,主要用于后面进一步获取租户内存管理器(ObTenantMemoryMgr)从而得到内存使用情况
if (OB_FAIL(ObResourceMgr::get_instance().
get_tenant_resource_mgr(tenant_id,
resource_handle))) {
// 如果资源管理器获取失败
LOG_WARN("[TenantFreezer] fail to get resource mgr", KR(ret), K(tenant_id));
ret = OB_SUCCESS;
// freeze_trigger_percentage的取值范围是[1,99]
// C++中除法 '/'会舍去小数部分
// 所以资源管理器获取失败的情况下,memstore_freeze_trigger的值恒为0(此处缺陷已提pr)
memstore_freeze_trigger =
get_freeze_trigger_percentage_() / 100 * mem_memstore_limit;
} else {
// 从资源管理器中获取各内存值
int64_t tenant_mem_limit = get_tenant_memory_limit(tenant_id);
int64_t tenant_mem_hold = get_tenant_memory_hold(tenant_id);
int64_t tenant_memstore_hold = get_tenant_memory_hold(tenant_id,
ObCtxIds::MEMSTORE_CTX_ID);
// 声明is_overflow,初始值为true,即溢出
bool is_overflow = true;
// 获取kv_cache持有值
kv_cache_mem = resource_handle.get_memory_mgr()->get_cache_hold();
// 如果租户总内存持有值大于租户内存上限,说明溢出
if (tenant_mem_limit < tenant_mem_hold) {
LOG_WARN("[TenantFreezer] tenant_mem_limit is smaller than tenant_mem_hold",
K(tenant_mem_limit), K(tenant_mem_hold), K(tenant_id));
}
// is_add_overflow接收三个参数,第一个和第二个参数相加的和如果小于0,返回 true,表明溢出
// 否则,将前两个参数的和赋值给第三个参数,然后返回flase,表明未溢出
// 此处max_mem_memstore_can_get_now = (tenant_mem_limit - tenant_mem_hold)+ tenant_memstore_hold
// 即max_mem_memstore_can_get_now = 租户当前未分配的内存 + 当前memstore已持有的值
else if (is_add_overflow(tenant_mem_limit - tenant_mem_hold,
tenant_memstore_hold,
max_mem_memstore_can_get_now)) {
if (REACH_TIME_INTERVAL(1 * 1000 * 1000)) {
LOG_WARN("[TenantFreezer] max memstore can get is overflow", K(tenant_mem_limit),
K(tenant_mem_hold), K(tenant_memstore_hold), K(tenant_id));
}
}
// 此处二次调用is_add_overflow,即max_mem_memstore_can_get_now = max_mem_memstore_can_get_now + kv_cache_mem
// 代入上一步的计算式,即max_mem_memstore_can_get_now = (tenant_mem_limit - tenant_mem_hold)+ tenant_memstore_hold + kv_cache_mem
// 总结:max_mem_memstore_can_get_now = 租户下当前未分配的内存 + memstore 持有内存 + kv_cache 持有内存
else if (is_add_overflow(max_mem_memstore_can_get_now,
kv_cache_mem,
max_mem_memstore_can_get_now)) {
if (REACH_TIME_INTERVAL(1 * 1000 * 1000)) {
LOG_WARN("[TenantFreezer] max memstore can get is overflow",
K(tenant_mem_limit), K(tenant_mem_hold), K(tenant_memstore_hold),
K(kv_cache_mem), K(tenant_id));
}
} else {
// 以上都没溢出的话,is_overflow 设置为 false
is_overflow = false;
}
int64_t min = mem_memstore_limit;
if (!is_overflow) {
// 如果未溢出,取 mem_memstore_limit 和max_mem_memstore_can_get_now 中较小的值来计算最终的冻结触发阈值
min = MIN(mem_memstore_limit, max_mem_memstore_can_get_now);
}
// 各内存单位为字节,这里以 100 为分界线分为两种计算方式,意义不大,已提 pr
if (min < 100) {
memstore_freeze_trigger = get_freeze_trigger_percentage_() * min / 100;
} else {
memstore_freeze_trigger = min / 100 * get_freeze_trigger_percentage_();
}
}
// result
ctx.max_mem_memstore_can_get_now_ = max_mem_memstore_can_get_now;
ctx.memstore_freeze_trigger_ = memstore_freeze_trigger;
ctx.kvcache_mem_ = kv_cache_mem;
return ret;
}
计算方式小结
ObTenantFreezer::get_freeze_trigger_()
方法中涉及七个内存相关的变量:- tenant_mem_limit:租户内存上限
- tenant_mem_hold:租户内存持有值
- mem_memstore_limit:租户 memstore 上限
- tenant_memstore_hold:租户 memstore 持有值
- kv_cache_mem:kv_cache 持有值
- max_mem_memstore_can_get_now:memstore 当前能达到的最大值
- memstore_freeze_trigger:触发冻结的阈值
- 开始会先获取租户资源管理器,用于后续获取实时内存值。如果获取失败,会采用默认的
freeze_trigger_percentage / 100 * memstore_limit
进行计算(此处存在缺陷,计算结果恒为 0,已提 PR)。 - 获取资源管理器成功后,最终冻结阈值 memstore_freeze_trigger 的计算基于以下两个值中的一个:
- mem_memstore_limit:由参数 memstore_limit_percentage 设置。
- max_mem_memstore_can_get_now:根据实时内存情况计算得出,即 max_mem_memstore_can_get_now = 租户下当前未分配的内存 + memstore持有内存 + kv_cache 持有内存。
- 取上面两个值中较小的一个记为min,根据min 的大小,以100为分界线分为两种计算方法(此处分两种计算意义不大,已提PR):
- min<100:get_freeze_trigger_percentage_() * min / 100
- min ≥ 100:min / 100 * get_freeze_trigger_percentage_()
综上,当 max_mem_memstore_can_get_now < mem_memstore_limit
时,监控图上的冻结阈值就会有变化,且这个冻结阈值不可能高于由 mem_memstore_limit
计算得出的阈值,这与我们观察到的冻结阈值曲线一致。
此外,对于在理解源码时发现的两处代码逻辑缺陷,已提 PR 和 Issue。
总结
在 OceanBase 中,冻结阈值并不是一个固定的值,它会根据当前内存情况进行实时判断。当存在除了 MemStore 和 kv_cache 之外的内存模块占用了较多的内存,挤占了 MemStore 的内存上限时,冻结阈值就会相应地降低。
结合本文场景监控图,租户 fuse_row_cache (蓝线)与冻结阈值曲线的变化趋势正好相反,说明该场景下应该是 fuse_row_cache 的增长挤占了 memstore 的内存,导致冻结阈值变低。
运维建议
当我们的运维同学发现冻结阈值发生变化时,说明存在其他内存模块挤占了 memstore 的内存,此时需要格外注意内存的使用情况,结合实际业务场景评估影响面。
另外,需要注意的是,max_mem_memstore_can_get_now
计算式中的 kv_cache 并不是所有可动态伸缩内存(比如 fuse_row_cache/user_row_cache
等)加起来的和,而是特指内存模块 KvstorCacheMb ,这部分将留到下一篇文章介绍。
更多技术文章,请访问:https://opensource.actionsky.com/
关于 SQLE
爱可生开源社区的 SQLE 是一款面向数据库使用者和管理者,支持多场景审核,支持标准化上线流程,原生支持 MySQL 审核且数据库类型可扩展的 SQL 审核工具。
SQLE 获取
类型 | 地址 |
---|---|
版本库 | https://github.com/actiontech/sqle |
文档 | https://actiontech.github.io/sqle-docs/ |
发布信息 | https://github.com/actiontech/sqle/releases |
数据审核插件开发文档 | https://actiontech.github.io/sqle-docs/docs/dev-manual/plugins/howtouse |