基本概念
我们都知道 OceanBase 数据库的 Clog 日志类似于传统数据库的 Redo 日志,因此在分布式场景下需要多副本同步。而 Slog 不一样,Slog 可以理解为服务器的本地日志,是一台服务器上一些全局信息变更操作(如新增租户、分区创建和新增 SSTable 等)的 redo log。
一个服务器只拥有一个 Slog 写入流,也就是说同一台服务器上具有不同资源池的不同租户并不具有单独的 Slog 文件,所有租户的 Slog 写入请求最后都会汇入服务器所拥有的 Slog 文件中。
一条 Slog 日志记录的格式为:

从上图可以看出,Slog 分为三部分,其中每个部分可以理解为一个 log 块,具有递增的 log_seq 序号,保存在LogEntry 内(log 块的 header):
1. Logs,有效的 redo log 内容,可能包含多条实际的子 redo log,每条子 log 有子 log_seq(从 0 开始递增)。
2.NopLog,无意义的 log 内容,只是为了让整条记录 4k 对齐。
3.SwitchLog,只在一个 slog 文件的最后一条记录出现,包含 next file id,便于切换下一个 Slog 文件继续读取,同时也会包含 padding buffer 做对齐。
代码分析
接下来从代码层面展开分析 Slog。(由于 NopLog 和 SwtichLog 没有实际的 redo log 内容,因此这里不展开分析)
日志结构
每条 Slog 记录的 logs 部分(上图所示的第一部分 log 块)的格式如下:
ObLogEntry + n * log content
ObLogEntry 可以理解为整个 log 块的 header:
struct ObLogEntry {
ObRecordHeader header_;
uint64_t seq_; // 整个 log 的 log_seqint32_t cmd_; // 该 log 的类型,比如 OB_LOG_NOP 表示该 log 的类型为 NopLog// 具体见 enum LogCommand
}
struct ObRecordHeader {
int16_t magic_; // magic numberint16_t header_length_; // header lengthint16_t version_; // versionint16_t header_checksum_; // header checksumint64_t timestamp_; //int32_t data_length_; // length before compressint32_t data_zlength_; // length after compress, if without compresssion
// data_length_= data_zlength_int64_t data_checksum_; // record checksum
...
}
n * log content 在内存中由 ObStorageLogActiveTrans 结构进行组织管理,以序列化的形式共同存放在 log_buffer_:
struct ObStorageLogActiveTrans {enum common::LogCommand cmd_; // 等同于 ObLogEntry 中的 cmd_
int64_t log_count_; // log_buffer_ 中包含的 log 数量
common::ObLogCursor start_cursor_; // log_buffer_ 中第一条 log 刷盘的位置
// 在 log_buffer_ 刷盘时被赋值
ObStorageLogValidRecordEntry valid_record_;
ObBaseStorageLogBuffer log_buffer_; // 所有 log content 的 buffer
...
}
每个 log content 由一个 log header(ObBaseStorageLogHeader 结构)和实际的 log data 组成。
struct ObBaseStorageLogHeader {
...
int64_t trans_id_; // 事务 id,每次 Slog 的写入对应有一个事务 idint64_t log_seq_; // 每条 log content 在整个 logs 里的 seqint64_t subcmd_; // 32bit(main_type)+32bit(sub_type)
// main_type 表示日志与什么相关,sub_type 表示该日志的具体操作类型
// 如main_type = OB_REDO_LOG_PARTITION 表示日志与分区相关,具体见 ObRedoLogMainType
// 如sub_type = REDO_LOG_ADD_PARTITION 表示增加分区日志,每种 main_type 都有单独对应的 XXXRedoLogSubcmdint64_t log_len_;
uint64_t tenant_id_;
int64_t data_file_id_;
...
}
每次 Slog 的写入可以看做是一次事务,包括了 trans begin/n * trans/trans commit,而每个事务的子操作对应着一个 log content(包括 begin/commit)。
写入流程
1.ObBaseStorageLogger::begin 开启一次 Slog 写事务:
(1)从ObBaseStorageLogger 的 ObStorageActiveTrans 池中取出一个元素 trans_entry 用于本次 Slog 写事务。
(2)向 trans_entry 中 append 一条 begin 日志,即写入 trans_entry 的 log_buffer_。
2.多次调用 ObBaseStorageLogger::write_log:
(1)正常条件下每次调用都向 trans_entry 的 log_buffer_ append 一条 log content。
(2)当 trans_entry 的 log_buffer_ 达到上限(512-3*4-4 KB)时,对 log_buffer_ 里已有的 log 内容进行 flush。
(3)当 log 本身长度超出上限(512-3*4-4 KB)时,首先对 log_buffer_ 里已有的 log 内容进行 flush,然后扩大 trans_entry 的 log_buffer_,将超长 log append 到 log_buffer_,并再次 flush,最后恢复 trans_entry 的 log_buffer_ 大小。
3.ObBaseStorageLogger::commit 提交(结束)Slog 写事务:
(1)向 trans_entry 中 append 一条 begin 日志,即写入 trans_entry 的 log_buffer_。
(2)直接对 log_buffer_ 进行 flush。
flush 流程
1.ObBaseStorageLogger::flush_log 对一个 log_buffer_ 已满(或超长 log)的 ObStorageActiveTrans 进行刷盘操作:
(1)调用 Slog 写盘类 ObStorageLogWriter 的 flush_log 函数进行 Slog 落盘。
(2)首先从 ObStorageLogWriter 的 ObStorageLogItem(结构如下)池中取出一个元素 log_item,然后使用 log_buffer_ 构造 Logs 的 log 块(log_buffer_ 就对应了前文的 n*log content),并依次构造 NopLog 的 log 块以及 SwitchLog 的 log 块(如果有的话),最终构造成完整的一条 Slog 记录(最长为 32 MB)并填充到 log_item 的 buf_。
(3)log_item 构造完成后进行写盘操作(涉及到异步操作,这里不做介绍)。
class ObStorageLogItem : public common::ObIBaseLogItem {
...
bool is_inited_;
bool is_local_; // indicate whether buf_ is allocated locally or notint64_t buf_size_;
char* buf_;
int64_t len_;
common::ObThreadCond flush_cond_;
bool flush_finish_;
int flush_ret_;
}
flush 流程的核心是通过写流程中得到的 n * log content 构造出一条完整的 Slog 记录,通常可以直接用 ObStorageLogItem 结构指代。
示例

