OceanBase源码解读:物化视图日志修剪任务 ob_mview_trim_mlog_task

物化视图日志(MLog)是 OceanBase 4.3 MV 快速刷新体系的基石:基表每次 DML 都会往 MLog 里追加一条变更记录,供 MV 做增量刷新。但 MLog 的结构不是一成不变的——随着 MV 增删、列被修改、刷新方式演进,MLog 里会逐渐积累”没人引用”的冗余列,甚至整个 MLog 都可能变成无主之物。本篇解读的 ob_mview_trim_mlog_task.cpp(483 行)就是解决这个问题的后台任务:ObMViewTrimMLogTask,一个跑在 RootService 里、周期性执行的”MLog 瘦身师”。它对每个 MLog 做一次审计:没有 MV 还需要就整体删除;仍有 MV 依赖但存在冗余列,就按引用列集合重建一个更瘦的新 MLog 替换旧的,从源头降低 DML 的日志写放大。

架构位置:rootserver/mview 后台任务家族

ObMViewTrimMLogTask 继承自定时任务基类,与之前解读过的 ob_mview_timer_task(定时基类)、ob_mlog_maintenance_task(MLog 写入侧维护)、ob_mlog_purge(MLog 数据清理)同属一个家族。分工上有个清晰的维度:前几个任务管”日志的内容”——写进去什么、什么时候清掉;本任务管”日志的结构”——schema 演进后 MLog 这张表本身的列是否还合理。三者叠加,才构成 MLog 生命周期的完整闭环。

任务由 init → start → runTimerTask → stop/destroy 的标准生命周期驱动。init 只记录 MTL 租户号;start 调 schedule_task(MVIEW_TRIM_MLOG_INTERVAL, true) 挂成周期任务;destroy 先 cancel 后 wait,保证停机时没有线程还停在修剪流程里。每次触发 runTimerTask,先过五连前置检查的瀑布式短路:

if (IS_NOT_INIT) {
  ...
} else if (OB_UNLIKELY(is_stop_)) {
} else if (OB_FAIL(GET_MIN_DATA_VERSION(tenant_id_, data_version))) {
  ...
} else if (data_version < DATA_VERSION_4_3_5_4) {
  // 版本门禁:低于 4.3.5.4 的租户没有这个特性
} else if (!tenant_config.is_valid() || !tenant_config->enable_mlog_auto_maintenance) {
  // 租户参数开关
} else if (cur_timestamp - last_trim_time_ < tenant_config->mlog_trim_interval) {
  // 自限流:定时器间隔之外,还有一层可配置的修剪间隔
}

值得注意的细节是:这些”跳过”分支全部是空分支,静默返回而不报错——定时任务的常态就是”大多数时候没活干”,错误码瀑布里让正常跳过走得更安静。真正开始工作前还有一道并发防御:查 __all_ddl_task_status 表确认没有进行中的 DDL_CREATE_MVIEW,因为建 MV 会同时建 MLog 和容器表,修剪流程读到中间态 schema 会有竞态,宁可整体跳过等下一轮。

核心数据结构与前置准备

主循环的准备工作包括三件事。其一,ObMLogInfo::batch_fetch_mlog_ids 一次性拉取租户内全部 MLog 的 id 列表,为空直接收工。其二,从系统变量 schema 读出租户的 MySQL/Oracle 模式,并切换当前 Worker 的兼容模式——这是后台任务代码里容易被忽略的坑:定时线程是全局复用的,内部会话执行 SQL 时解析行为必须与租户一致,跑完还要把原模式还原回去,否则会污染复用同一线程的下一个任务。其三,通过 ObMViewUtils::create_inner_session 创建内部 SQL 会话供 DDL 等待使用。

逐个修剪时用了 tmp_ret 而不是主 ret 记录单条失败,保证一个 MLog 出问题不会中断整批——这是批处理任务的经典写法:

for (int64_t i = 0; i < mlog_count; ++i) {
  const uint64_t mlog_id = mlog_ids.at(i);
  int64_t tmp_ret = OB_SUCCESS;
  if (OB_TMP_FAIL(trim_mlog_impl(mlog_id, schema_guard, sql_proxy, session))) {
    LOG_WARN("failed to trim mlog", KR(tmp_ret), K(mlog_id));
  }
}

trim_mlog_impl:一次修剪的二分决策

单个 MLog 的处理是五连 schema 校验(MLog schema → 经 get_data_table_id 找到基表 schema → 库 schema → 相关 MV 清单)之后进入二分路由:相关 MV 清单为空就 drop_mlog 整删;非空就 replace_mlog 尝试裁列。这里有个对并发 DDL 的容忍设计:如果 MLog schema 拿不到(NULL),视为”刚被用户删掉”,只打日志 bypass,不算错误——定时任务与用户操作天然并发,这种边界的语义选择很能体现工程成熟度。

而”相关 MV”的判定在 get_relevent_mviews 里,有三个过滤层次,其中两个特别值得展开。

第一层是容器表 id 换回。如果基表本身是某个 MV 的容器表(complete refresh 之后容器表 id 会变,但依赖信息不会跟着更新),必须先通过 get_mview_id_from_container_id 把 id 换回 mview id 再去查依赖,否则查出来恒为空,MLog 会被误判成”无人使用”而遭整体删除——这是 complete refresh 路径埋下的坑,在这里做了关键防御。

