OceanBase源码解读:MLog维护任务 ob_mlog_maintenance_task

物化视图能加速复杂查询,靠的是一张”记账本”——物化视图日志(MLog,Materialized View Log)。基表每次 DML 都会把变更记录追加到 MLog 里,刷新时据此增量更新物化视图。但记账本本身也需要人管:用户把基表或物化视图删掉之后,遗留的 MLog 元数据和它挂载的 purge 定时任务如果没人清理,就会在系统表里越积越多。本文要解读的 ob_mlog_maintenance_task.cpp(约 340 行,位于 rootserver/mview 模块)就是干这件事的”MLog 垃圾回收员”——它周期性扫描全租户的 MLog 元数据,发现 schema 里已经查不到对应对象时,就把 MLog 干净利落地回收掉。

整体设计:一个会自己排班的状态机

ObMLogMaintenanceTask 继承自定时任务基类,但它的调度方式很有意思:不是常驻的周期定时器,而是”一次性任务自续期”——每轮执行结束时由 switch_status() 主动排下一次调度。这样做的好处是每一轮的状态都清晰可观测,中途可以随时取消,失败也不会陷入高频空转。

整个任务是一台四状态的状态机:

PREPARE ──► GC_MLOG ──► SUCCESS/FAIL ──► cleanup ──► PREPARE(下一轮)
   │            │
   └─ EAGAIN ───┘  (可重试错误,原地重试)

每次定时器到期,runTimerTask() 只执行当前状态对应的一小步,分批推进而非一口气做完,避免长时间占用定时器线程。状态流转的唯一出口是 switch_status():成功就切状态;遇到 OB_EAGAIN 这类”暂时性失败”就保持原状态原地重试;其他错误一律转 FAIL 并记录错误码。无论哪种结果,末尾都会再排下一次调度,任务链永远不断。

核心阶段一:PREPARE 的灰度门槛

PREPARE 阶段做一件小事但很关键——取租户最小数据版本做兼容性检查:

if (OB_FAIL(GET_MIN_DATA_VERSION(tenant_id_, compat_version))) {
  LOG_WARN("fail to get data version", KR(ret), K_(tenant_id));
} else if (OB_UNLIKELY(compat_version < DATA_VERSION_4_3_0_0)) {
  ret = OB_EAGAIN;  // 租户还没升到 4.3,物化视图特性不存在,等下次再试
  ...
}

低于 4.3 说明本租户根本还没有物化视图特性,直接返回 OB_EAGAIN 原地等待。这是 4.3 新特性代码的标准姿势:用数据版本做灰度锚点,保证升级过程中新旧逻辑互不干扰。

核心阶段二:GC_MLOG 的分批扫描与误判防御

GC_MLOG 是真正干活的地方,采用”拉一批、清一批”的交替模式。当待处理队列为空时,按主键游标续扫下一批 MLog 元数据:

if (mlog_ids_.empty()) { // fetch next batch
  if (OB_FAIL(ObMLogInfo::batch_fetch_mlog_ids(*GCTX.sql_proxy_, tenant_id_,
      last_fetch_mlog_id_, mlog_ids_, MLOG_NUM_FETCH_PER_SCHED))) {
    ...
  } else {
    fetch_mlog_num_ += mlog_ids_.count();
    fetch_finish_ = mlog_ids_.count() < MLOG_NUM_FETCH_PER_SCHED; // 不满一批即扫完
    if (!mlog_ids_.empty()) {
      last_fetch_mlog_id_ = mlog_ids_.at(mlog_ids_.count() - 1);   // 推进游标
    }
  }
}

每批最多取 MLOG_NUM_FETCH_PER_SCHED 条,取不满一批说明全表扫描完成。这种游标分页方式对大租户非常友好,任何时刻内存里只持有一小批数据。

拿到一批后,逐条判定是否需要回收:先取 MLog 元信息,若其 schema 版本大于租户当前 schema 版本就跳过等下一轮;否则去 schema 缓存里查对应的表 schema,查不到就调用 drop_mlog() 回收。

这里藏着整份文件最有价值的防御逻辑——GC 前必须确认租户 schema 已完整刷新。源码里的大段英文注释完整推演了这个 bad case:observer 重启后 schema 刷新是分阶段的(核心 schema → 系统表 → 用户表全部刷新完,租户 schema 版本才算”正式”)。如果任务在只刷新到系统表阶段就开始工作,会把”schema 还没加载出来”的活着的 MLog 误判成”已不存在”,错杀无辜。所以代码先调 is_tenant_refreshed() 确认前提,没刷新完就返回 OB_EAGAIN 等待。这是典型的”先确认前提再决策”设计,值得所有做后台清理类任务的人借鉴。

核心阶段三:drop_mlog 的事务化回收

单个 MLog 的回收在一个事务里完成,分三步:

} else if (OB_FAIL(ObMLogInfo::fetch_mlog_info(trans, tenant_id_, mlog_id, mlog_info,
                                               true /*for_update*/, true /*nowait*/))) {
  if (OB_LIKELY(OB_ERR_EXCLUSIVE_LOCK_CONFLICT_NOWAIT == ret)) {
    ret = OB_SUCCESS; // 锁冲突:别人正在用,跳过等下一轮
  } else if (OB_LIKELY(OB_ENTRY_NOT_EXIST == ret)) {
    ret = OB_SUCCESS; // 已不存在:目标达成,同样视为成功
  }
  ...
} else if (!mlog_info.get_purge_job().empty() &&
           OB_FAIL(ObDBMSSchedJobUtils::remove_dbms_sched_job(
             trans, tenant_id_, mlog_info.get_purge_job(), true /*if_exists*/))) {
  ...
} else if (OB_FAIL(ObMLogInfo::drop_mlog_info(trans, mlog_info))) {
  ...
}

三步分别是:① 加锁取元信息(for_update + nowait),锁冲突或记录已不存在都”吞错转成功”——这保证了整个 GC 流程的幂等性,任何干扰都不会让任务失败,只是推迟到下一轮;② 删除 MLog 挂载的 purge 定时任务(DBMS_SCHED job),防止留下孤儿 job 继续空跑;③ 删除元数据记录并提交事务。整个设计处处透着”清理工作宁可慢、不可错”的原则。

小结

ob_mlog_maintenance_task.cpp 是一个只有三百多行的小文件,但麻雀虽小五脏俱全:状态机驱动的分批推进、数据版本灰度门槛、schema 刷新前提检查、游标分页扫描、幂等的吞错设计、工作/重试双间隔调度——一个生产级后台清理任务该有的防御手段在这里几乎都能找到。配合此前解读过的物化视图刷新(ob_mview_refresh)、刷新事务封装(ob_mview_transaction),4.3 物化视图体系的全貌又清晰了一分:刷新负责”记账本怎么用”,维护任务负责”记账本怎么清”,一用一清,闭环才完整。

发表回复

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