OceanBase 压测时为什么冻结阈值在变化?

本文从源码角度分析了 OceanBase 压测中冻结阈值动态变化的原因,并给出运维建议。

作者:张乾

外星人2号,兼任五位喵星人的铲屎官。

本文来源:原创投稿

  • 爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。

背景

测试在用小租户进行压测时,注意到监控中触发内存冻结的阈值是动态变化的,现象如下:

  • 非压测时: 冻结阈值表现为 OceanBase 官方文档说明的 memstore_limit_percentage * freeze_trigger_percentage 所计算的值,约为 322MB
  • 压测时: 冻结阈值表现出不规律的降低,但没有出现升高的情况,即始终低于非压测时的 322MB

OceanBase 压测时为什么冻结阈值在变化?_第1张图片

为什么压测时该阈值会变化?接下来分析一下现象的原因。

环境信息

版本信息 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 中检索视图名称,找到该视图的定义如下:

OceanBase 压测时为什么冻结阈值在变化?_第2张图片

可以看到查询视图 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

OceanBase 压测时为什么冻结阈值在变化?_第3张图片

通常,从方法 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 压测时为什么冻结阈值在变化?_第4张图片

全部堆栈如下:

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::_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::operator()() const(const std::function * 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;
}

计算方式小结

  1. 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:触发冻结的阈值
  2. 开始会先获取租户资源管理器,用于后续获取实时内存值。如果获取失败,会采用默认的 freeze_trigger_percentage / 100 * memstore_limit 进行计算(此处存在缺陷,计算结果恒为 0,已提 PR)。
  3. 获取资源管理器成功后,最终冻结阈值 memstore_freeze_trigger 的计算基于以下两个值中的一个:
    • mem_memstore_limit:由参数 memstore_limit_percentage 设置。
    • max_mem_memstore_can_get_now:根据实时内存情况计算得出,即 max_mem_memstore_can_get_now = 租户下当前未分配的内存 + memstore持有内存 + kv_cache 持有内存。
  4. 取上面两个值中较小的一个记为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 的内存,导致冻结阈值变低。

OceanBase 压测时为什么冻结阈值在变化?_第5张图片

运维建议

当我们的运维同学发现冻结阈值发生变化时,说明存在其他内存模块挤占了 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

你可能感兴趣的:(mysql,oceanbase)