共计 2829 个字符,预计需要花费 8 分钟才能阅读完成。
导读 | 我们在《大容量类 Redis 存储 — 有关 pika 的一切》里介绍过 pika 的诞生、pika 的特点、pika 的核心以及 pika 的使用。文章中非常详细的解析了 pika 同步逻辑中的重要文件:“write2file”的数据存储方式及实现原理,非常值得一看! |
pika 是 360 Web 平台部 DBA 与基础架构组合作开发的大容量类 Redis 存储,pika 的出现并不是为了替代 Redis,而是 Redis 的场景补充。pika 力求在完全兼容 Redis 协议、继承 Redis 便捷运维设计的前提下通过持久化存储的方式解决 Redis 在大容量场景下的问题,如恢复时间慢、主从同步代价高、单线程相对脆弱、承载数据较有限、内存成本高昂等。
binlog 相关的文件包含两部分: manifest 和 write2file,其中 manifest 记录了日志元信息,包括当前日志文件编号、当前日志文件偏移量,write2file+num 记录了 pika 接收到的所有 redis 写命令、参数。
manifest 文件格式:
日志偏移量 (8 字节)|con_offset(8 字节,未使用)| 元素个数 (4 字节,未使用)| 日志文件编号 (4 字节)。
Binlog 文件格式:
Binlog 文件固定大小为 100MB,每个 Binlog 文件由多个 Block 组成,每个 Block 大小固定为 64KB,每一个写 redis 命令称为一个 Record。一个 Record 可以分布在多个 Block 中,但只会分布在一个 Binlog 文件里,所以 Binlog 文件有可能大于 100MB。
Record 格式:Header|Cmd
Header: Record Length(3 字节)| 时间戳 (4 字节)| 记录类型 (1 字节)。
Cmd: redis 命令的一部分或者全部,取决于当前 Block 剩余空间是否可以存放该 Record。
基本类
Version: 元信息类,通过 mmap 与 manifest 文件映射。
Binlog: 日志类,通过 mmap 与 write2file 文件映射。
PikaBinlogSenderThread: 日志消费类,顺序读取日志文件内容,消费日志。
构造 Binlog
//file_size 可以在配置文件指定,默认为 100MB
Binlog::Binlog(const std::string& binlog_path, const int file_size)
1.1 创建 binlog 文件目录。
1.2 检查 log 目录下 manifest 文件是否存在,不存在则新建。
1.3 根据 manifest 文件初始化 Version 类。
1.4 根据 manifest 中的 filenum 找到对应的日志文件,根据 pro_offset 定位到文件 append 的位置,初始化日志指针、记录日志内容长度、Block 块数量。
更新当前日志生产状态
//pro_num: 日志文件编号
//pro_offset: 日志文件偏移量
// 用在需要全量同步时更新 slave 实例对应的 binlog 信息
Status Binlog::SetProducerStatus(uint32_t pro_num, uint64_t pro_offset)
2.1 删除 write2file0。
2.2 删除 write2file+pro_num。
2.3 构造新的 write2file+pro_num 文件,填充 pro_offset 个空格,初始化 version->pro_num 为 pro_num,version->pro_offset 为 pro_offset,并刷新到 manifest 文件中。
2.4 初始化当前 filesize、block_offset。
更新当前日志生产状态
//filenum: 当前日志编号
//pro_offset: 当前日志偏移量
Status Binlog::GetProducerStatus(uint32_t* filenum, uint64_t* pro_offset)
3.1 读取 version 中的 pro_num、pro_offset 并返回。
生产日志
//Put->Produce->EmitPhysicalRecord
Status Binlog::Put(const std::string &item)
4.1 检查当前日志文件是否满足切割条件,如果满足则进行切割。
4.1.1 pro_num 自增加 1,初始化新的日志文件,version->pro_num=pro_num,version->pro_offset = 0,binlog->filesize = 0,binlog->block_offset = 0。
4.1.2 如果当前 block 剩余大小 <kHeaderSize(8 字节),则填充剩余空间为’/x00″。
4.1.3 Produce 是一个循环,保证在 item 大小超过 kBlockSize 时,可以进行多次 EmitPhysicalRecord,完成 item 全部数据落入 binlog 文件,循环正常退出的条件是 left==0。
4.1.3.1 如果 left<avail,代表当前 block 可以存放完整的 item,则 type=kFullType,调用 EmitPhysicalRecord 一次,循环退出。
4.1.3.2 如果 left > avail,代表需要多个 Block 存放 item,则第一次 Type=kFirstType,调用 EmitPhysicalRecord 多次。
4.1.3.3 如果 left > avail, 且不是第一次 EmitPhysicalRecord,则 Type=kMiddleType,调用 EmitPhysicalRecord 多次。
4.1.4EmitPhysicalRecord。
4.1.4.1 拼接 RecordHeader(3 字节长度 + 4 字节时间 + 1 字节 Type),写入数据,更新 block_offset、pro_offset。
消费日志
//scratch: 消费结果返回一个完整的 redis cmd
//Consume->ReadPhysicalRecord,ReadPhysicalRecord 每次读取一个完整的 Record,多个 Record 构成一个完整的 redis cmd
Status PikaBinlogSenderThread::Consume(std::string &scratch)
5.1Consume 是一个循环,可能多次调用 ReadPhysicalRecord,循环退出的条件是读取到的 record_type==kFullType 或 record_type==kLastType。
5.1.1 如果读取到的 kBlockSize-last_record_offset_ <= kHeaderSize 代表读到了 Block 的末尾,且为填充数据,skip 掉。
5.1.2 读取数据,更新 last_record_offset_,con_offset。