MyException - 我的异常网
当前位置:我的异常网» 数据库 » redis 持久化懂得

redis 持久化懂得

www.MyException.Cn  网友分享于:2013-09-03  浏览:8次
redis 持久化理解

快照方式(Snapshotting)
作者将这种持久化方式称为point-in-time, 即它并不能保证每个时刻内存中的数据集与磁盘上的二进制文件是完全一样的,但可以保证磁盘上的二进制文件与系统内存中某个时刻(最近一次fork时刻)的数据是完全一样的。若以时间为横轴,且每个fork时刻用Delta函数来描述,你会看到很多脉冲式的图像。当然,若某次持久化失败,那么相应的Delta函数也应该删除掉。在每次fork的时刻,系统都会把脏数据的数目清零(若持久化成功的话,是这样的;若失败的话,在失败后需要将当前脏数据的数目加上持久化前的数值作为新的脏数据数目),也同时将该时刻作为新的零时刻来计算,上述说明会直接影响到参数设置的理解。

若 fork的时刻是t1, 相应的持久化成功结束的时刻是t2,  那么t2-t1是该次持久化需要的时间。每次持久化过程需要的时间是与系统数据集的大小成正比的,那么典型的数值是什么呢?同时,在加载的时候,需要的时间是多少呢?现在我还没有太多数据做测试,以后慢慢积累这些有价值的经验数据了。
 
参数设置
Save 60 10000
Save 300 10
Save 900 1
根据上述解释,所有的参数设置只有在两个Delta函数之间才是有意义的,即两类计数都不会跨越脉冲/Delta函数。参数的意义是,若每六十秒内,有一万或以上个键的值改变过(包含新创建或删除的键值对),则启动持久化过程;若连续五次都不满足此要求,就会同时进行后面的判断,即若每五分钟内有十或以上个键的值改变过,则启动持久化过程;依次类推。采取类似的设置,我估计原因可能是:在实际中,单位时间键值的改变数量,随时间的分布是随机的,有高有低,需要使用不同频率来匹配各种情形(好似用网捉鱼,用最大网眼的网快速扫,同时用中等网眼的网以一般速度扫,再同时用非常密的网来慢慢扫,比喻也不太恰当,呵呵)。只要数据有任何改变,此设置保证15分钟内系统会至少持久化一次,至多1分钟会持久化一次,因此在系统出问题时,会最多丢失“一分钟的数据”。

需要注意的是,尽管系统是利用单位时间内脏键值的数量来启动持久化事件,但在持久化过程中,并不是采取增量的方式进行的,而是每次都将此刻整个数据集的快照重新编码写到硬盘的二进制文件中。在持久化结束后,硬盘上的数据是数据库中某时刻(持久化过程启动的时刻)的数据快照。若由于系统断电或操作系统出问题,而使得持久化过程失败,那么系统的损失是自上次持久化之后新增的脏数据信息。

系统是利用上述参数来判断何时启动快照方式的持久化过程的,而此功能的实现是基于REDIS自带的事件驱动库完成的。之后,我们再专门讨论这个简洁的事件驱动库。
 
持久化过程(copy-on-write)
当持久化事件被启动时,系统的主进程会采用copy-on-write的模式 fork一个子进程。当然,具体的模式与操作系统中fork的实现有关。所谓copy-on-write, 在此例中可以做如下理解:

系统会根据父进程的信息创建其子进程,但创建之初,他们是共享地址空间的。父进程继续提供各种读写操作的服务,而子进程则进行持久化操作,将内存中的数据重新编码后全部写到硬盘上。对于客户端来讲,它的请求会像往常一样得到响应,与相应的数据信息是否被持久化无关,所以快照持久化是异步进行的。其实,也不是完全无关,因系统的CPU与内存等资源是有限的,两个进程有时会存在资源竞争关系,从而造成相互影响,比如当数据量已占据内存的80%时,父进程同时要对高负载的读写操作作出响应,尤其是写操作。此种情况后面会仔细讨论。

