OceanBase源码解读:物化视图维护任务 ob_mview_maintenance_task

OceanBase 4.3 引入物化视图(Materialized View)后,系统里多了一类需要长期”养护”的对象:用户的 MV 可能被反复创建删除,而它的元数据记录、刷新统计、自动刷新作业分散在多张系统表里。如果某个 MV 被删除后残留了元数据(孤儿 MV),这些脏记录会越积越多。本文解读的 ob_mview_maintenance_task.cpp(位于 rootserver/mview 目录,共 370 行)就是负责这件事的后台任务——ObMViewMaintenanceTask。它是物化视图体系里的”守门员”:每分钟巡检一轮,分批扫描全租户物化视图,找出表结构已删但元数据还在的孤儿 MV,把刷新统计、DBMS Scheduler 刷新作业和元数据记录一并清掉。整个任务采用”定时器 + 状态机”的写法,每 10 秒醒一次、每次只做一小步,天然支持断点续传,是读懂 OceanBase 后台任务设计风格的绝佳样本。

一、架构位置与整体设计

ObMViewMaintenanceTask 继承自定时任务基类,每个租户一个实例(通过 MTL 多租户框架初始化)。它承担两个职责:

  • GC 孤儿物化视图:扫描全租户 MV,删除元数据残留;
  • 顺带清理刷新统计:GC 某 MV 前先清掉它的刷新统计记录,与 storage/mview 侧的 ObMViewRefreshStatsPurgeUtil 联动。

任务的状态机只有四个状态,跳转关系非常简单:

enum class StatusType
{
  PREPARE = 0,    // 环境检查:等数据版本升到 4.3
  GC_MVIEW = 1,   // 分批扫描 + GC(核心阶段,可跨多轮执行)
  SUCCESS = 100,  // 本轮完成
  FAIL = 101,     // 本轮失败
};

定时器入口 runTimerTask 每 10 秒触发一次,按当前状态分发一步操作。关键在于状态迁移函数 switch_status 的容错设计:

void ObMViewMaintenanceTask::switch_status(StatusType new_status, int ret_code)
{
  int ret = OB_SUCCESS;
  if (OB_LIKELY(OB_SUCCESS == ret_code)) {
    status_ = new_status;           // 成功 → 进入下一状态
  } else if (is_retry_ret_code(ret_code)) {
    // OB_EAGAIN → 状态保持原样,下一轮原地重试
  } else {
    status_ = StatusType::FAIL;     // 其他错误 → 判定失败
    error_code_ = ret_code;
  }
  if (in_sched_ && OB_FAIL(schedule_task(MVIEW_MAINTENANCE_SCHED_INTERVAL, false /*repeat*/))) {
    LOG_WARN("fail to schedule mview maintenance task", KR(ret));
  }
}

只有 OB_EAGAIN 被视为”环境未就绪”,状态机原地不动;其余错误一律进 FAIL。无论哪种结果,最后都会再排一次 10 秒后的唤醒——保证任务不会因一次失败而停转。这种”每步一醒、失败重试”的模式,让一个重活被天然切成了无数个小步。

二、PREPARE:等待集群版本就绪

PREPARE 阶段只做一件事:检查租户数据版本是否已升级到 4.3.0.0(物化视图特性引入的版本)。

int ObMViewMaintenanceTask::prepare()
{
  int ret = OB_SUCCESS;
  uint64_t compat_version = 0;
  if (start_time_ == -1) {
    start_time_ = ObTimeUtil::current_time();
  }
  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;   // 版本未到,EAGAIN 留在原地
    LOG_WARN("version lower than 4.3, try again", KR(ret), K_(tenant_id), K(compat_version));
  } else {
    ++round_;
    prepare_cost_us_ = ObTimeUtil::current_time() - start_time_;
    LOG_INFO("mview maintenance task prepare success", ...);
  }
  switch_status(StatusType::GC_MVIEW, ret);
  return ret;
}

版本未到就返回 OB_EAGAIN,任务每 10 秒安静空转一次,直到集群升级完成才真正开工。这保证了升级过程中的旧版本集群不会被新逻辑”越权”操作。

三、GC_MVIEW:分批扫描与孤儿判定

GC 阶段是核心,采用”取批 / 处理批”交替推进:本地批处理完后,用 last_fetch_mview_id_ 游标(keyset 分页)从系统表再取 1000 个 MV id;取不满一批说明全租户扫完,置 fetch_finish_。

对批内每个 MV,判定是否为孤儿的逻辑藏着一条重要的防线:

