浅谈分布式系统与一致性协议(一)
浅谈分布式系统与一致性协议(二)
浅谈分布式系统与一致性协议(三)
深入浅出之etcd
深入浅出之etcd(二)
etcd版本之v3
etcd之安全性阐述
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。两个目录的作用说明如下:
故障快速恢复:如果你的数据遭到颇快,就可以通过执行所有WAL中记录的修改操作,快速从原始的数据恢复到数据损坏之前的状态
数据回滚(undo)/重做(redo):因为所有的修改操作都被记录在WAL中,所以进行回滚或者重做时,只需要反响或者正向执行日志即可
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管理所有的更新日志,主要处理日志的追加,日志文件的切换,日志的回放等操作
etcd所有的日志项最终都会被追加存储到WAL文件中,日志项有很多类型,具体如下:
每个日志项都由四部分组成:
etcd的WAL库提供了初始化方法,应用需要显示调用初始化方法来完成日志初始化的功能,初始化方法主要包括两个函数Create()与Open()
Create()所做的事情比较简单,具体如下:
日志项的追加通过调用etcd的wal库的Save()方法来实现,该函数的核心内容具体如下:
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协议每获取到日志项之后,在处理该日志的过程中就会判断是否创建快照。创建快照的具体包含如下几个步骤:
对于执行快照创建的时机进行判断时,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)
}
持久化存储具体包括以下几个内容