問題概述
在分布式系統(tǒng)中冤吨,宕機是需要考慮的重要組成部分呻右。日志技術是宕機恢復的重要技術之一哑子。日志技術應用廣泛舅列,早些更是廣泛應用在數(shù)據(jù)庫設計實現(xiàn)中肌割。本文先介紹基本原理概念,最后通過redis介紹生產(chǎn)環(huán)境中的實現(xiàn)方法帐要。
Redo Log
數(shù)據(jù)庫設計中把敞,需要滿足ACID,尤其是在支持事務的系統(tǒng)中榨惠。當系統(tǒng)遇到未知錯誤時奋早,可以恢復到一個穩(wěn)定可靠的狀態(tài)。有一個很簡單的思路赠橙,就是記錄所有對數(shù)據(jù)庫的寫操作日志耽装。那么一旦發(fā)生故障,即使丟失掉內(nèi)存中所有數(shù)據(jù)期揪,當下一次啟動時掉奄,通過復現(xiàn)已經(jīng)記錄的數(shù)據(jù)庫寫操作日志,依然可以回到故障之前的狀態(tài)(如果在寫操作作日志的時候發(fā)生故障凤薛,那么這次數(shù)據(jù)庫操作失斝战ā)。
操作流程簡單如下(假設每次數(shù)據(jù)變化缤苫,都提交):
- 更新的操作方式依次記錄到磁盤日志文件速兔。
- 更新內(nèi)存中的數(shù)據(jù)。
- 返回更新成功結果活玲。
恢復流程如下
- 讀取日志文件憨栽,依次修改內(nèi)存中的數(shù)據(jù)。
優(yōu)點:
- 日志文件有序翼虫,可以通過append的方式寫入磁盤屑柔,性能很高。
- 簡單可靠珍剑,應用廣泛掸宛。可以把內(nèi)存中的數(shù)據(jù)招拙,做備份在磁盤中唧瘾。
缺點:
- 使用時間一長,恢復宕機的時間很慢别凤。
解決方法
先具體化下饰序,如果我們內(nèi)存中保留一個a的值,記錄了寫操作比如 a = 4; a++; a--; 當這些操作上千萬规哪、億之后求豫,恢復非常慢。甚至可能最后一條就是a=0,按照之前的算法蝠嘉,我們卻跑了很長時間最疆。
那么根據(jù)這個場景,很容易想到一個解決方案蚤告。
操作流程:
- 日志文件記錄begin check point
- 在某個時刻努酸,把內(nèi)存中的數(shù)值,直接snapshot或dump到磁盤上.(比如直接記錄a=4)
- 日志文件記錄end check point
恢復流程:
1.掃描日志文件杜恰,找到最后的end check point中配對的begin check point获诈。
2.讀入dump文件。
3.依次回放記錄的日志操作心褐。
優(yōu)點:
- 應用廣泛烙荷,包括 mysql,oracle檬寂。
一些棘手的問題:
在做snapshot的時候终抽,往往不能停止數(shù)據(jù)庫的服務,那么很可能記錄了begin check point之后的日志桶至。那么在重新load begin check point之后的日志時昼伴,最后恢復的數(shù)據(jù)很有可能不對刃泌。比如我們記錄的是a++這樣的日志, 那么重復一條日志美浦,就會讓a的值加1。反之如果我們記錄是冪等的且改,比如一直是 a=5 這種操作女蜈,那么就對最后結果沒有影響持舆。很顯然,設計冪等操作系統(tǒng)很麻煩伪窖。
設計一個支持snapshot的內(nèi)存數(shù)據(jù)結構逸寓,也比較麻煩。
典型的是通過copy-on-write機制覆山。和操作系統(tǒng)中的概念一樣竹伸。當這個數(shù)據(jù)結構被修改,就創(chuàng)建一份真正的copy簇宽。老數(shù)據(jù)增加一份dirty flag勋篓。如果沒有修改就繼續(xù)使用之前的內(nèi)存。這樣在做snapshot的時候魏割,保證我們的dump數(shù)據(jù)是begin check point這個時刻的數(shù)據(jù)譬嚣。顯然這個也比較麻煩。
還有一種支持snapshot的思路是begin check point后钞它,不動老的數(shù)據(jù)拜银。內(nèi)存中的數(shù)據(jù)在新的地方殊鞭,日志也寫在新的地方。最后在end check point做一次merge盐股。這個實現(xiàn)起來簡單,但是內(nèi)存消耗不小耻卡。
Redis是如何解決日志問題的
Redis 是一個基于內(nèi)存的database疯汁,不同于memcached,他支持持久化卵酪。另外由于redis處理client request 和 response 都是在一個thread里面幌蚊,也沒有搶占式的調(diào)度系統(tǒng),核心業(yè)務都是按照event loop順序執(zhí)行溃卡,而磁盤寫日志又開銷很大溢豆,所以redis實現(xiàn)日志功能做了很多優(yōu)化。并且提供2種持久化方案瘸羡。我們需要在不同的場景下漩仙,采用不同的方式配置。
snapshotting
某個時刻犹赖,redis會把內(nèi)存中的所有數(shù)據(jù)snapshot到磁盤文件队他。更通俗的說法是fork一個child process,把內(nèi)存中的數(shù)據(jù)序列化到臨時文件峻村,然后在main event loop 中原子的更換文件名麸折。redis,利用了操作系統(tǒng)VM的copy-on-write機制粘昨,在不阻塞主線程的情況下垢啼,利用子進程和父進程共享的data segment實現(xiàn)snapshot。具體是代碼實現(xiàn)在rdb.c, function at rdbSaveBackground
優(yōu)點:
- 簡單可靠张肾,如果database 不大芭析,執(zhí)行的效果非常好。
缺點:
1.如果database size 很大吞瞪,每一次snapshot時間非常長放刨。不得不配置大的間隔,提高了宕機時數(shù)據(jù)丟失的風險尸饺。
為了解決上面的問題进统,redis增加了AOF。
Append Only File(AOF)
在database術語中浪听,也被叫做WAL螟碎。如果開啟的AOF的配置,redis會記錄所有寫操作到日志文件中迹栓。那么redis同樣會遇到之前我們提到過的問題掉分。
- 即便是追加寫,磁盤的操作依然比內(nèi)存慢好幾個數(shù)量級,頻繁的操作容易產(chǎn)生瓶頸酥郭。
- 如果數(shù)據(jù)量操作頻繁华坦,會產(chǎn)生大量的重復日志數(shù)據(jù),導致恢復時間太長不从。比如記錄一條微博的瀏覽量惜姐,會記錄大量重復的+1日志。
那么redis是如何解決的呢椿息?
- 文件寫操作消耗的時間很長歹袁,redis會先把記錄日志寫在內(nèi)存buffer中,在每一次event loop 結束之后寝优,根據(jù)配置判斷是否做寫操作条舔。每個buffer的大小有限制,這樣每次寫操作時間不會太長乏矾。
- 即便是調(diào)用write操作孟抗,OS并沒有立即寫入磁盤,redis 同樣提供了一些方案決定刷新OS IO buffer的時機(1秒钻心、從不夸浅、每次)。
- redis 提供一種AOF重寫的方式rewriteAppendOnlyFile來處理AOF文件過大情況扔役。
前面我們知道了帆喇,這種check point
的機制還是比較麻煩的。那么redis是怎么設計的呢?
- 為了避免加鎖亿胸,redis 依然創(chuàng)建了一個child process坯钦,利用VM的copy-on-write,共享數(shù)據(jù)侈玄。同時保證主線程依然可以處理client請求婉刀。
- 根據(jù)KV的類型,先從內(nèi)存讀取數(shù)據(jù)序仙,然后再寫數(shù)據(jù)到磁盤突颊,和之前的AOF文件無關。
那么當子進程rewrite AOF的過程中潘悼,main thread依然可以處理新的client request律秃。新增的數(shù)據(jù)會被放在rewrite buffer中,而且寫到原有的AOF文件中治唤。 - child process完成后會通知主線程棒动。主線程有一個定時任務,也就是會不斷輪詢child process是否已經(jīng)完成(通過信號量)宾添。
- 主線程會merge 變化的數(shù)據(jù)到temp file船惨。
- 主線程原子的rename到一個新的AOF文件柜裸,老AOF就不起作用了。
優(yōu)點:
- 除了merge 和 rename需要阻塞主線程粱锐,rewrite不會阻塞主線程疙挺。(前提是使用bgrewrite command)。
最后
這些都是性能和穩(wěn)定性之間做的權衡怜浅,根據(jù)不同場景需要調(diào)整铐然。
參考
Redis latency problems troubleshooting
分布式系統(tǒng)原理介紹
Thoughts on Redis