etcd之⽇志和快照管理
系列⽂章⽬录
⽬录
概述
etcd对数据的持久化采⽤的是binlog(⽇志,也称为WAL,即Write-Ahead-Log)加Snapshot(快照)的⽅式
在计算机科学中,预写式⽇志(Write-Ahead-Log,WAL)是关系数据库系统中⽤于提供原⼦性和持久性(ACID中的两个特性)的⼀系列技术。在使⽤WAL系统中,所有修改在提交之前都要写⼊log⽂件中
log⽂件中通常包括redo信息和undo信息。假设⼀个程序在执⾏某些操作过程中机器掉电了。在重新启动时,程序可能需要直到当时执⾏得操作是完全成功了还是部分成功或者完全失败。如果使⽤了WAL,那么程序就可以检查log⽂件,并对突然掉电时计划执⾏的操作内容与实际上执⾏的操作内容进⾏⽐较。在这个⽐较的基础上,程序就可以决定是撤销已做的还是继续完成已做的操作,或者保持原样
WAL允许⽤in-space的⽅式更新数据库。另⼀种⽤来实现原⼦更新的⽅法是shadow paging,它并不是
⼀种in-place⽅式。⽤in-place⽅式进⾏更新的主要有点是减少索引和块列表的修改。ARIES是WAL系列技术常⽤的算法。在⽂件系统中,WAL通常称为journaling。PostgreSQL也是⽤WAL来提供oint-in-time恢复和数据库复制特性的
etcd数据库的所有更新操作都需要先写⼊到binlog中,⽽binlog是实时写到磁盘上的,因此这样就可以保证不会丢失数据,即使机器断电,重新启动以后etcd也能通过读取并重放binlog⾥⾯的操作记录来重新建⽴数据库
etcd数据的⾼可⽤性和⼀致性是通过Raft算法实现的,Master节点会通过Raft协议向Slave节点复制binlog,Slave节点根据binlog对操作进⾏重放,以维持数据的多个副本的⼀致性。也就是说binlog不仅仅是实现数据库持久化的⼀种⼿段,其实还是实现不同副本间⼀致性协议的重要⼿段。客户端对数据库发起所有写操作都会记录在binlog中,待主节点将更新⽇志在集多数节点之间完成同步后,以便内存中的数据库中应⽤该⽇志项的内容,进⽽完成⼀次客户端的写请求
数据的持久化和复制
先看个例⼦。例如,通过以下命令向etcd中插⼊⼀个键值对
etcdctl set /foo bar
etcd会在默认的⼯作⽬录下⽣成两个⼦⽬录:snap和wal。两个⽬录的作⽤说明如下:
snap:⽤于存放快照数据。etcd为了防⽌WAL⽂件过多就会创建快照,snap⽤于存储etcd的快照数据状态
wal:⽤于存放预写式⽇志,其最⼤的作⽤是记录整个数据变化的全部历程。在etcd中,所有数据的修改在提交之前,都要写⼊WAL 中。使⽤WAL进⾏数据的存储使得etcd拥有故障快速恢复和数据回滚两个重要的功能
故障快速恢复:如果你的数据遭到颇快,就可以通过执⾏所有WAL中记录的修改操作,快速从原始的数据恢复到数据损坏之前的状态
数据回滚(undo)/重做(redo):因为所有的修改操作都被记录在WAL中,所以进⾏回滚或者重做时,只需要反响或者正向执⾏⽇志即可
etcd的⽇志管理
etcd提供了⼀个WAL⽇志库,⽇志追加等功能均有该库完成。下⾯让我们先看⼀下WAL数据结构定义
WAL数据结构
WAL数据结构定义如下:
type WAL struct{
dir string
dirFile *os.File
metadata []byte
state raftpb.HardState
start walpb.Snapshot
decoder *decoder
readClose func()error
mu sync.Mutex
enti uint64
encoder *encoder
locks []*fileutil.LockedFile
fp *filePipeline
}
WAL管理所有的更新⽇志,主要处理⽇志的追加,⽇志⽂件的切换,⽇志的回放等操作
WAL⽂件物理格式
etcd所有的⽇志项最终都会被追加存储到WAL⽂件中,⽇志项有很多类型,具体如下:
metadataType :这是⼀个特殊的⽇志项,被写在每个WAL⽂件的头部
entryType:应⽤的更新数据,也是⽇志中存储的最关键数据
stateType:代表⽇志项中存储的内容时快照
crcType:前⼀个WAL⽂件⾥⾯的数据的crc,也是WAL⽂件的第⼀个记录项 snapshotType:当前快照
的索引{term,index},即当前快照位于哪个⽇志记录,不同于stateType,这⾥只是记录快照的索引,⽽⾮快照的数据。
每个⽇志项都由四部分组成:
type:⽇志项类
crc:校验和
data:根据⽇志项类型存储的实际数据也不尽相同,如果snapshotType类型的⽇志项存储的是快照的⽇志索引,crcType类型的⽇志项中则⽆数据项,其crc字段便充当了数据项
padding:为了保持数据项8⼦节对其⽽填充的数据
WAL⽂件的初始化
etcd的WAL库提供了初始化⽅法,应⽤需要显⽰调⽤初始化⽅法来完成⽇志初始化的功能,初始化⽅法主要包括两个函数Create()与Open()
Create()所做的事情⽐较简单,具体如下:
1. 创建WAL⽬录,⽤于存储WAL⽇志⽂件
2. 预分配第⼀个WAL⽇志⽂件,默认是64MB,使⽤预分配机制可以提⾼写⼊性能
3. Open则是在Create完成后被调⽤,主要是打开WAL⽬录下的⽇志⽂件
4. Open的主要作⽤是到当前快照以后的所有WAL⽇志,这是因为快照之前的⽇志我们不再关⼼,因为⽇志的内容肯定都已经被更新
⾄快照了,这些⽇志也是在后⾯回收⽇志操作中可以被删除的部分
WAL追加⽇志项
⽇志项的追加通过调⽤etcd的wal库的Save()⽅法来实现,该函数的核⼼内容具体如下:
调⽤saveEntry()将⽇志项存储到WAL⽂件中
如果追加后⽇志⽂件超过了既定的SegmentSizeBytes⼤⼩,则需要调⽤w.cut()进⾏WAL⽂件的切换,即关闭当前WAL⽇志,创建新的WAL⽇志,继续⽤于⽇志追加
cut()的⽬的⽤于实现WAL⽂件切换的功能,每个WAL⽂件的预设⼤⼩均为64MB,⼀旦超过该⼤⼩,
便会创建新的WAL⽂件,这样做的好处便是对旧的WAL⽂件进⾏删除
etcd v2的快照管理
etcd v2是⼀个纯内存数据库,写操作先通过Raft协议复制binlog,复制成功后将数据写⼊到内存中,整个数据库在内存中是⼀个简单的树结构,其轻微将数据实时写⼊到磁盘中,持久化考的是binlog和定期做快照实现的,总的俩讲,etcd v2做快照的⽅法就是将内存中的整个数据库复制⼀份,然后序列化成JSON,写⼊到磁盘中,称为快照。做快照的时候使⽤的是复制出来的数据库,客户端的读写请求依然会落到原始的数据库,也就是说做快照的操作不会阻塞客户端的读写请求
因为操作系统对内存进⾏了分页,同时内存的复制操作实际是COW的,所以只有当复制的某⼀个内存页发⽣更改时才会发⽣复制⾏为,即只有那些被客户端读到的数据也才会在内存中被复制,那些没有读到的压根不会发⽣复制。
快照数据结构如下:
type ConfState struct{
Nodes []uint64
提交更改是内存条吗}
type SnapshotMetadata struct{
ConsState ConfState
Index uint64
Term uint64
}
type Snapshot struct{
Data []byte
Metadata SnapshotMetadata
}
创建快照
创建快照的时机时在请求的处理的流程之中,具体来说,Raft协议每获取到⽇志项之后,在处理该⽇志的过程中就会判断是否创建快照。创建快照的具体包含如下⼏个步骤:
1. 判断是否传创建快照,该过程有⼀定的代价,因此不会每次都执⾏。
2. 创建快照,由应⽤实现具体的创建⽅法
3. 通过raftStorage创建快照
4. 存储⽇志
5. 进⾏⽇志回收(compact)
对于执⾏快照创建的时机进⾏判断时,etcd采⽤较为简单的策略:每处理10000条⽇志进⾏⼀此快照
创建快照的的操作代码如下,直接将内存中的数据库复制⼀份转换成JSON即可
func(s *kvstore)getSnapshot()([]byte,error){
s.mu.Lock()
defers.mu.Unlock()
return json.Marshal(s.kvStore)
}
拿到整个数据库快照之后,还要添加⼀些metadata,⽐如该快照的版本号等,然后就可以将快照进⾏持久化了
func(rc *raftNode)saveSnap(snap raftpb.Snapshot)error{
walSnap:=walpb.Snapshot{
Index:=snap.Metadata.Index,
Term:=snap.Metadata.Term,
}
if erro:=rc.wal.SaveSnapshot(WalSnap);err!=nil{
return err
}
if err:=rc.snapshotter.SaveSnap(snap);err!=nil{
return err
}
return rc.wal.ReleaseLockTo(snap.Metadata.Index)
}
持久化存储具体包括以下⼏个内容
快照索引:即当前快照的其实⽇志项索引信息(term/index),该信息被存储在WAL⽇志⽂件所在⽬录快照数据:即快照的真正数据

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。