有状态高可用:状态存储引擎
状态存储引擎
有状态服务的难点在于如何在故障后或者维护中,能恢复状态从而继续提供服务。通过状态存储引擎实现
注意:最佳办法还是保存状态数据保存到Redis,MongDB等NOSQL中。这是把有状态服务改成无状态服务方法。如果客户端能自动上报状态则无需这种集中式保存状态方式,比如设备连接的设备网关,可以上报自己的产品等信息。
有状态服务可以持华还状态到磁盘或者云存储上,以方便宕机后恢复,本章将介绍持久化通常的方式,这使用的技术跟Redis,ZK,以及其他Key—Value 数据库一样。持久化和下一节提到复制都是分布式核心技术。
下图展示了一个具备容灾有状态服务通常架构。包含如下组件

- 预写日志WAL: 客户端发送到有状态节点的每条命令,实现了状态修改,命令还会按照顺序序列化到文件系统里。如果系统重启,可以重放预写日志的命令从而恢复数据
- 如果只使用WAL恢复数据,随着WAL记录增多,恢复较慢。像Redis,Zookeeper等会按照一定策略把有状态数据序列化到文件系统。因此在系统恢复时候,将快照恢复成有状态数据,生成快照后的那些WAL记录也会重放
- 有状态数据,可以是任何Key-Value结构,或者像Zookeeper那样ZNode
- 存储引擎:用于查询或者更改状态数据,存储引擎负责预写日志和定时生成快照。
- 复制:备用节点将从主节点复制数据。通常策略是请求主节点的一份快照和快照生成后续的预写日志。备用节点后续将从主节点得到发送给主节点的命令以保持数据同步。
补充状态机的概念
10.4.1 预写日志
在有状态服务更改内存的状态数据后,会使用预写日志(Write-Ahead Logging,WAL)记录了客户端发出更改数据的命令后。WAL采用的是文件顺序写,因此有着非常高的磁盘效率。
WAL的主要作用是故障恢复,通过重放WAL的所有命令恢复系统,以Redis为例子,WAL称为AOF文件,修改命令将保存到此文件,如下命令
set name xiandafu
del nameAOF中的保存了如下文本内容
*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$8\r\nxiandafu\r\n
*2\r\n$3\r\nDEL\r\n$4\r\nname\r\nRedis 使用RESP协议,如上del name 命令,包含了2个参数,参数之间使用
\r\n隔开。每个参数包含了长度和内容,比如del对应的内容是3\r\nDEL\r\n
本章LSM存储引擎,一个用于实现高速写的Key—Value数据引擎,它不支持del命令,但支持set命令赋予空值表示删除
set name xiandafu
set name其WAL的格式跟Redis类似,参数之间使用|, 命令之间使用\r\n,比如
set|name|xiandafu|时间戳\r\nset|name||时间戳Zookeeper 的WAL 称为事务日志 txnlog,使用类FileTxnLog保存客户端的每次的命令,其命令是Record的子类,比如SetDataRequest,DeleteRequest。WAL的格式包含文件头,以及若干有校验码的事务记录。每条事务记录格式如下
| 序号 | 字段 | 长度 | 描述 |
|---|---|---|---|
| 1 | header | 16字节 | 包含了4字节魔数,4字节版本以及8字节预留 |
| 2 | txn | 包含了多条txn记录 |
每条txn记录格式如下
| 序号 | 字段 | 长度 | 内容 |
|---|---|---|---|
| 1 | checksum | 8 | 后续内容的校验,使用Adler-32 |
| 2 | txlen | 4 | 后续内容的长度 |
| 3 | TxnHeader | 28 | 包含了命令类型、客户端sessionId、zxid等。 |
| 4 | Record | 命令序列化的内容,比如create ``/lock 命令是CreateRequest ,此类是Record的子类,实现了序列化和反序列化功能。 | |
| 5 | marker | 1 | 固定位0x42,结束标记 |
zookeeper的txnlog 是在多个节点协商一致成功后才记录到磁盘上。关于分布式一致,参考10.8 共识
追加命令到操作系统文件末尾拥有高效率,但何时同步到物理磁盘设备上,也影响效率。需要权衡
- 如果每追加一条命令就要同步到磁盘,考虑到磁盘的性能,服务的性能将下降。
- 依赖操作系统,由操作系统决定何时把内容同步到到磁盘上。
- 定时一批命令同步到磁盘,如果机器故障,可能导致命令丢失
Zookeeper默认采用的是前者,确保数据落盘。Redis则默认采用定时1秒调用一次同步到磁盘设备,这样Redis最多会丢失1秒的数据
10.4.4 快照
随着WAL的增长,使得使用WAL恢复需要较长时间,并且WAL里记录了一些可以合并执行的命令,比如Redis中,先后执行了
set name xiandafu
set name joelli实际上合并的WAL保留第二条即可。为了快速故障恢复,直接将内存的状态数据生成一份二进制快照。通过快照(Check Point或者Snapshot)以及新的WAL来恢复数据。