当父进程进行写操作时,它只把被操作的键值从共享地址空间里做本地化的复制,之后在副本上进行写操作,而不是去修改“与子进程共享的地址空间里”的键值信息,因为那样的行为会违背 point-in-time的语义,即在持久化结束前fork时刻内存数据的快照会被改变。若此时父进程需要进行大量写操作,系统会对每个被操作的键值做复制,从而需要更多的内存资源。极限情况是,在子进程持久化的过程中,父进程应客户端的请求,对所有的键值都进行写操作,那么就需要“容量为两倍于数据集大小”的内存才可以应付。当子进程结束持久化时,系统会将其关掉,其被复制的键值所占用的地址空间将被释放,未被复制的键值直接归父进程私有。

通过上述解释,在持久化过程中,内存的使用会出现暂时地上升。当然,若系统是处于一次接一次地进行持久化的状态,那么内存的使用会一直是数据集的1.x倍。那么,具体数值与哪些因素有关呢? 我想基本有以下三个:写操作的负载(平均每秒有多少写操作,取决于业务逻辑),子进程持久化的速度(取决于REDIS内部实现),还有数据集的大小(基本取决于可以占用的内存空间)。这是快照方式需要注意的核心问题,作者也给出过一些典型的数值,请参见[4]

到此为止,算是基本上将 copy-on-write的语义与在持久化背景中的后果解释清楚,那么我们再来仔细看看,子进程所进行的持久化过程是具体怎样的呢。基本上,是通过遍历REDIS实例的每个数据库中的主哈希表,将键与值分别编码而存储起来。至于具体的编码形式,我会在相应的代码阅读中,详细地整理出来。需要提醒一点是,在REDIS 2.4中作者改进了持久化速度,主要是针对小的 hash /list/set/zset。因这四种数据结构的mini版本实现,完全是利用单纯的数组实现的(没有复杂的指针),且其在内存中的结构 [3] 与持久化后的存储方式基本一致。那么,在持久化此类数据时,REDIS没有必要进行编码,只需直接放进缓冲区中。在REDIS将全部数据集写到临时文件后,系统会将之替换掉原来的 RDB文件,从而使得快照持久化过程也是原子操作。

也许你还可能有另外个疑问,在持久化过程中,当内存的需求量超过实际内存的大小时,系统会怎样处理呢?这触及到内存数据库的另一个死穴,而这两个问题,即持久化与容量限制,在传统的硬盘数据库领域,它们是比较容易被同时解决地。但对于REDIS来讲,将来可能会在其自带虚拟内存模块与更先进的持久化技术的结合下,得到较好的解答吧。作者对此的见解是这样 [4],但这是在解决集群与开发出更好的持久化技术之后,才可能被回答的。所以,对于REDIS社区来讲,至少是一年以后的事情。我对容量限制的解决也有些浅显的认识,以后再专门谈。


追加命令方式(Append Only File)
由上述分析可知,快照方式的持久化效果并非尽如人意,在机器或操作系统出现某种问题时,系统会丢失部分数据,同时内存被过多消耗也是遭人诟病的一点(但作者觉得这是个tradeoff)。为此,REDIS提供另一种更好的持久化方式AOF,即主进程每次将收到的写操作命令 以追加的方式写到同一个文件AOF中。而当加载的时候,通过重新播放即可得到数据集原有的状态。

参数设置及追加过程
在AOF中,有三点值得注意:其一,AOF以增长方式进行存储的,所以每次写的信息比快照少很多,只与单位时间内的写操作数目成正比。其二,系统的主进程是在将写操作命令放到通往AOF文件的缓冲区之后,才对相应客户端的请求作出回应的。这表明AOF具有更好的持久化效果,但同时也意味着主进程时刻占用着Disk I/O这个宝贵的资源。表面乍看,好似AOF方式是同步的,其实不然,因它并不能保证在回复客户端前已将命令写落到硬盘上,而只是写到缓存区中。其三,从缓冲区到硬盘的写操作是由系统函数fsync控制的,REDIS提供三种方式。其中,每秒钟 fsync一次是默认的。

appendfsync always
appendfsync everysec
appendfsync no
第一种设置具有最好的持久化效果,每个命令都要即时写到硬盘上,但DISK IO与系统的CPU等主要资源会被占用很多;第二种是每秒做一次fsync,持久化效果也比快照的每分钟做一次好很多;第三个策略是完全由操作系统掌控,比如,在等缓冲区满后,操作系统调用fsync写数据到硬盘上。

