Redis持久化的思考
Redis的作者在其博客中提到,他看到的所有針對(duì)Redis的討論中佩憾,對(duì)Redis持久化的誤解是最大的哮伟,于是他寫了一篇長(zhǎng)文來(lái)對(duì)Redis的持久化進(jìn)行了系統(tǒng)性的論述。以下為文章的部分內(nèi)容:
什么是持久化妄帘?簡(jiǎn)單來(lái)講就是將數(shù)據(jù)放到斷電后數(shù)據(jù)不會(huì)丟失的設(shè)備中楞黄,也就是我們通常理解的硬盤上。
首先我們來(lái)看一下數(shù)據(jù)庫(kù)在進(jìn)行寫操作時(shí)到底做了哪些事抡驼,主要有下面五個(gè)過(guò)程:
- 客戶端向服務(wù)端發(fā)送寫操作(數(shù)據(jù)在客戶端的內(nèi)存中)鬼廓。
- 數(shù)據(jù)庫(kù)服務(wù)端接收到寫請(qǐng)求的數(shù)據(jù)(數(shù)據(jù)在服務(wù)端的內(nèi)存中)。
- 服務(wù)端調(diào)用write這個(gè)系統(tǒng)調(diào)用致盟,將數(shù)據(jù)往磁盤上寫(數(shù)據(jù)在系統(tǒng)內(nèi)存的緩沖區(qū)中)碎税。
- 操作系統(tǒng)將緩沖區(qū)中的數(shù)據(jù)轉(zhuǎn)移到磁盤控制器上(數(shù)據(jù)在磁盤緩存中)。
- 磁盤控制器將數(shù)據(jù)寫到磁盤的物理介質(zhì)中(數(shù)據(jù)真正落到磁盤上)馏锡。
故障分析
寫操作大致有上面5個(gè)流程雷蹂,下面我們結(jié)合上面的5個(gè)流程看一下各種級(jí)別的故障:
- 當(dāng)數(shù)據(jù)庫(kù)系統(tǒng)故障時(shí),這時(shí)候系統(tǒng)內(nèi)核還是完好的杯道。那么此時(shí)只要我們執(zhí)行完了第3步萎河,那么數(shù)據(jù)就是安全的,因?yàn)楹罄m(xù)操作系統(tǒng)會(huì)來(lái)完成后面幾步蕉饼,保證數(shù)據(jù)最終會(huì)落到磁盤上。
- 當(dāng)系統(tǒng)斷電時(shí)玛歌,這時(shí)候上面5項(xiàng)中提到的所有緩存都會(huì)失效昧港,并且數(shù)據(jù)庫(kù)和操作系統(tǒng)都會(huì)停止工作。所以只有當(dāng)數(shù)據(jù)在完成第5步后支子,才能保證在斷電后數(shù)據(jù)不丟失创肥。
通過(guò)上面5步的了解,可能我們會(huì)希望搞清下面一些問(wèn)題:
- 數(shù)據(jù)庫(kù)多長(zhǎng)時(shí)間調(diào)用一次write值朋,將數(shù)據(jù)寫到內(nèi)核緩沖區(qū)叹侄?
- 內(nèi)核多長(zhǎng)時(shí)間會(huì)將系統(tǒng)緩沖區(qū)中的數(shù)據(jù)寫到磁盤控制器?
- 磁盤控制器又在什么時(shí)候把緩存中的數(shù)據(jù)寫到物理介質(zhì)上昨登?
對(duì)于第一個(gè)問(wèn)題趾代,通常數(shù)據(jù)庫(kù)層面會(huì)進(jìn)行全面控制。
而對(duì)第二個(gè)問(wèn)題丰辣,操作系統(tǒng)有其默認(rèn)的策略撒强,但是我們也可以通過(guò)POSIX API提供的fsync系列命令強(qiáng)制操作系統(tǒng)將數(shù)據(jù)從內(nèi)核區(qū)寫到磁盤控制器上禽捆。
對(duì)于第三個(gè)問(wèn)題,好像數(shù)據(jù)庫(kù)已經(jīng)無(wú)法觸及飘哨,但實(shí)際上胚想,大多數(shù)情況下磁盤緩存是被設(shè)置關(guān)閉的,或者是只開啟為讀緩存芽隆,也就是說(shuō)寫操作不會(huì)進(jìn)行緩存浊服,直接寫到磁盤。
數(shù)據(jù)損壞
所謂數(shù)據(jù)損壞胚吁,就是數(shù)據(jù)無(wú)法恢復(fù)牙躺,上面我們講的都是如何保證數(shù)據(jù)是確實(shí)寫到磁盤上去,但是寫到磁盤上可能并不意味著數(shù)據(jù)不會(huì)損壞囤采。比如我們可能一次寫請(qǐng)求會(huì)進(jìn)行兩次不同的寫操作述呐,當(dāng)意外發(fā)生時(shí),可能會(huì)導(dǎo)致一次寫操作安全完成蕉毯,但是另一次還沒(méi)有進(jìn)行乓搬。如果數(shù)據(jù)庫(kù)的數(shù)據(jù)文件結(jié)構(gòu)組織不合理,可能就會(huì)導(dǎo)致數(shù)據(jù)完全不能恢復(fù)的狀況出現(xiàn)代虾。
這里通常也有三種策略來(lái)組織數(shù)據(jù)进肯,以防止數(shù)據(jù)文件損壞到無(wú)法恢復(fù)的情況:
- 第一種是最粗糙的處理,就是不通過(guò)數(shù)據(jù)的組織形式保證數(shù)據(jù)的可恢復(fù)性棉磨。而是通過(guò)配置數(shù)據(jù)同步備份的方式江掩,在數(shù)據(jù)文件損壞后通過(guò)數(shù)據(jù)備份來(lái)進(jìn)行恢復(fù)。實(shí)際上MongoDB在不開啟操作日志乘瓤,通過(guò)配置Replica Sets時(shí)就是這種情況环形。
- 另一種是在上面基礎(chǔ)上添加一個(gè)操作日志,每次操作時(shí)記一下操作的行為衙傀,這樣我們可以通過(guò)操作日志來(lái)進(jìn)行數(shù)據(jù)恢復(fù)抬吟。因?yàn)椴僮魅罩臼琼樞蜃芳拥姆绞綄懙模圆粫?huì)出現(xiàn)操作日志也無(wú)法恢復(fù)的情況统抬。這也類似于MongoDB開啟了操作日志的情況火本。
- 更保險(xiǎn)的做法是數(shù)據(jù)庫(kù)不進(jìn)行舊數(shù)據(jù)的修改,只是以追加方式去完成寫操作聪建,這樣數(shù)據(jù)本身就是一份日志钙畔,這樣就永遠(yuǎn)不會(huì)出現(xiàn)數(shù)據(jù)無(wú)法恢復(fù)的情況了。實(shí)際上CouchDB就是此做法的優(yōu)秀范例金麸。
持久化
持久化是將程序數(shù)據(jù)在持久狀態(tài)和瞬時(shí)狀態(tài)間轉(zhuǎn)換的機(jī)制擎析。對(duì)于程序來(lái)說(shuō),程序運(yùn)行中數(shù)據(jù)是在內(nèi)存的挥下,如果沒(méi)有及時(shí)同步寫入到磁盤叔锐,那么一旦斷電或者程序突然奔潰挪鹏,數(shù)據(jù)就會(huì)丟失了,只有把數(shù)據(jù)及時(shí)同步到磁盤愉烙,數(shù)據(jù)才能永久保存讨盒,不會(huì)因?yàn)殄礄C(jī)影像數(shù)據(jù)的有效性。而持久化就是將數(shù)據(jù)從程序同步到磁盤的一個(gè)動(dòng)作過(guò)程步责。
Redis的持久化
redis有RDB和AOF兩種持久化方式返顺。RDB是快照文件的方式,redis通過(guò)執(zhí)行SAVE/BGSAVE命令蔓肯,執(zhí)行數(shù)據(jù)的備份遂鹊,將redis當(dāng)前的數(shù)據(jù)保存到.rdb文件中,文件保存了所有的數(shù)據(jù)集合蔗包。AOF是服務(wù)器通過(guò)讀取配置秉扑,在指定的時(shí)間里,追加redis寫操作的命令到.aof文件中调限,是一種增量的持久化方式舟陆。
RDB SAVE
RDB文件通過(guò)SAVE或BGSAVE命令實(shí)現(xiàn)。
SAVE命令會(huì)阻塞Redis服務(wù)進(jìn)程耻矮,直到RDB文件創(chuàng)建完成為止秦躯。
可以通過(guò)配置設(shè)置自動(dòng)做快照持久化的方式。我們可以配置redis在n秒內(nèi)如果超過(guò)m個(gè)key被修改就自動(dòng)做快照裆装,下面是默認(rèn)的快照保存配置:
save 900 1 #900秒內(nèi)如果超過(guò)1個(gè)key被修改踱承,則發(fā)起快照保存
save 300 10 #300秒內(nèi)容如超過(guò)10個(gè)key被修改,則發(fā)起快照保存
save 60 10000
RDB BGSAVE
BGSAVE命令通過(guò)fork子進(jìn)程哨免,由子進(jìn)程來(lái)進(jìn)行創(chuàng)建RDB文件茎活,父進(jìn)程和子進(jìn)程共享物理頁(yè)面,父進(jìn)程繼續(xù)提供讀寫服務(wù)琢唾,子進(jìn)程實(shí)現(xiàn)備份功能载荔。BGSAVE階段只有在需要修改共享數(shù)據(jù)段的時(shí)候才進(jìn)行拷貝,也就是COW(Copy On Write)慧耍,父進(jìn)程處理寫請(qǐng)求時(shí)OS會(huì)為父進(jìn)程要修改的頁(yè)面創(chuàng)建副本,而不是寫共享的頁(yè)面丐谋。當(dāng)子進(jìn)程將快照寫入臨時(shí)文件完畢后芍碧,用臨時(shí)文件替換原來(lái)的快照文件,然后子進(jìn)程退出号俐。
有了RDB文件之后泌豆,如果服務(wù)器關(guān)機(jī)了,或者需要新增一個(gè)服務(wù)器吏饿,重新啟動(dòng)數(shù)據(jù)庫(kù)服務(wù)器之后踪危,就可以通過(guò)載入RDB文件恢復(fù)之前備份的數(shù)據(jù)蔬浙。 但是bgsave會(huì)耗費(fèi)較長(zhǎng)時(shí)間,不夠?qū)崟r(shí)贞远,會(huì)導(dǎo)致在停機(jī)的時(shí)候丟失大量數(shù)據(jù)畴博。
RDB的優(yōu)勢(shì)
- 一旦采用該方式,那么你的整個(gè)Redis數(shù)據(jù)庫(kù)將只包含一個(gè)文件蓝仲,這樣非常方便進(jìn)行備份俱病。比如你可能打算每1天歸檔一些數(shù)據(jù)。
- 方便備份袱结,我們可以很容易的將一個(gè)一個(gè)RDB文件移動(dòng)到其他的存儲(chǔ)介質(zhì)上
- RDB 在恢復(fù)大數(shù)據(jù)集時(shí)的速度比 AOF 的恢復(fù)速度要快亮隙。
- RDB 可以最大化 Redis 的性能:父進(jìn)程在保存 RDB 文件時(shí)唯一要做的就是 fork 出一個(gè)子進(jìn)程,然后這個(gè)子進(jìn)程就會(huì)處理接下來(lái)的所有保存工作垢夹,父進(jìn)程無(wú)須執(zhí)行任何磁盤 I/O 操作溢吻。
RDB劣勢(shì)
- 如果你需要盡量避免在服務(wù)器故障時(shí)丟失數(shù)據(jù),那么 RDB 不適合你果元。 雖然 Redis 允許你設(shè)置不同的保存點(diǎn)(save point)來(lái)控制保存 RDB 文件的頻率促王, 但是, 因?yàn)镽DB 文件需要保存整個(gè)數(shù)據(jù)集的狀態(tài)噪漾, 所以它并不是一個(gè)輕松的操作硼砰。 因此你可能會(huì)至少 5 分鐘才保存一次 RDB 文件。 在這種情況下欣硼, 一旦發(fā)生故障停機(jī)题翰, 你就可能會(huì)丟失好幾分鐘的數(shù)據(jù)。
- 每次保存 RDB 的時(shí)候诈胜,Redis 都要 fork() 出一個(gè)子進(jìn)程豹障,并由子進(jìn)程來(lái)進(jìn)行實(shí)際的持久化工作。 在數(shù)據(jù)集比較龐大時(shí)焦匈, fork() 可能會(huì)非常耗時(shí)血公,造成服務(wù)器在某某毫秒內(nèi)停止處理客戶端; 如果數(shù)據(jù)集非常巨大缓熟,并且 CPU 時(shí)間非常緊張的話累魔,那么這種停止時(shí)間甚至可能會(huì)長(zhǎng)達(dá)整整一秒。 雖然 AOF 重寫也需要進(jìn)行 fork() 够滑,但無(wú)論 AOF 重寫的執(zhí)行間隔有多長(zhǎng)垦写,數(shù)據(jù)的耐久性都不會(huì)有任何損失。
AOF
redis會(huì)將每一個(gè)收到的寫命令都通過(guò)write函數(shù)追加到文件中(默認(rèn)是 appendonly.aof)彰触。
當(dāng)redis重啟時(shí)會(huì)通過(guò)重新執(zhí)行文件中保存的寫命令來(lái)在內(nèi)存中重建整個(gè)數(shù)據(jù)庫(kù)的內(nèi)容梯投。當(dāng)然由于os會(huì)在內(nèi)核中緩存 write做的修改,所以可能不是立即寫到磁盤上。這樣aof方式的持久化也還是有可能會(huì)丟失部分修改分蓖。不過(guò)我們可以通過(guò)配置文件告訴redis我們想要 通過(guò)fsync函數(shù)強(qiáng)制os寫入到磁盤的時(shí)機(jī)尔艇。有三種方式如下(默認(rèn)是:每秒fsync一次)
appendonly yes //啟用aof持久化方式
# appendfsync always //每次收到寫命令就立即強(qiáng)制寫入磁盤,最慢的么鹤,但是保證完全的持久化终娃,不推薦使用
appendfsync everysec //每秒鐘強(qiáng)制寫入磁盤一次,在性能和持久化方面做了很好的折中午磁,推薦
# appendfsync no //完全依賴os尝抖,性能最好,持久化沒(méi)保證
AOF是一個(gè)寫文件操作迅皇,其目的是將操作日志寫到磁盤上昧辽,所以它也同樣會(huì)遇到我們上面說(shuō)的寫操作的5個(gè)流程。那么寫AOF的操作安全性又有多高呢登颓?實(shí)際上這是可以設(shè)置的搅荞,在Redis中對(duì)AOF調(diào)用write寫入后,何時(shí)再調(diào)用fsync將其寫到磁盤上,通過(guò)appendfsync選項(xiàng)來(lái)控制,下面appendfsync的三個(gè)設(shè)置項(xiàng)洞就,安全強(qiáng)度逐漸變強(qiáng)汰寓。
1抹竹、appendfsync no
當(dāng)設(shè)置appendfsync為no的時(shí)候,Redis不會(huì)主動(dòng)調(diào)用fsync去將AOF日志內(nèi)容同步到磁盤,所以這一切就完全依賴于操作系統(tǒng)的調(diào)試了。對(duì)大多數(shù)Linux操作系統(tǒng)腔丧,是每30秒進(jìn)行一次fsync,將緩沖區(qū)中的數(shù)據(jù)寫到磁盤上作烟。
2愉粤、appendfsync everysec
當(dāng)設(shè)置appendfsync為everysec的時(shí)候,Redis會(huì)默認(rèn)每隔一秒進(jìn)行一次fsync調(diào)用拿撩,將緩沖區(qū)中的數(shù)據(jù)寫到磁盤衣厘。但是當(dāng)這一 次的fsync調(diào)用時(shí)長(zhǎng)超過(guò)1秒時(shí)。Redis會(huì)采取延遲fsync的策略压恒,再等一秒鐘影暴。也就是在兩秒后再進(jìn)行fsync,這一次的fsync就不管會(huì)執(zhí)行多長(zhǎng)時(shí)間都會(huì)進(jìn)行探赫。這時(shí)候由于在fsync時(shí)文件描述符會(huì)被阻塞型宙,所以當(dāng)前的寫操作就會(huì)阻塞。
所以期吓,結(jié)論就是:在絕大多數(shù)情況下早歇,Redis會(huì)每隔一秒進(jìn)行一次fsync。在最壞的情況下讨勤,兩秒鐘會(huì)進(jìn)行一次fsync操作箭跳。
這一操作在大多數(shù)數(shù)據(jù)庫(kù)系統(tǒng)中被稱為group commit,就是組合多次寫操作的數(shù)據(jù)潭千,一次性將日志寫到磁盤谱姓。
3、appednfsync always
當(dāng)設(shè)置appendfsync為always時(shí)刨晴,每一次寫操作都會(huì)調(diào)用一次fsync屉来,這時(shí)數(shù)據(jù)是最安全的,當(dāng)然狈癞,由于每次都會(huì)執(zhí)行fsync茄靠,所以其性
aof文件的壓縮
aof 的方式也同時(shí)帶來(lái)了另一個(gè)問(wèn)題。持久化文件會(huì)變的越來(lái)越大蝶桶。例如我們調(diào)用incr test命令100次慨绳,文件中必須保存全部的100條命令,其實(shí)有99條都是多余的真竖。因?yàn)橐謴?fù)數(shù)據(jù)庫(kù)的狀態(tài)其實(shí)文件中保存一條set test 100就夠了脐雪。
為了壓縮aof的持久化文件。redis提供了bgrewriteaof命令恢共。收到此命令redis將使用與快照類似的方式將內(nèi)存中的數(shù)據(jù)以命令的方式保存到臨時(shí)文件中战秋,最后替換原來(lái)的文件。具體過(guò)程如下:
- redis調(diào)用fork 讨韭,現(xiàn)在有父子兩個(gè)進(jìn)程
- 子進(jìn)程根據(jù)內(nèi)存中的數(shù)據(jù)庫(kù)快照脂信,往臨時(shí)文件中寫入重建數(shù)據(jù)庫(kù)狀態(tài)的命令
- 父進(jìn)程繼續(xù)處理client請(qǐng)求,除了把寫命令寫入到原來(lái)的aof文件中拐袜。同時(shí)把收到的寫命令緩存起來(lái)吉嚣。這樣就能保證如果子進(jìn)程重寫失敗的話并不會(huì)出問(wèn)題。
- 當(dāng)子進(jìn)程把快照內(nèi)容寫入已命令方式寫到臨時(shí)文件中后蹬铺,子進(jìn)程發(fā)信號(hào)通知父進(jìn)程尝哆。然后父進(jìn)程把緩存的寫命令也寫入到臨時(shí)文件。
- 現(xiàn)在父進(jìn)程可以使用臨時(shí)文件替換老的aof文件甜攀,并重命名秋泄,后面收到的寫命令也開始往新的aof文件中追加。
需要注意到是重寫aof文件的操作规阀,并沒(méi)有讀取舊的aof文件恒序,而是將整個(gè)內(nèi)存中的數(shù)據(jù)庫(kù)內(nèi)容用命令的方式重寫了一個(gè)新的aof文件,這點(diǎn)和快照有點(diǎn)類似。
aof優(yōu)勢(shì)
- 使用 AOF 持久化會(huì)讓 Redis 變得非常耐久(much more durable):你可以設(shè)置不同的 fsync 策略谁撼,比如無(wú) fsync 歧胁,每秒鐘一次 fsync ,或者每次執(zhí)行寫入命令時(shí) fsync 。 AOF 的默認(rèn)策略為每秒鐘 fsync 一次喊巍,在這種配置下屠缭,Redis 仍然可以保持良好的性能,并且就算發(fā)生故障停機(jī)崭参,也最多只會(huì)丟失一秒鐘的數(shù)據(jù)( fsync 會(huì)在后臺(tái)線程執(zhí)行呵曹,所以主線程可以繼續(xù)努力地處理命令請(qǐng)求)。
- AOF 文件是一個(gè)只進(jìn)行追加操作的日志文件(append only log)何暮, 因此對(duì) AOF 文件的寫入不需要進(jìn)行 seek 奄喂, 即使日志因?yàn)槟承┰蚨宋磳懭胪暾拿睿ū热鐚懭霑r(shí)磁盤已滿,寫入中途停機(jī)海洼,等等)跨新, redis-check-aof 工具也可以輕易地修復(fù)這種問(wèn)題。
Redis 可以在 AOF 文件體積變得過(guò)大時(shí)坏逢,自動(dòng)地在后臺(tái)對(duì) AOF 進(jìn)行重寫: 重寫后的新 AOF 文件包含了恢復(fù)當(dāng)前數(shù)據(jù)集所需的最小命令集合玻蝌。 整個(gè)重寫操作是絕對(duì)安全的,因?yàn)?Redis 在創(chuàng)建新 AOF 文件的過(guò)程中词疼,會(huì)繼續(xù)將命令追加到現(xiàn)有的 AOF 文件里面俯树,即使重寫過(guò)程中發(fā)生停機(jī),現(xiàn)有的 AOF 文件也不會(huì)丟失贰盗。 而一旦新 AOF 文件創(chuàng)建完畢许饿,Redis 就會(huì)從舊 AOF 文件切換到新 AOF 文件,并開始對(duì)新 AOF 文件進(jìn)行追加操作舵盈。 - AOF 文件有序地保存了對(duì)數(shù)據(jù)庫(kù)執(zhí)行的所有寫入操作陋率, 這些寫入操作以 Redis 協(xié)議的格式保存, 因此 AOF 文件的內(nèi)容非常容易被人讀懂秽晚, 對(duì)文件進(jìn)行分析(parse)也很輕松瓦糟。 導(dǎo)出(export) AOF 文件也非常簡(jiǎn)單: 舉個(gè)例子, 如果你不小心執(zhí)行了 FLUSHALL 命令赴蝇, 但只要 AOF 文件未被重寫菩浙, 那么只要停止服務(wù)器, 移除 AOF 文件末尾的 FLUSHALL 命令句伶, 并重啟 Redis 劲蜻, 就可以將數(shù)據(jù)集恢復(fù)到 FLUSHALL 執(zhí)行之前的狀態(tài)。
aof劣勢(shì)
- 對(duì)于相同的數(shù)據(jù)集來(lái)說(shuō)考余,AOF 文件的體積通常要大于 RDB 文件的體積先嬉。
- 根據(jù)所使用的 fsync 策略,AOF 的速度可能會(huì)慢于 RDB 楚堤。 在一般情況下疫蔓, 每秒 fsync 的性能依然非常高含懊, 而關(guān)閉 fsync 可以讓 AOF 的速度和 RDB 一樣快, 即使在高負(fù)荷之下也是如此衅胀。 不過(guò)在處理巨大的寫入載入時(shí)绢要,RDB 可以提供更有保證的最大延遲時(shí)間(latency)。
- AOF 在過(guò)去曾經(jīng)發(fā)生過(guò)這樣的 bug : 因?yàn)閭€(gè)別命令的原因拗小,導(dǎo)致 AOF 文件在重新載入時(shí),無(wú)法將數(shù)據(jù)集恢復(fù)成保存時(shí)的原樣樱哼。