上一篇文章我们讲了使用quorum机制选出新的primary,常用的做法可以借助带有quorum机制的第三方(zookeeper),还可以使用选举协议(paxos、raft)。给数据带上版本号,版本号是递增的。WARO是quorum机制的严格模式,实现数据强一致性,对于更新服务,副本只要有1个异常则更新服务不可用。读服务能容忍N-1个副本异常,只要有1个副本正常就可以提供读服务。
实际工程中NWR是宽松模式,中心节点(第三方zookeeper)读取R个副本,选择R个副本中版本号最高的为新的primary。还可以利用paxos等协议选出新的primary,每个节点以自己的版本号发起paxos提议,选出的新primary是某个超过半数副本中版本号最大的副本。
当机器宕机的时候,我们需要如何去恢复宕机之前那些数据?日志技术是宕机恢复的主要技术之一。日志技术最初使用在数据库系统中。严格来说日志技术不是一种分布式系统的技术,但在分布式系统的实践中,却广泛使用了日志技术做宕机恢复,甚至如BigTable等系统将日志保存到一个分布式系统中进一步增强了系统容错能力。
先简单介绍数据库系统中的日志技术,抽象简化问题模式,在简化模型的基础上介绍两种实用的日志技术Redo Log与No Redo/ No Undo Log。
数据库系统日志技术简述
在数据库系统中实现宕机恢复,其难点在于数据库操作需要满足ACID,尤其在支持事务的数据库系统中宕机往往发生在某些事务只执行了部分操作的时候。此时宕机恢复的主要目标就是数据库系统恢复到一个稳定可靠状态,消除未完成的事物对数据库状态的影响。
数据库日志主要分为Undo Log、Redo Log、Redo/Undo Log与No Redo/ No Undo Log。这四类日志的区别在更新日志文件和数据文件的时间点要求不同,从而造成性能和效率也不相同。可以参考有关数据库系统方面的资料去深入了解这四类日志技术。
Redo Log与Check point
问题模型:首先简化原数据库系统中的问题模型为一个较为简单的模型:假设需要涉及一个高速的单机查询系统,将数据全部存放在内存中以实现高速的数据查询,每次更新操作更新一小部分数据(列如key-value中的某一个key)。现在问题为利用日志技术实现该内存查询系统的宕机恢复。与数据库的事务不同的是,这个问题模型中的每个成功的更新操作都会生效。也等效为数据库的每个事务只有一个更新操作,且每次更新操作都可以也必须立即提交。
Redo Log:Redo Log是一种非常简单实用的日志技术。在上面的问题模型中,只需按照如下流程更新既可以使用Redo Log。
Redo Log更新流程
将更新操作的结果(例如set k1=1,则记录k1=1)以追加(append)的方式写入磁盘日志文件
按更新操作修改内存中的数据
返回更新成功
上述更新流程中第2步如果没有考虑修改内存数据需要多线程互斥等问题,对于说明Redo Log的原理没有影响。
从Redo Log的流程可以看出,Redo写入日志的内容是更新操作完成后的结果(这点是与Undo Log的区别之一),由于是顺序追加日志文件,在磁盘等对顺序写有力的存储设备上效率较高。
用Redo Log进行宕机恢复非常简单,只需要”回放”日志即可。
Redo Log的宕机恢复
从头读取日志文件中的每次更新操作的结果,用这些结果修改内存中的数据。
从Redo Log的宕机恢复流程也可以看出,只有写入日志文件的更新结果才能在宕机后恢复。这也是为什么在Redo Log流程中需要先更新日志文件再更新内存中的数据的原因。假如先更新内存中的数据,那么用户立刻就能读到更新后的数据,一旦在完成内存修改与写入日志直接发生宕机,那么最后一次更新操作无法恢复,但之前与困难已经读取到了更新后的数据,从而引起不一致的问题。
Check point
宕机恢复流量的缺点是需要回放所有redo日志,效率较低,假如需要恢复的操作非常多,那么这个宕机恢复过程将非常漫长。解决这一问题的方法引入了check point技术。在简化的模型下,check point技术的过程即将内存中的数据以某种易于重新加载的数据组织方式完整的dump到磁盘,从而减少宕机恢复时需要回放的日志数据。
check point流程
向日志文件中记录“begin check point”
将内存中的数据以某种易于重新加载的数据组织方式dump到磁盘上
像日志文件中记录“end check point”
在check point流程中,数据可以继续按照Redo Log更新流程走,这段过程中新更新的数据可以dump到磁盘也可以不dump到磁盘,具体取决于实现。例如,check point开始时k1=v1,check point过程中某次更新为k1=v2,那么dump到磁盘上的k1的值可以是v1也可以是v2。
check point的宕机恢复
将dump到磁盘的数据加载到内存
从后向前扫描日志文件,寻找最后一个“end check point”日志
从最后一个“end check point”日志向前找到最近的一个“begin check point”日志,并回放该日志之后的所有更新操作日志
上述的check point的方式依赖redo日志中记录的都是更新后的数据结果这一特征,即使dump的数据已经包含了某些操作的结果,重新回放这些操作的日志也不会造成数据错误。同一条日志可以重复回放的操作即所谓具有”幂等性”的操作。工程中,有些时候Redo日志无法具有幂等性,例如加法操作、append操作等。此时,dump的内存数据一定不能包括“begin check point”日志之后的操作。为此,有两种方法,其一是checkpoint的过程中停更新服务,不能进行新的操作,另一种方法是,设计一种支持快照(snapshot)的内存数据结构,可以快速的将内存生成快照,然后写入check point日志再dump快照数据。至于如何设计支持快照的内存数据结构,方式也很多,例如假设内存数据结构维护key-value值,那么可以使用哈希表数据结构,当做快照时,新建一个哈希表接收新的更新,原哈希表用于dump数据,此时内存存在两个哈希表,查询数据时查询两个哈希表并合并结果。
No Undo/ No Redo Log
介绍另一种特殊的日志技术“No Undo/ No Redo Log”,这种技术也称之为“0/1目录”(0/1 directory) 假设另一种问题场景:若数据维护在磁盘中,某批更新由若干个更新操作组成,这些更新操作需要原子生效,即要么同时生效,要么都不生效。
0/1目录技术中有两个目录结构,称为目录0和目录1。另有一个结构称为主记录(master record)记录当前正在使用的目录称为活动目录。主记录中要么记录使用目录0,要么记录使用目录1。目录0或者目录1记录了各个数据在日志文件中的位置。
图中给出了一个0/1目录的例子。活动目录为目录1,数据有A、B、C三项。查目录1可得A、B、C三项的值分别为2、5、2。0/1目录的数据更新过程始终在非活动目录上进行,只是在数据生效前。将主记录中的0、1值反转,从而切换主记录。
0/1目录数据更新流程
将活动目录完整拷贝到非活动目录
对于每个更新操纵,新建一个日志项记录操作后的值,并在非活动目录中将相应数据的位置修改为新建的日志项的位置
原子性修改主记录:反转主记录中的值,使得非活动目录生效
0/1目录的更新流程很简单,通过0、1目录的主记录切换使得一批修改的生效是原子的。
0/1目录将批量事务操作的原子性通过目录手段归结到主记录的原子切换。由于多条记录的原子修改一般较难实现而单条记录的原子修改往往可以实现,从而降低了问题实现的难度。在工程中0/1目录思想运用非常广泛,其形式也不局限在上面的流程中,可以是内存中两个数据结构的来回切换,也可以是磁盘上的两个文件目录的来回生效切换。
工程投影
日志技术的使用非常广泛,在zookeeper系统中,为了实现高效的塑胶访问,数据完全保存在内存中,但更新操作的日志不断持久化到磁盘,另一方面,为了实现较快速的宕机恢复,zookeeper周期性的将内存数据以checkpoint的方式dump到磁盘。
MySQL的主从库设计也是基于日志。从库只需通过回放主库的日志,就可以实现与主库的同步。由于从库同步的速度与主库更新的速度没有强约束,这种方式只能实现最终一致性。
下一篇文章我们将开始进行阶段性的总结回顾。
参考资料:《分布式系统原理介绍》作者:刘杰
作者:井底知蛙,多年架构经验,倾囊相授
个人微信公众号:井底倁蛙(id:upgrade366)
(6)工程投影
技术与生活,微服务架构,分享交流,不做井底之蛙。
长按二维码即可关注我
求好看|求转发