OceanBase源码解读:物化视图日志清理器 ob_mlog_purge

物化视图的快速刷新(FAST REFRESH)依赖一张”变更日志表”——MLog(materialized view log)。基表每发生一次 DML,MLog 就追加一行带旧值/新值的变更记录。只写不删,这张表迟早会吃掉整个租户的存储空间,所以必须有人负责回收。OceanBase 4.3 里承担这个职责的是 storage/mview/ob_mlog_purge.cpp 中的 ObMLogPurger。这篇文章拆解它的三段式流程,以及最核心的那个问题:一次到底能删到哪个位点。

一、架构位置与三段式流程

调用方是 rootserver 一侧的物化视图后台任务(MLog 维护任务、日志裁剪任务),它们按阈值或按需触发;真正”怎么删”的逻辑全部收敛在 ObMLogPurger 里,最终通过内部事务 ObMViewTransaction 落到 SQL 层。

rootserver/mview 后台任务
        ↓ 按阈值 / 按需调度
ObMLogPurger::purge()            ← 总编排
        ↓ trans_(内部表事务封装)
DELETE FROM __mv_log_xxx WHERE ... (purge_scn 之前的行)
        ↓
更新 __all_mlog:last_purge_scn / date / rows / trace_id

整个类只有四个成员函数,职责切得很干净:

  • init():只做入参校验和浅拷贝,不干重活(典型的两阶段构造)。
  • get_and_check_mlog_database_schema():顺着基表找到所在 database,因为内部事务必须绑定 db 上下文。
  • prepare_for_purge():加锁、算水位线、拼 SQL —— 全文件最烧脑的一段。
  • do_purge():执行 DELETE、回写元信息、清优化器缓存。

二、核心数据结构

三个关键对象串起了整个流程:

ObMLogPurgeParam   输入参数(tenant_id_、master_table_id_、purge_log_parallel_)
ObMLogInfo         日志元信息(mlog_id、last_purge_scn、data_sync_scn)
ObMViewTransaction 内部表专用事务,写内部表必须裹守卫宏

purge_scn_         本次清理的安全水位线
need_purge_        提前退出的短路标志,避免空转事务

其中 purge_scn_ 是整个设计的题眼,下一节专门讲。need_purge_ 则是一个务实的优化:没有新日志可清时,连 MDS 注册和 DELETE 都跳过,省掉一次内部表写入。

三、purge_scn 怎么算:取所有下游 MV 的最小水位

这里是唯一真正需要权衡的地方。删得太狠,会删掉还没被物化视图消费的日志,导致快速刷新失败、退化成全量刷新;删得太保守,日志就会无限堆积。OceanBase 的答案是:

purge_scn = MIN(每个依赖该基表的物化视图的 refresh_scn)

但不是所有物化视图都参与这个最小值计算。代码里有一组很精确的筛选条件:

// 只挑"真的会读 MLog"的物化视图参与水位计算
} else if (mview_info.get_refresh_method() == ObMVRefreshMethod::FAST
        || mview_info.get_refresh_method() == ObMVRefreshMethod::FORCE
        || table_schema->mv_on_query_computation()) {
  // for fast refresh mv or real-query mv
  if (mview_info.get_is_synced()) {
    // 已同步:日志已投递到 MV 侧,可用更激进的 data_sync_scn
    min_mview_refresh_scn = MIN(min_mview_refresh_scn, mview_info.get_data_sync_scn());
  } else {
    // 未同步:只能用保守的 last_refresh_scn,慢一点但绝不误删
    min_mview_refresh_scn = MIN(min_mview_refresh_scn, mview_info.get_last_refresh_scn());
  }
}

三种情况被排除在外:COMPLETE 刷新方式的物化视图每次全量重算、根本不读日志;last_refresh_scn 为 0 或无效说明这张 MV 从未刷新过,若让它参与计算会把水位压到 0、导致永远清不掉;元信息缺失(很可能正在被 drop)时直接把错误吞掉,当作”无此依赖”。

还有一处细节值得留意:is_synced 为真时用 data_sync_scn(日志已投递到的位点),为假时退回 last_refresh_scn(刷新真正完成的位置)。前者更激进、吞吐更好,后者更保守、绝对安全,代码选择了”状态允许才激进”。