上图是 ob_admin 的 slog_tool 工具所显示的某 slog 文件的部分日志内容。
1.log_seq=1 的 redo log 共有三个 log content,其中每个 log content 都有自身的 sub_seq;
(1)每个 log content 的 command = 626(OB_LOG_TABLE_MGR),表明都是与 TableMgr 相关的 log。
(2)sub_seq=1 的子日志的 main_type = 3(OB_REDO_LOG_TABLE_MGR),表明是与 TableMgr 相关的 log,而 sub_type 在图中没有以值的方式标出,实际上其 sub_type = 8(REDO_LOG_COMPELTE_SSTABLE),也就是图中的 complete sstable 含义。
2.log_seq=2 的 log 为第一条 Slog 记录的 NopLog,因此图中并没有显示该 log,而是只显示了 log_seq=1 以及 log_seq=3 的 redo log。
优化版本
由于一次事务的所有 redo log 数据量可能很小,如果每一次事务都直接增加 noplog 构造 ObStorageLogItem 刷盘,空间浪费且带宽浪费,因此可以聚合多次事务,flush 时将多次事务的 redo log 添加到队列,真正异步写盘时将多次事务的 logs 聚合成新的 ObStorageLogItem,从而形成如下结构:

checkpoint
这里不介绍 checkpoint 的具体触发条件和执行流程,感兴趣的同学可以进一步阅读代码。
工作机制
为了加快恢复,Slog 提供 checkpoint 机制,生成 checkpoint 的过程也就是形成元数据宏块的过程,元数据宏块,也就是所谓的 Meta Block,会写入 block_file,Slog 同时会具有一个回放点(replay_start_point_),标明了 Slog 内尚未生成 checkpoint 的剩余日志的起始偏移位置。Meta Block 的入口点以及 Slog 的回放点都保存在 Super Block,想要了解更多相关内容可以阅读宏块或 OBServer 恢复的相关文章。
恢复
OBServer 对于 Slog 日志需要恢复的数据分为两部分,一部分是已经 checkpoint 形成 Meta Block 的基线元数据,另一部分是从 Slog 回放点开始的 Slog 日志,也就是增量元数据。