if (!GCTX.schema_service_->is_tenant_refreshed(tenant_id_)) {
  ret = OB_EAGAIN;   // 租户 schema 未刷全,绝不动手
} else if (OB_FAIL(GCTX.schema_service_->get_tenant_schema_guard(...))) {
  ...
}
// 批内逐个判断
} else if (mview_info.get_schema_version() > tenant_schema_version) {
  is_exist = true;   // MV 的 schema 版本比租户还新,说明 schema 还没追上来,跳过等下轮
} else if (OB_FAIL(schema_guard.get_table_schema(tenant_id_, mview_id, table_schema))) {
  ...
} else {
  is_exist = (nullptr != table_schema);  // 查不到表结构才认定可 GC
}

源码注释里详细记录了误删场景:observer 重启后租户 schema 是渐进刷新的(核心表 → 系统表 → 用户表),如果只刷到 199/200 就认为 MV 的表结构”查不到 = 已删除”,会把还活着的物化视图误 GC。所以必须先确认 is_tenant_refreshed(全量用户表 schema 刷完、版本号才正式),并且对”mview_info 版本比租户 schema 版本还新”的情况一律跳过。删除操作宁可慢一拍,也不能凭半新不旧的 schema 视图下刀——这是后台 GC 类任务非常值得借鉴的防御性设计。

找到一个孤儿 MV 后的处理分两步,且每步都可跨轮续传:

const uint64_t mview_id = gc_mview_id_;
ObMViewRefreshStats::FilterParam filter_param;
filter_param.set_mview_id(mview_id);
if (OB_FAIL(ObMViewRefreshStatsPurgeUtil::purge_refresh_stats(
      *GCTX.sql_proxy_, tenant_id_, filter_param, affected_rows, limit))) {
  ...
} else {
  gc_stats_num += affected_rows;
}
if (OB_SUCC(ret) && affected_rows < limit) {
  if (OB_FAIL(drop_mview(mview_id))) {   // 统计清完才删元数据
    ...
  } else {
    gc_mview_id_ = OB_INVALID_ID;
    ++gc_mview_num;
  }
}

第一步按 mview_id 清理刷新统计,每次唤醒累计不超过 1000 行;affected_rows < limit 说明统计已清干净,第二步才调用 drop_mview 删元数据。如果某个 MV 的统计一次清不完,gc_mview_id_ 保持不变,下一轮唤醒直接续做——这就是”断点续传”的具体实现。

四、drop_mview:一个事务清四样东西

孤儿 MV 的元数据收尾在单个事务内完成,按序清理:

int ObMViewMaintenanceTask::drop_mview(uint64_t mview_id)
{
  ...
  if (OB_FAIL(trans.start(GCTX.sql_proxy_, tenant_id_))) {
    ...
  } else if (OB_FAIL(ObMViewInfo::fetch_mview_info(trans, tenant_id_, mview_id, mview_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 (OB_FAIL(ObMViewRefreshStatsParams::drop_mview_refresh_stats_params(
               trans, tenant_id_, mview_id, true /*if_exists*/))) {
    // 删刷新统计参数
  } else if (!mview_info.get_refresh_job().empty() &&
             OB_FAIL(ObDBMSSchedJobUtils::remove_dbms_sched_job(
               trans, tenant_id_, mview_info.get_refresh_job(), true /*if_exists*/))) {
    // 删 DBMS Scheduler 自动刷新作业
  } else if (OB_FAIL(ObMViewInfo::drop_mview_info(trans, mview_info))) {
    // 最后删 mview_info 记录本身
  }
  ...
}

三个细节值得注意:一是加锁用 for_update + nowait,锁冲突或记录不存在都不报错,不让并发操作阻塞整个 GC 流程;二是刷新统计参数和 DBMS 作业都用 if_exists 删,容忍重复清理;三是四个动作放进同一个事务,保证”统计参数、刷新作业、元信息”要么一起删掉要么都不动,不会出现删了一半的中间态。

五、finish 与节奏控制

SUCCESS/FAIL 两个状态共用 finish:打印本轮全程耗时与各计数器(扫描数、GC 数、清统计数),调用 cleanup 复位所有断点状态,然后按 1 分钟间隔排下一轮。对比一下两个间隔常量就明白了整个节奏:工作中间的步进唤醒是 10 秒(尽快续上断点),而一轮完整维护结束后的下一轮是 1 分钟——既保证断点快速续传,又不会在没活干时空转太频繁。

小结

ob_mview_maintenance_task.cpp 篇幅不大,但浓缩了 OceanBase 后台任务的三条经典设计原则:状态机切步(重活拆成 10 秒一步的小操作,EAGAIN 断点续传)、防御性 GC(schema 未刷全绝不动手、版本比对跳过、锁冲突可跳过)、事务化收尾(关联数据一次事务删干净)。如果你在写类似的”巡检 + 清理”型后台任务,这个文件是一个可以直接照抄的模板。

发表回复

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