拿到最小值之后,决定 need_purge_ 的逻辑只有三行味道:

if (min_mview_refresh_scn != UINT64_MAX) {
  if (min_mview_refresh_scn <= mlog_info_.get_last_purge_scn()) {
    need_purge_ = false;            // 上次已清到这个位置,本次无事可做
  } else if (OB_FAIL(purge_scn_.convert_for_inner_table_field(min_mview_refresh_scn))) {
    ...
  } else {
    need_purge_ = true;             // 有新增的可回收区间
  }
} else {
  purge_scn_ = current_scn;         // 没有任何 MV 依赖 → 全量回收
  need_purge_ = true;
}

那个 convert_for_inner_table_field 是容易踩坑的地方:SCN 的”事务视图值”和”内部表字段值”是两套编码,跨边界必须显式转换,否则会写出一个语义错乱的 SCN。

四、并发与事务的边界

purge() 里事务的开启位置很有讲究——它在 prepare_for_purge() 之前就启动,原因在 prepare 里:fetch_mlog_info(..., true /*for_update*/) 会对 __all_mlog 中那行元信息加行锁。这样两个并发的 purge 请求会在这把行锁上串行化,不会同时去删同一张日志表。

另一个顺序细节是”先注册 MDS、再 do_purge”。MDS 是物化视图侧的元数据操作日志,先把 MVIEW_OP_TYPE::PURGE_MLOG 的意图登记下来,再去真正删数据,便于下游感知”这些日志已经被清过了”。

事务收尾用了统一的一段:

if (trans_.is_started()) {
  int tmp_ret = OB_SUCCESS;
  if (OB_SUCCESS != (tmp_ret = trans_.end(OB_SUCC(ret)))) {
    LOG_WARN("failed to commit trans", KR(ret), KR(tmp_ret));
    ret = COVER_SUCC(tmp_ret);   // 前面已失败时,不让提交错误覆盖原始错误码
  }
}

COVER_SUCC 是 OceanBase 里非常常见的错误码优先级惯用法:只有当 ret 还是成功时才用新错误覆盖它,保证调用方看到的是最先发生的根因。

五、落地三件事与可观测性

do_purge() 做三件事,任何一件失败都整体回滚:

  1. 执行 DELETE:trans_.write() 直接执行拼好的 SQL 文本,拿到 affected_rows。之所以用文本而非预编译,是因为表名和并行度都是运行期动态的。
  2. 回写元信息:把 last_purge_scn、last_purge_date、last_purge_time(纳秒转毫秒要除两次 1000)、last_purge_rows、last_purge_trace_id 一并写回 __all_mlog。这是一份相当完整的”清理账本”,DBA 靠它就能判断 GC 是否健康、有没有积压。
  3. 清动态采样缓存:日志被大量删除后,优化器手里基于旧数据采样出的统计(ObOptDSStat)已经失效,必须显式 erase,否则可能选出坏计划。这一步刻意放在事务外——即使事务回滚,多清一次缓存最多多采一次样,代价极小。

此外所有写内部表的代码都被 WITH_MVIEW_TRANS_INNER_MYSQL_GUARD 包住。这个守卫宏把事务切到 inner-table mysql 模式,让内部表操作绕开用户态的审计、权限和会话变量路径——在 OceanBase 里,只要碰 __all_mlog 这类内部表,这个宏基本是标配。

六、小结

ob_mlog_purge.cpp 只有 300 多行,但它把一件事做得很完整:用一种保守而精确的水位线,在”日志不能误删”和”日志必须回收”之间取平衡。值得记住的几个设计点:

  • 水位线的计算严格按”谁会读 MLog”来筛选,COMPLETE 刷新的 MV 完全不参与;
  • 同步状态决定用激进还是保守的 SCN,让安全性和吞吐都各得其所;
  • 行锁 + 单事务把 DELETE 与元信息回写绑成原子操作,崩溃不会留下”账实不符”;
  • 清理完顺手让优化器的采样缓存失效,把”数据变了但统计没变”的隐患一并消掉。

理解了 purge_scn 的算法,再回头看物化视图的整条刷新链路——刷新器负责推进水位,清理器负责回收水位以下的空间——两个角色正好咬合成一个闭环。这也是 OceanBase 4.3 物化视图实现的骨架所在。

发表回复

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