Redis中,快照保存在RDB文件中,格式如下
| 顺序 | 字段名 | 长度 | 内容 |
|---|---|---|---|
| 1 | 魔数 | 5 | 固定为REDIS |
| 2 | 版本号 | 4 | RDB的版本号,如“0012” |
| 3 | 数据库0 | 每个数据库段以 0xFE和数据库编号开头,随后是键值对。每个键值对的结构为 [可选过期时间] [类型字节] [键] [值],类型字节标识 String、List、Hash 等不同编码方式。 | |
| 5 | 数据库1 | 可选,如果还有数据库1 | |
| 6 | 数据库N | 可选,如果还有数据库N | |
| 6 | EOF | 1 | 结束标记,内容为0XFF |
| 7 | CRC64 | 8 | 校验和 |
Zookeeper的快照是在写入的事务日志txnlog数量超过一个随机阈值,或者日志的长度超过一定随机阈值后开始生成快照,这里采用随机数是避免多个节点同时生成快照文件造成造成集群整体此刻吞吐量下降
快照时机参考 SyncRequestProcessor.shouldSnapshot,FileSnap.serialize实现了生成快照
其快照格式如下
| 顺序 | 字段名 | 长度 | 内容 |
|---|---|---|---|
| 1 | 文件头 | 16 | 包含了4字节魔数,4字节版本以及8字节预留 |
| 2 | DataTree | 整棵zk树,所有znode,包含了path,data,acl等信息 | |
| 3 | sessionsMap | 会话信息,超时时间 |
服务生成快照的时候,同时也会有新的命令传入。如果此时停止命令执行,则会影响服务吞吐量。Redis采用的是COW(Copy-on-Write),不需要暂停服务。需要快照时候,会Fork出一个子进程,内存数据只读以用于快照生成。如果有对内存的数据某一个key操作,此key所在的内存Page将会复制一份并被Redis主进程持有,并执行命令。当复制结束后,子进程结束,主进程会销毁没有被引用的旧页。如下是快照就是生成那一个的内存数据

如上图,Redis内存有俩个Page,分别是PageA和PageB,每页都包含Key—Value数据。当要生成快照的时候,整个内存被标记为只读以供子进程使用。当主进程收到对PageA中的name进行修改时候,PageA将会复制出一个新的PageA-new,连同name和PageA中的其他数据一起复制。主进程将修改PageA-new的内容并将引用指向pageA-new. 最后,主进程的内存是pageA-new和pageB。
对于Zookeeper来说,它同样不需要暂停服务,它的原理与Redis不同。它会直接序列化DataTree到文件系统,后续的命令执行对DataTree的的数据部分改也会生成在快照里。当故障恢复时候,需要联合快照和WAL。因为Zookeeper的命令实现了幂等(这不同于Redis,比如Redis的自增命令无法幂等),因此在快照基础上,重复WAL中的命令,也能正确恢复数据
下一节将介绍一个简化版的LSM-Tree存储引擎,也采用COW技术提高性能