问题及引入日志重新技术
乍看这个方案很完美,不像快照那样利用多余的内存,且由于采取追加的方式,系统的CPU与DISK IO等资源的负载压力都被分散掉,同时也得到较好的持久化效果。但任何问题的难度永远是个“守恒量”,除非快照的想法太差,不然AOF方案是不会这样完美的。它的问题是硬盘上的AOF文件会一直单调递增下去,除了硬盘的容量有限制,其他诸如REDIS启动时加载AOF文件会非常慢,备份会非常慢,网络传输也会非常慢等等。

为了解决这个问题,作者提出日志重写的方法(Log rewrite)来不断控制AOF文件的过速增长。关键点是,对于某时刻的数据集来讲,硬盘上庞大的AOF文件可以对之进行重新构建,但这是个反问题,所以有多种答案。在此环境下,反问题的理解是:给定命令序列,可以得到唯一的数据集;但给定数据集,可以有众多命令序列得到该数据集,其长度可长可短,而我们寻求的就是尽量短的命令序列。最佳答案是显然的,即每次根据某时刻的数据集(快照),遍历所有数据库,对每个键值构建“插入操作”的命令,从而组成最直接/短的命令序列。

也许你已经感觉到,日志重写策略在实现上与快照十分类似。那我们现在主要来分析下他们的不同点:其一,启动方式不同,日志重写的启动在于硬盘上AOF文件的大小与增长速度有关。默认设置是:

auto-AOF-rewrite-percentage 100
auto-AOF-rewrite-min-size 64mb
即当前AOF文件大小是上次日志重写得到AOF文件大小的二倍时,启动新的日志重写过程;但当刚刚启动REDIS时,这个策略是有严重缺陷的,文件的尺寸可以由1KB变为2KB,1MB变为2MB,但是没有必要重写,所以需要引入另一个参数作为补充,即auto-AOF-rewrite-min-size。其二,构建数据的方式不同,快照是直接对数据集进行重新编码,而日志重写是根据数据集构建命令序列。但可想而知,他们所占的空间都是与数据集大小成正比的;相对于分散的追加方式来说,内存的消耗量是很大的。其三,在子进程进行日志重写的同时,主进程对客户端请求的响应也像平常一样,将写操作命令追加到原有的AOF文件中,当然这也是显然的,因“追加命令方式”就是这样设计的。整个日志重新的细节,请参见[1].

AOF总结
到此为止,我们发现AOF持久化方式包含两个部分,即分散的追加命令与不断的日志重写。主进程负责追加命令(主要利用CPU与DISK IO),子进程在需要的时候被创建而负责日志重写(主要利用CPU,内存与DISK IO)。由于REDIS很少是CPU瓶颈,所以资源的竞争关系主要集中在DISK IO上。日志重写有快照所具有的缺点-内存的消耗量是数据集的1.x倍,同时又会与命令的追加造成DISK IO上的冲突。为了避免这种冲突,系统提供如下参数:

no-appendfsync-on-rewrite yes
即在日志重写时,不进行命令追加操作,而只是将其放在缓冲区里。也许你会觉得,这样的设置会使得AOF持久化效果与快照是一样的。仔细分析还是有不同的,在相同的写操作负载下,AOF中重写的频率应该少于快照持久化的频率。整体来讲,还是有提高的,当然也与具体的业务逻辑有关系,假如系统不断的增加一些计数器的值,此种情况快照的持久化方式会有明显的优势。

持久化总结
快照持久化的问题在于,持久化效果不佳造成的数据丢失,内存消耗量是数据集的1.x倍,以及持久化速度是否可以更快些。而AOF方式,在持久化效果方面,相对快照来讲有改进;但在DISK IO过度消耗时,会使得整个REDIS系统不稳定;其他方面与快照有着类似的问题。其实,除以上两种直接的办法外,在版本2.4下,还可以利用主从复制结构来进一步解决持久化中仍存在的问题;同时也部分地解决容量限制的问题(但要花钱买多台机器)。关于这些,在“主从复制结构”话题中,再作仔细分析。

