OceanBase 的 Paxos 一致性协议由两层构成:上层是 PalfHandleImpl 负责副本间协议协调(选举、日志序号分配、确认),下层则是 LogEngine 负责把已经达成共识的日志条目落盘、读取、推送给 follower。LogEngine 是 PalfHandleImpl 与物理块存储之间的”单机适配层”,本文聚焦其核心实现 log_engine.cpp(1915 行),梳理它的架构位置、关键数据结构、初始化与重启路径、以及面向协议层的高频入口。
架构位置
LogEngine 位于 src/logservice/palf/ 目录,每条 Paxos 副本持有一个实例。它的上下游分工清晰:
- 上层:PalfHandleImpl / PalfReplicator 提交日志、变更成员、发起 truncate/flashback;
- 下层:LogIOWorker(异步 IO 派发)、LogStorage(redo 块管理)、LogMetaStorage(元数据块管理)、LogNetService(日志推送 RPC)。
这样的分层让”协议层”只关心 LSN 连续性,”IO 层”只关心 block 持久化,二者通过异步任务对象解耦。
关键数据结构
LogEngine 内部聚合了五个核心对象,每一个都对应一类明确的职责:
- log_meta_:LogMeta,封装五类元数据 —— prepare_meta(最新提案号)、config_meta(副本列表)、mode_meta(成员变更阶段)、snapshot_meta(基线快照)、replica_property_meta(副本属性)。五类元数据被打包成单一 LogMeta,由
log_meta_lock_串行化保护,避免分散加锁导致死锁与顺序混乱。 - log_storage_:redo 日志的物理块管理,按
log_storage_block_size切块循环利用。 - log_meta_storage_:元数据块的物理管理,体量小、刷盘频率低,与 redo 物理隔离便于独立调优。
- log_io_worker_:所有写盘 IO 的统一派发器,主链路(提交流水)把任务投递出去后立即返回,不阻塞等磁盘。
- log_net_service_:负责把日志条目以 RPC 推送给 follower。
初始化与重启路径
LogEngine 提供 init/load 两条入口:
init 路径(首次创建)顺序固定为参数校验 → log_meta_storage_.init → log_storage_.init → log_net_service_.init → append_log_meta_(log_meta)。最后一步把首条 LogMeta 写入 meta_storage,确立副本的初始成员与状态。失败时除 OB_INIT_TWICE 外会自动调 destroy() 清理半初始化资源。
load 路径(重启)要复杂得多。它先 load meta_storage,再用 construct_log_meta_ 读取最后一条 LogMetaEntry 还原内存 LogMeta;接着 load redo log_storage;之后调 try_clear_up_holes_and_check_storage_integrity_ 补齐空洞;最后 integrity_verify_ 校验四种 meta/redo 组合的合法性。该函数对”meta 空 + redo 空”的场景返回 is_integrity=false,让上层 PalfEnvImpl 把目录挪走并从副本列表移除 —— 这种设计让运维误删场景下 LogEngine 不至于启动失败,而是被上层优雅回收。
int LogEngine::integrity_verify_(const LSN &last_meta_entry_start_lsn,
const LSN &last_group_entry_header_lsn,
bool &is_integrity)
{
int ret = OB_SUCCESS;
is_integrity = true;
bool meta_is_valid = last_meta_entry_start_lsn.is_valid();
bool redo_is_valid = last_group_entry_header_lsn.is_valid();
// 1. meta 空 redo 非空 → 致命错误
// 2. 双空 → 非完整性,挪走目录
// 3. meta 非空 redo 空 → 正常
// 4. 双非空 → 正常
if (false == meta_is_valid && true == redo_is_valid) {
ret = OB_ERR_UNEXPECTED;
} else if (false == meta_is_valid && false == redo_is_valid) {
is_integrity = false;
}
return ret;
}
高频入口:提交 redo 日志
协议层最常用的接口是 submit_flush_log_task,它把 FlushLogCbCtx 和 LogWriteBuf 打包成 LogIOFlushLogTask,由 log_io_worker_ 异步派发,主线程不阻塞等磁盘:
int LogEngine::submit_flush_log_task(const FlushLogCbCtx &flush_log_cb_ctx,
const LogWriteBuf &write_buf)
{
int ret = OB_SUCCESS;
LogIOFlushLogTask *flush_log_task = NULL;
if (IS_NOT_INIT) {
ret = OB_NOT_INIT;
} else if (false == flush_log_cb_ctx.is_valid() || false == write_buf.is_valid()) {
ret = OB_INVALID_ARGUMENT;
} else if (OB_FAIL(generate_flush_log_task_(flush_log_cb_ctx, write_buf, flush_log_task))) {
} else if (OB_FAIL(log_io_worker_->submit_io_task(flush_log_task))) {
}
if (OB_FAIL(ret) && OB_NOT_NULL(flush_log_task)) {
alloc_mgr_->free_log_io_flush_log_task(flush_log_task);
}
return ret;
}
对应的 append_log 是更底层的同步入口(writev 支持 vector write),二者配合实现了”高频派发 + 批量落盘”的写盘模式。任务执行到 block 切换时会回调 update_manifest,把新的 block_id 推给 meta_storage,从而维持 redo 与 meta 的 block 进度同步。
五类 meta 的统一提交模式
成员变更、配置变更、成员变更阶段、snapshot、副本属性这五类元数据落盘都走同一个模式:
- 拿 ObSpinLockGuard(log_meta_lock_),串行化所有 meta 操作;
- log_meta_.update_log_xxx_meta 修改内存对象;
- submit_flush_meta_task_ 内部走 log_io_worker_ 异步刷盘;
- 失败时(如 snapshot_meta)显式回滚到 prev_log_snapshot_meta,避免下次请求被错误状态干扰。
submit_purge_throttling_task 还在这之上加了节流:距上次成功 purge 小于 PURGE_THROTTLING_INTERVAL 且非强制类型时直接跳过,避免频繁 purge 抖动 IO;need_force_purge 让紧急恢复路径可以绕过节流。
任务工厂的复用模式
generate_flush_log_task_ 是所有”生成 IO 任务”函数的统一模板:alloc_mgr_ 多租户分配 → task 携带 palf_id_/palf_epoch_ → 失败时显式回收。这一模式被 generate_truncate_log_task_ / generate_truncate_prefix_blocks_task_ / generate_flashback_task_ / generate_purge_throttling_task_ / generate_fill_cache_task_ 完整复用,是 palf 模块的”任务工厂”通用模板,palf_id_/palf_epoch_ 在 epoch 切换时区分复用与重建。
小结
LogEngine 的设计核心是解耦:协议层与物理层之间通过异步 IO 任务对象隔离,五类元数据通过单锁聚合统一管理,redo 与 meta 分块管理便于独立调优。完整走通 init / load / submit_flush_log_task / update_manifest / 五类 meta 提交 / 任务工厂这六条主线,就能掌握 OceanBase 单机日志引擎的完整骨架,也是阅读更上层的 PalfHandleImpl 副本协议实现的基础。