在分布式系统中,宕机是需要考虑的重要组成部分。日志技术是宕机恢复的重要技术之一。日志技术应用广泛,早些更是广泛应用在数据库设计实现中。本文先介绍基本原理概念,最后通过redis介绍生产环境中的实现方法。
数据库设计中,需要满足ACID,尤其是在支持事务的系统中。当系统遇到未知错误时,可以恢复到一个稳定可靠的状态。有一个很简单的思路,就是记录所有对数据库的写操作日志。那么一旦发生故障,即使丢失掉内存中所有数据,当下一次启动时,通过复现已经记录的数据库写操作日志,依然可以回到故障之前的状态(如果在写操作作日志的时候发生故障,那么这次数据库操作失败)。
操作流程简单如下(假设每次数据变化,都提交):
恢复流程:
优点:
缺点:
先具体化下,如果我们内存中保留一个a的值,记录了写操作比如 a = 4; a++; a--;
当这些操作上千万、亿之后,恢复非常慢。甚至可能最后一条就是a=0
,按照之前的算法,我们却跑了很长时间。
那么根据这个场景,很容易想到一个解决方案。
操作流程:
begin check point
end check point
恢复流程:
end check point
中配对的begin check point
。优点:
一些棘手的问题:
在做snapshot的时候,往往不能停止数据库的服务,那么很可能记录了begin check point
之后的日志。那么在重新load begin check point
之后的日志时,最后恢复的数据很有可能不对。比如我们记录的是a++
这样的日志, 那么重复一条日志,就会让a的值加1。反之如果我们记录是幂等的,比如一直是 a=5
这种操作,那么就对最后结果没有影响。很显然,设计幂等操作系统很麻烦。
设计一个支持snapshot的内存数据结构,也比较麻烦。
典型的是通过copy-on-write机制。和操作系统中的概念一样。当这个数据结构被修改,就创建一份真正的copy。老数据增加一份dirty flag。如果没有修改就继续使用之前的内存。这样在做snapshot的时候,保证我们的dump数据是begin check point
这个时刻的数据。显然这个也比较麻烦。
还有一种支持snapshot的思路是begin check point
后,不动老的数据。内存中的数据在新的地方,日志也写在新的地方。最后在end check point
做一次merge。这个实现起来简单,但是内存消耗不小。
Redis 是一个基于内存的database,不同于memcached,他支持持久化。另外由于redis处理client request 和 response 都是在一个thread里面,也没有抢占式的调度系统,核心业务都是按照event loop顺序执行,而磁盘写日志又开销很大,所以redis实现日志功能做了很多优化。并且提供2种持久化方案。我们需要在不同的场景下,采用不同的方式配置。
某个时刻,redis会把内存中的所有数据snapshot到磁盘文件。更通俗的说法是fork一个child process,把内存中的数据序列化到临时文件,然后在main event loop 中原子的更换文件名。redis,利用了操作系统VM的copy-on-write机制,在不阻塞主线程的情况下,利用子进程和父进程共享的data segment实现snapshot。具体是代码实现在rdb.c
, function at rdbSaveBackground
优点:
缺点:
为了解决上面的问题,redis增加了AOF。
在database术语中,也被叫做WAL。如果开启的AOF的配置,redis会记录所有写操作到日志文件中。那么redis同样会遇到之前我们提到过的问题。
+1
日志。那么redis是如何解决的呢?
rewriteAppendOnlyFile
来处理AOF文件过大情况。前面我们知道了,这种check point
的机制还是比较麻烦的。那么redis是这么设计的。
优点:
这些都是性能和稳定性之间做的权衡,根据不同场景需要调整。