具体细节可参加如下网页链接
[1] Official manual: http://redis.io/topics/persistence
[2] Copy-on-write: http://en.wikipedia.org/wiki/Copy-on-write
[3] Mini-version of data type: http://redis.io/topics/memory-optimization
[4] Disk or not to disk? Let us move forward:---chapter one
http://groups.google.com/group/redis-db/browse_thread/thread/823d725335c66a69/8011ea10e97563a2?lnk=gst=disk+or+not+#8011ea10e97563a2
[5] Fork: http://linux.die.net/man/2/fork/
  http://en.wikipedia.org/wiki/Fork_(operating_system)
[6] Fsync: http://linux.die.net/man/2/fsync
[7] redis.conf: https://github.com/antirez/redis/blob/unstable/redis.conf

 

 

欢迎讨论/沟通...

文章评论

团队中“技术大拿”并非越多越好
团队中“技术大拿”并非越多越好
那些争议最大的编程观点
那些争议最大的编程观点
做程序猿的老婆应该注意的一些事情
做程序猿的老婆应该注意的一些事情
程序员眼里IE浏览器是什么样的
程序员眼里IE浏览器是什么样的
不懂技术不要对懂技术的人说这很容易实现
不懂技术不要对懂技术的人说这很容易实现
写给自己也写给你 自己到底该何去何从
写给自己也写给你 自己到底该何去何从
程序员和编码员之间的区别
程序员和编码员之间的区别
漫画:程序员的工作
漫画:程序员的工作
聊聊HTTPS和SSL/TLS协议
聊聊HTTPS和SSL/TLS协议
5款最佳正则表达式编辑调试器
5款最佳正则表达式编辑调试器
初级 vs 高级开发者 哪个性价比更高?
初级 vs 高级开发者 哪个性价比更高?
鲜为人知的编程真相
鲜为人知的编程真相
Java 与 .NET 的平台发展之争
Java 与 .NET 的平台发展之争
程序员必看的十大电影
程序员必看的十大电影
Web开发人员为什么越来越懒了?
Web开发人员为什么越来越懒了?
十大编程算法助程序员走上高手之路
十大编程算法助程序员走上高手之路
旅行,写作,编程
旅行,写作,编程
老美怎么看待阿里赴美上市
老美怎么看待阿里赴美上市
60个开发者不容错过的免费资源库
60个开发者不容错过的免费资源库
总结2014中国互联网十大段子
总结2014中国互联网十大段子
为啥Android手机总会越用越慢?
为啥Android手机总会越用越慢?
看13位CEO、创始人和高管如何提高工作效率
看13位CEO、创始人和高管如何提高工作效率
10个调试和排错的小建议
10个调试和排错的小建议
程序员最害怕的5件事 你中招了吗?
程序员最害怕的5件事 你中招了吗?
程序员的一天:一寸光阴一寸金
程序员的一天:一寸光阴一寸金
程序员都该阅读的书
程序员都该阅读的书
程序员的鄙视链
程序员的鄙视链
Java程序员必看电影
Java程序员必看电影
中美印日四国程序员比较
中美印日四国程序员比较
为什么程序员都是夜猫子
为什么程序员都是夜猫子
什么才是优秀的用户界面设计
什么才是优秀的用户界面设计
每天工作4小时的程序员
每天工作4小时的程序员
如何成为一名黑客
如何成为一名黑客
我的丈夫是个程序员
我的丈夫是个程序员
要嫁就嫁程序猿—钱多话少死的早
要嫁就嫁程序猿—钱多话少死的早
程序猿的崛起——Growth Hacker
程序猿的崛起——Growth Hacker
如何区分一个程序员是“老手“还是“新手“?
如何区分一个程序员是“老手“还是“新手“?
程序员应该关注的一些事儿
程序员应该关注的一些事儿
10个帮程序员减压放松的网站
10个帮程序员减压放松的网站
“肮脏的”IT工作排行榜
“肮脏的”IT工作排行榜
Web开发者需具备的8个好习惯
Web开发者需具备的8个好习惯
我跳槽是因为他们的显示器更大
我跳槽是因为他们的显示器更大
当下全球最炙手可热的八位少年创业者
当下全球最炙手可热的八位少年创业者
 程序员的样子
程序员的样子
一个程序员的时间管理
一个程序员的时间管理
软件开发程序错误异常ExceptionCopyright © 2009-2015 MyException 版权所有