前言
之前讲 Raft 协议的提过,用户提交写请求之后,etcd 会将用户请求打包成一个 proposal,交给 raft 模块。raft 模块会将 proposal 打包成一条日志条目,之后会将该日志条目会发送给其他 follower 节点,同时也会将日志条目持久话存储到 WAL 日志中。因此,本文主要介绍 WAL 日志。
WAL 文件
WAL 文件名格式
WAL 文件的文件名格式为:seq-index.wal。其中:
- seq:序列号,从 0 开始递增。
- index:该 WAL 文件存储的第一条日志的索引。因此给定一个日志索引后,很快就能知道该索引的日志落在哪个 WAL 文件之中的。
WAL 文件内容
WAL 文件由多个 WAL 日志记录顺序追加写入组成,如下图所示。每条记录由以下三部分组成,
- type:表示记录类型,目前支持元数据、日志数据、状态数据、校验初始值、快照数据 5 种。
- crc:data 中的 crc32 校验码。
- data:数据部分,存对应类型的数据。
message Record {
optional int64 type = 1 [(gogoproto.nullable) = false];
optional uint32 crc = 2 [(gogoproto.nullable) = false];
optional bytes data = 3;
}
元数据
-
对于 Etcd 来说,元数据中的数据中包含了节点 ID、集群 ID 信息。Etcd 是在创建 WAL 文件的时候,就写入了元数据。
metadata := pbutil.MustMarshal( &pb.Metadata{ NodeID: uint64(member.ID), ClusterID: uint64(cl.ID()), }, ) if w, err = wal.Create(cfg.WALDir(), metadata); err != nil { plog.Fatalf("create wal error: %v", err) }
-
一个服务中如果有多个 WAL 文件,且这些文件中有多份元数据,那么这些元数据都必须一致,否则报错。
日志数据
日志数据包含了 leader term、日志条目的索引、日志类型(普通的命令日志、集群配置变更日志)、proposal 内容等内容。
message Entry {
optional uint64 Term = 2 [(gogoproto.nullable) = false]; // must be 64-bit aligned for atomic operations
optional uint64 Index = 3 [(gogoproto.nullable) = false]; // must be 64-bit aligned for atomic operations
optional EntryType Type = 1 [(gogoproto.nullable) = false];
optional bytes Data = 4;
}
状态数据
保存当前“硬状态(HardState)”的记录:当前任期号、当前给哪个节点 ID 投票、当前已提交(commited)的最大日志条目索引。一个 WAL 日志文件会有多条状态数据,但是以最后的记录为准。
message HardState {
optional uint64 term = 1 [(gogoproto.nullable) = false];
optional uint64 vote = 2 [(gogoproto.nullable) = false];
optional uint64 commit = 3 [(gogoproto.nullable) = false];
}
校验初始值
为上一个 WAL 文件的最后一条日志数据的 CRC 信息。在创建、切割 WAL 日志文件时,作为第一条记录写入到新的 WAL 文件,用于校验数据文件的完整性、准确性,如下所示。
- 每个 WAL 文件必须有校验初始值类型的数据,后续所有写入该 WAL 文件的记录,都使用该初始值来计算 CRC 校验值。
- 校验初始值位于 WAL 文件中第一条日志数据的前面,这样才能确保后续基于它计算出来的该日志数据的 crc 校验码正确。
- 第一个 WAL 文件,即序列号为 0 的 WAL 文件,其校验初始值为 0。
- 之后的 WAL 文件,以上一个 WAL 文件的最后一条日志数据的 CRC 校验码来做为该文件的校验初始值。
快照数据
存储的是当前快照的索引和任期号,而快照的详细数据都存在快照数据文件中。
snapshot 文件
etcd-raft 使用日志在集群节点之间进行状态同步,通过日志复制保持集群状态一致。节点可以通过重新顺序执行日志将节点的状态复原,此时日志相当于状态变化的记录。
如果将节点当前的状态保存下来,也就是对当前的状态进行快照的话,那么该时间点之前的日志其实都可以删除,因为这些日志最终执行的效果已经被保存下来了,所以之前的日志记录都可以删除了。
快照的使用有以下优势:
- 控制日志的增长。维护集群状态一致性的日志随时间变得越来越长。日志存储了集群中每个状态改变的历史记录,随着更多的状态变更(如键值对更新)被记录,日志会不断增长。长期增长的日志会消耗大量的存储空间。定期创建快照可以删除已经不再需要的老旧日志项。
- 加快数据恢复和新节点加入的速度。数据恢复和新节点的加入可以在快照的基础之上,应用快照之后的日志部分,大大减少了时间和资源开销。
- 数据迁移和备份:快照文件提供了一种方便的数据迁移和备份手段。它是一个独立的数据副本,可以轻松地移到其他存储介质或环境中。这也为灾备策略提供了重要支持。
- 逻辑上的数据隔离和一致性保障:快照是系统在某一时刻的全局一致视图,不同于 boltdb,boltdb 可能只包含部分数据更新的状态。这提供了逻辑上的隔离,保证了快照中数据的完整性和一致性。
snapshot 文件名格式
snapshot 文件的文件名格式为:任期号-索引号.snap。任期号和索引号就是上述 WAL 中快照数据的内容,同时也是下面 snapshot 文件中 metadata 的内容。
snapshot 文件内容
文件中存储快照数据的格式为:校验值及其快照数据。
message snapshot {
optional uint32 crc = 1 [(gogoproto.nullable) = false];
optional bytes data = 2;
}
其中快照数据(data 字段)的具体格式由存储快照数据的使用方来解释。在 etcd 中,上述 data 字段中的实际数据格式为 Snapshot,为:
- data:v2store json 序列化后的数据。其中 v2store 是 key-value 在内存中的存储结构,相当于序列化了 key-value 数据。
- metadata:index 为已 applied 的日志的最大索引,term 为已 applied 的日志的 term,conf_state 集群节点信息。
message Snapshot {
optional bytes data = 1;
optional SnapshotMetadata metadata = 2 [(gogoproto.nullable) = false];
}
message SnapshotMetadata {
optional ConfState conf_state = 1 [(gogoproto.nullable) = false];
optional uint64 index = 2 [(gogoproto.nullable) = false];
optional uint64 term = 3 [(gogoproto.nullable) = false];
}
snapshot 创建流程
-
触发快照: 快照创建可以通过几种方式触发:
- 自动触发:etcd 定期(根据配置项)自动生成快照。
- 手动触发:可以手动发送请求给 etcd 以创建快照。
- 基于日志大小:当日志达到配置的阈值大小时,etcd 会自动触发一次快照。
-
创建快照文件:冻结 boltdb 并从其中读取内容来生成快照内容,同时创建快照文件,将快照内容写入。生成的快照内容包含了能够重建 boltdb 的所有信息,包括键值数据和必要的元数据,如快照时的索引和任期(term)等。
-
清理日志:对内存和 WAL 文件中的日志进行 compact。
Etcd 快照相关配置
主要两个配置参数:
- snapshot-count:表示执行多少条日志之后才能进行快照。
- snapshot-catchup-entries:一个新的或落后的成员在请求发送快照以加快追赶之前,可以接受多少条日志条目。
相关 Etcd 配置可参考:https://doczhcn.gitbook.io/etcd/index/index-1/configuration
相关链接
etcd-raft snapshot实现分析:https://zhuanlan.zhihu.com/p/29865583
Etcd Raft库的日志存储:https://www.codedump.info/post/20210628-etcd-wal/