第二层是刷新方式过滤。依赖清单里只保留 FAST / FORCE 刷新的 MV 和 on-query-computation 的 MV。逻辑很直白:只有做增量刷新的 MV 才真正消费 MLog 数据,COMPLETE 刷新是全量重算,不需要日志。一个基表若只剩 COMPLETE 刷新的 MV,它的 MLog 就成了纯粹的写放大,该删。

drop_mlog:复用 DDL 通道的删除

整删 MLog 没有走内部 SQL 直接删表,而是组装 ObDropIndexArg,把 index_action_type_ 设为 DROP_MLOG,通过 RPC 发给主 RootService 走正规 DDL 流水线。这个设计很聪明:在 schema 层面,MLog 本来就被建模为基表上的”索引”,复用 drop_index 通道意味着 DDL 调度、事务性、回滚、失败重试全部免费获得。参数里的 based_schema_object_infos_ 带上基表的 schema 版本快照,DDL 执行期可以检测版本冲突;is_add_to_scheduler_ = true 则交给 DDL 调度器异步执行。

动手前还有一道幂等防御:校验 MLog 的 table_id 与基表 schema 的 get_mlog_tid() 一致,防止拿着过期的 schema 引用删错对象。执行完成后通过 TENANT_EVENT(..., AUTO_DROP_MLOG, ...) 把耗时、基表名、新旧 id 写进租户事件中心——自动化动作必须可观测,这是整个文件一以贯之的原则。

replace_mlog:裁列重建的主路径

裁列重建是最有含金量的部分,四步流水:

// ① 引用收集:每个相关 MV 用 ObMVProvider 解析出它引用基表的列
sql::ObMVProvider mv_provider(tenant_id_, mview_id);
mv_provider.get_columns_referenced_by_mv(tenant_id_, mview_id,
    base_table_schema->get_table_id(), session, &schema_guard,
    referenced_column_set);

// ② 冗余判定:MLog 现有列逐个比对引用集合
if (column_id < OB_MLOG_SEQ_NO_COLUMN_ID) {   // 跳过 seq_no 等系统列
  if (column_schema->is_rowkey_column()) {
    // rowkey 永不被裁
  } else if (OB_FAIL(referenced_column_set.exist_refactored(column_id))) {
    if (OB_HASH_NOT_EXIST == ret) {
      ret = OB_SUCCESS;
      need_replace_mlog = true;   // 发现无人引用的列 → 触发重建
    }
  }
}

②里 exist_refactored 把错误码当返回值用:OB_HASH_EXIST 表示列被引用、OB_HASH_NOT_EXIST 表示冗余,两种”正常”结果都要手工把 ret 归零,否则污染主错误码——哈希容器 API 的经典处理范式。另外遍历条件里 `!need_replace_mlog` 意味着只要发现一个冗余列就提前停止扫描,不需要精确清单,反正重建时会重新收集。

③组装新列清单时有三类排除:rowkey 列和 heap 表主键列会被新 MLog 自动带上,生成列(generated column)则因为 MLog 本身不支持而跳过。④执行替换:submit_build_mlog_task 提交”按列清单建新 MLog”的 DDL 任务,wait_ddl_finish 同步等待完成。一个容易踩的坑在这里显式处理了:DDL 换掉 MLog 之后,旧的 schema 引用全部过期,必须新开一个 schema guard 重新取基表 schema,才能拿到新 MLog 的 id:

if (OB_FAIL(ObMViewUtils::submit_build_mlog_task(tenant_id_, final_columns_list,
        schema_guard, base_table_schema, task_id))) {
  ...
} else if (OB_FAIL(ObDDLExecutorUtil::wait_ddl_finish(
        tenant_id_, task_id, false, session, GCTX.rs_rpc_proxy_))) {
  ...
} else {
  ObSchemaGetterGuard new_schema_guard;          // 新 guard,旧 guard 里的 schema 已过期
  ...
  new_mlog_id = base_table_schema->get_mlog_tid();
}
TENANT_EVENT(tenant_id_, MVIEW, AUTO_CLIP_MLOG, start_ts, ret,
    cost_time, base_table_id, base_table_name_for_print.ptr(),
    old_mlog_id, new_mlog_id, new_mlog_columns.ptr());

旧 MLog 的删除由 DDL 流水线内部处理,任务侧只关心”新 MLog 建好了、id 拿到了”。最终通过 AUTO_CLIP_MLOG 事件记录裁剪前后的 mlog id 与保留的列清单。

小结

ob_mview_trim_mlog_task.cpp 展示了 OceanBase 处理”schema 演进遗留垃圾”的完整思路:定时器驱动 + 租户参数开关 + 数据版本门禁 + 自限流间隔 + 建表期竞态防御,层层筛掉不值得跑的场景;真要动手时,删除复用 DROP_MLOG DDL 通道获得全部事务保障,裁列则走”建新替旧”的替换模式,全程用租户事件中心留下观测痕迹。对使用者来说,只要保持 enable_mlog_auto_maintenance 开启,MLog 就会在后台自动跟上 MV 的增删改——删掉不再需要的 MV、裁掉不再被引用的列,都是静默完成的。对源码读者来说,这个文件是学习”自动化 DDL 运维任务该如何设计”的极佳样本:防御、幂等、观测三件套齐备,而且删除与替换分别展示了 OceanBase DDL 体系的两种典型接入姿势。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注