持久化對于任何數(shù)據(jù)庫來說都是重要的知識點(diǎn)食侮。很久前寫過mongo和mysql的日志。今天記錄下redis的日志設(shè)計目胡。
Redis把后端數(shù)據(jù)庫中的數(shù)據(jù)存儲在內(nèi)存中锯七,然后直接從內(nèi)存中讀取數(shù)據(jù),響應(yīng)速度會非秤海快眉尸。但服務(wù)器宕機(jī)時,內(nèi)存中的數(shù)據(jù)將全部丟失巨双。如果從后端數(shù)據(jù)庫(比如:mysql)恢復(fù)這些數(shù)據(jù)噪猾,那會出現(xiàn)大的緩存穿透,會給數(shù)據(jù)庫帶來巨大的壓力筑累;而且從慢速數(shù)據(jù)庫中讀取性能肯定比不上從 Redis 中讀取袱蜡,導(dǎo)致使用程序響應(yīng)變慢。所以慢宗,對 Redis 來說坪蚁,實(shí)現(xiàn)數(shù)據(jù)的持久化很重要。
目前镜沽,Redis 的持久化主要有兩大機(jī)制敏晤,即 AOF(Append Only File)日志和 RDB 快照。
寫后日志:AOF
Redis 是先執(zhí)行命令缅茉,把數(shù)據(jù)寫入內(nèi)存嘴脾,然后才記錄日志,如下圖所示:
傳統(tǒng)數(shù)據(jù)庫的日志蔬墩,例如 redo log(重做日志)译打,記錄的是修改后的數(shù)據(jù)耗拓,而 AOF 里記錄的是 Redis 收到的每一條命令,這些命令是以文本形式保存的扶平。
我們以 Redis 收到“set testkey testvalue”命令后記錄的日志為例帆离,看看 AOF 日志的內(nèi)容。其中结澄,“*3”表示當(dāng)前命令有三個部分哥谷,每部分都是由“$+數(shù)字”開頭,后面緊跟著具體的命令麻献、鍵或值们妥。這里,“數(shù)字”表示這部分中的命令勉吻、鍵或值一共有多少字節(jié)监婶。例如,“$3 set”表示這部分有 3 個字節(jié)齿桃,也就是“set”命令惑惶。
寫后日志的好處:
1,為了避免額外的檢查開銷短纵,Redis 在向 AOF 里面記錄日志的時候带污,并不會先去對這些命令進(jìn)行語法檢查。寫后日志這種方式保證了寫入日志的命令都是合法的香到。
2鱼冀,是在命令執(zhí)行后才記錄日志,所以不會阻塞當(dāng)前的寫操作悠就。
AOF落盤策略
Always千绪,同步寫回:每個寫命令執(zhí)行完,立馬同步地將日志寫回磁盤梗脾;
Everysec荸型,每秒寫回:每個寫命令執(zhí)行完,只是先把日志寫到 AOF 文件的內(nèi)存緩沖區(qū)炸茧,每隔一秒把緩沖區(qū)中的內(nèi)容寫入磁盤帆疟;
No,操作系統(tǒng)控制的寫回:每個寫命令執(zhí)行完宇立,只是先把日志寫到 AOF 文件的內(nèi)存緩沖區(qū),由操作系統(tǒng)決定何時將緩沖區(qū)內(nèi)容寫回磁盤自赔。
write 和 fsync
write 只要把日志記錄寫到內(nèi)核緩沖區(qū)妈嘹,就可以返回了,并不需要等待日志實(shí)際寫回到磁盤绍妨;
fsync 需要把日志記錄寫回到磁盤后才能返回润脸,時間較長柬脸。
當(dāng)寫回策略配置為 everysec 時,Redis 會使用后臺的子線程異步完成 fsync 的操作毙驯。
always 策略并不使用后臺子線程來執(zhí)行倒堕。
落盤策略性能比較
Always可以做到基本不丟數(shù)據(jù)(執(zhí)行瞬間立刻宕機(jī),還未落盤還是會丟失數(shù)據(jù))爆价,缺點(diǎn):每一個寫命令后都有一個慢速的落盤操作垦巴,不可避免地會影響主線程性能;
AOF 重寫機(jī)制
AOF 是以文件的形式在記錄接收到的所有寫命令铭段。隨著時間推移骤宣,AOF 文件會不斷膨脹。需要注意 AOF 文件過大帶來的性能問題序愚。(如果發(fā)生宕機(jī)憔披,AOF 中記錄的命令要一個個被重新執(zhí)行,用于故障恢復(fù)爸吮,如果日志文件太大芬膝,整個恢復(fù)過程就會非常緩慢),這時候就需要用到AOF 重寫機(jī)制形娇。
重寫機(jī)制通過“多變一”(舊日志文件中的多條命令锰霜,在重寫后的新日志中變成了一條命令)的方法,縮小 AOF 文件埂软。
什么時候會觸發(fā)AOF 重寫?
有兩個配置項(xiàng)在控制AOF重寫的觸發(fā)時機(jī):
1, auto-aof-rewrite-min-size: 表示運(yùn)行AOF重寫時文件的最小大小锈遥,默認(rèn)為64MB
2, auto-aof-rewrite-percentage: 這個值的計算方法是:當(dāng)前AOF文件大小和上一次重寫后AOF文件大小的差值,再除以上一次重寫后AOF文件大小勘畔。也就是當(dāng)前AOF文件比上一次重寫后AOF文件的增量大小所灸,和上一次重寫后AOF文件大小的比值。
AOF文件大小同時超出上面這兩個配置項(xiàng)時炫七,會觸發(fā)AOF重寫爬立。
AOF重寫對主線程的影響
和 AOF日志落盤不同浑劳,重寫過程是由后臺子進(jìn)程 bgrewriteaof 來完成的塘慕。這個過程并不會阻塞主線程蒋院。
重寫的過程總結(jié)為“一個拷貝坊夫,兩處日志”万皿。
一個拷貝
每次執(zhí)行重寫時化漆,主線程 fork 出后臺的 bgrewriteaof 子進(jìn)程棠绘。此時苟翻,fork 會把主線程的內(nèi)存映射拷貝一份給 bgrewriteaof 子進(jìn)程的止,這里面就包含了數(shù)據(jù)庫的最新數(shù)據(jù)檩坚。然后,bgrewriteaof 子進(jìn)程就可以在不影響主線程的情況下,逐一把拷貝的數(shù)據(jù)寫成操作匾委,記入重寫日志拖叙。
兩處日志
舊的AOF日志:因?yàn)橹骶€程未阻塞,仍然可以處理新來的操作赂乐。Redis 會把這個操作寫到它的緩沖區(qū)(很快就合到AOF日志中薯鳍,需要看落盤策略)。這樣一來挨措,即使宕機(jī)了挖滤,這個 AOF 日志的操作仍然是齊全的,可以用于恢復(fù)运嗜。
新的AOF日志:AOF 重寫日志壶辜。這個操作也會被寫到重寫日志的緩沖區(qū)。(拷貝完才合到日志中)這樣担租,重寫日志也不會丟失最新的操作砸民。等到拷貝數(shù)據(jù)的所有操作記錄重寫完成后,重寫日志記錄的這些最新操作也會寫入新的 AOF 文件奋救,以保證數(shù)據(jù)庫最新狀態(tài)的記錄岭参。此時,我們就可以用新的 AOF 文件替代舊文件了尝艘。
aof緩沖區(qū):是正常使用aof作為數(shù)據(jù)落地中間地帶演侯,所有的數(shù)據(jù)先到aof緩沖區(qū)再到aof文件中。
aof重寫緩沖區(qū): 是aof重寫時背亥,redis還要繼續(xù)接收數(shù)據(jù)秒际,這個數(shù)據(jù)就寫到aof重寫緩沖區(qū),當(dāng)aof重寫ok時狡汉,主進(jìn)程在把a(bǔ)of重寫緩沖區(qū)的數(shù)據(jù)寫到aof緩沖區(qū)娄徊,最后fsync到aof文件中。
AOF重寫的時候盾戴,子線程會首先拷貝必要的數(shù)據(jù)結(jié)構(gòu)包括內(nèi)存頁表寄锐,完成了這個操作就可以進(jìn)行重寫,只不過父子進(jìn)程這個時候指向的是同一個內(nèi)存尖啡,在子進(jìn)程重寫過程中若父進(jìn)程操作了已有的key橄仆,則會重新申請新的內(nèi)存,這樣父子進(jìn)程就逐漸的擁有獨(dú)自的內(nèi)存空間衅斩。
總結(jié)Linux的Copy On Write技術(shù):
1盆顾,fork出的子進(jìn)程共享父進(jìn)程的物理空間,當(dāng)父子進(jìn)程有內(nèi)存寫入操作時畏梆,read-only內(nèi)存頁發(fā)生中斷椎扬,將觸發(fā)的異常的內(nèi)存頁復(fù)制一份(其余的頁還是共享父進(jìn)程的)惫搏。
2,fork出的子進(jìn)程功能實(shí)現(xiàn)和父進(jìn)程是一樣的蚕涤。如果有需要,我們會用exec()把當(dāng)前進(jìn)程映像替換成新的進(jìn)程文件铣猩,完成自己想要實(shí)現(xiàn)的功能揖铜。
Copy On Write機(jī)制了解一下
簡單來說就是重寫是復(fù)制一份地址映射,父線程只要改動了达皿,子線程就開辟新的空間天吓,映射修改。然后還記錄一份緩沖區(qū)緩存日志峦椰,等備份完再執(zhí)行一下緩沖區(qū)的日志龄寞。
Redis采用fork子進(jìn)程重寫AOF文件時,潛在的阻塞風(fēng)險包括:fork子進(jìn)程 和 AOF重寫過程中父進(jìn)程產(chǎn)生寫入的場景
fork子進(jìn)程汤功,fork這個瞬間一定是會阻塞主線程的物邑,fork采用操作系統(tǒng)提供的寫實(shí)復(fù)制(Copy On Write)機(jī)制,就是為了避免一次性拷貝大量內(nèi)存數(shù)據(jù)給子進(jìn)程造成的長時間阻塞問題滔金,但fork子進(jìn)程需要拷貝進(jìn)程必要的數(shù)據(jù)結(jié)構(gòu)色解,其中有一項(xiàng)就是拷貝內(nèi)存頁表(虛擬內(nèi)存和物理內(nèi)存的映射索引表),這個拷貝過程會消耗大量CPU資源餐茵,拷貝完成之前整個進(jìn)程是會阻塞的科阎,阻塞時間取決于整個實(shí)例的內(nèi)存大小,實(shí)例越大忿族,內(nèi)存頁表越大锣笨,fork阻塞時間越久。
拷貝內(nèi)存頁表完成后道批,子進(jìn)程與父進(jìn)程指向相同的內(nèi)存地址空間错英,也就是說此時雖然產(chǎn)生了子進(jìn)程,但是并沒有申請與父進(jìn)程相同的內(nèi)存大小屹徘。那什么時候父子進(jìn)程才會真正內(nèi)存分離呢走趋?“寫實(shí)復(fù)制”顧名思義,就是在寫發(fā)生時噪伊,才真正拷貝內(nèi)存真正的數(shù)據(jù)簿煌,這個過程中,父進(jìn)程也可能會產(chǎn)生阻塞的風(fēng)險鉴吹,就是下面介紹的場景姨伟。
fork出的子進(jìn)程指向與父進(jìn)程相同的內(nèi)存地址空間,此時子進(jìn)程就可以執(zhí)行AOF重寫豆励,把內(nèi)存中的所有數(shù)據(jù)寫入到AOF文件中夺荒。但是此時父進(jìn)程依舊是會有流量寫入的瞒渠,如果父進(jìn)程操作的是一個已經(jīng)存在的key,那么這個時候父進(jìn)程就會真正拷貝這個key對應(yīng)的內(nèi)存數(shù)據(jù)技扼,申請新的內(nèi)存空間伍玖,這樣逐漸地,父子進(jìn)程內(nèi)存數(shù)據(jù)開始分離剿吻,父子進(jìn)程逐漸擁有各自獨(dú)立的內(nèi)存空間窍箍。因?yàn)閮?nèi)存分配是以頁為單位進(jìn)行分配的,默認(rèn)4k丽旅,如果父進(jìn)程此時操作的是一個bigkey椰棘,重新申請大塊內(nèi)存耗時會變長,可能會產(chǎn)阻塞風(fēng)險榄笙。另外邪狞,如果操作系統(tǒng)開啟了內(nèi)存大頁機(jī)制(Huge Page,頁面大小2M)茅撞,那么父進(jìn)程申請內(nèi)存時阻塞的概率將會大大提高帆卓,所以在Redis機(jī)器上需要關(guān)閉Huge Page機(jī)制。Redis每次fork生成RDB或AOF重寫完成后乡翅,都可以在Redis log中看到父進(jìn)程重新申請了多大的內(nèi)存空間鳞疲。
當(dāng)主線程使用后臺子線程執(zhí)行了一次 fsync,需要再次把新接收的操作記錄寫回磁盤時蠕蚜,如果主線程發(fā)現(xiàn)上一次的 fsync 還沒有執(zhí)行完尚洽,那么它就會阻塞。所以靶累,如果后臺子線程執(zhí)行的 fsync 頻繁阻塞的話(比如 AOF 重寫占用了大量的磁盤 IO 帶寬)腺毫,主線程也會阻塞,導(dǎo)致 Redis 性能變慢挣柬。
如果業(yè)務(wù)應(yīng)用對延遲非常敏感潮酒,但同時允許一定量的數(shù)據(jù)丟失,那么邪蛔,可以把配置項(xiàng) no-appendfsync-on-rewrite 設(shè)置為 yes急黎。這個配置項(xiàng)設(shè)置為 yes 時,表示在 AOF 重寫時侧到,不進(jìn)行 fsync 操作勃教。也就是說,Redis 實(shí)例把寫命令寫到內(nèi)存后匠抗,不調(diào)用后臺線程進(jìn)行 fsync 操作故源,就可以直接返回了。
落盤時機(jī)和重寫機(jī)制都是在“記日志”這一過程中發(fā)揮作用的汞贸。例如绳军,落盤時機(jī)的選擇可以避免記日志時阻塞主線程印机,重寫可以避免日志文件過大。但是门驾,在“用日志”的過程中射赛,也就是使用 AOF 進(jìn)行故障恢復(fù)時,我們?nèi)匀恍枰阉械牟僮饔涗浂歼\(yùn)行一遍奶是。再加上 Redis 的單線程設(shè)計咒劲,這些命令操作只能一條一條按順序執(zhí)行,這個“重放”的過程就會很慢了诫隅。
有沒有既能避免數(shù)據(jù)丟失,又能更快地恢復(fù)的方法呢帐偎?當(dāng)然有逐纬,那就是 RDB 快照了。
摘抄:《Redis 核心技術(shù)與實(shí)戰(zhàn)》-第4節(jié)