在Redis的使用當中,持久化一直是一個比較重要的話題,很多同學在使用Redis的過程中對持久化政策如何選擇、如何配置持久化存在疑問。本文試圖對Redis的持久化做比較系統地分析比較,以期達到能夠正确了解Redis的持久化,并且能夠結合應用實際選擇合理的持久化機制的目的。
持久化是一個資料存儲世界中的普遍話題。持久化可以描述為将關心的資料存儲在非易失性存儲(non-volatile memory)的過程。當資料得到持久化後,即使發生一定程度的故障,隻要持久化裝置不損壞,我們都可以利用持久化的資料進行恢複,在一定程度上降低故障帶來的損失。
我們使用的資料存儲,不論是傳統的關系型資料庫還是類似Redis的所謂NoSQL,當他們運作起來之後,都是一個運作于作業系統之上的程式,由應用程式發送的資料都需要由這些資料存儲程式先進行一定的處理,然後才能按照一定的格式進行持久化存儲。這個過程中資料必然先經過應用程式的記憶體,由CPU進行一定的運算,然後才能存儲到持久化裝置上。是以要了解持久化,就先得具備幾個方面的背景知識。
在Linux中,為了保證作業系統穩定、高效運作,記憶體空間被劃分為使用者空間和核心空間。簡單來說,各個應用程式一般情況下位于使用者空間,而作業系統核心運作于核心空間。之是以要做如此劃分,是因為對作業系統很多函數的調用,都會觸發敏感資源的操作,例如清理記憶體、設定時鐘等等。為了防止各個程式操作敏感資源造成系統崩潰,将記憶體空間進行了上述劃分。在使用者空間内,應用程式并不具備對敏感資源操作的權限(相應的指令被限制執行),這樣就避免了可能發生的系統崩潰等等異常現象。
我們知道,絕大部分應用程式都避免不了和底層資源打交道,例如從磁盤讀取資料、從網絡讀取或者接收封包等。但是,劃分了使用者空間和系統空間後,應用程式沒有權限通路系統資源。為了解決這個問題,作業系統暴露了一系列系統調用接口(System Call Interface)供應用程式和底層資源互動,這樣應用程式可以通過調用這些接口實作資源的通路。我們常見的系統調用有write(),read()等等。
那麼當應用程式觸發了系統調用接口之後會發生什麼呢。我們以寫入某個檔案為例,當應用程式調用了寫入操作,系統并不會直接通路硬碟進行檔案寫入,而是首先将檔案寫入核心緩沖區,此後再定時批量将核心緩沖區中的資料寫入磁盤(這個過程也可以由應用程式通過觸發調用fsync()來完成)。非常明顯,之是以會劃分出核心緩沖區,主要是為了解決底層IO讀寫速度和記憶體讀寫速度的不比對,如果頻繁地同步寫入硬碟,将會嚴重拖慢程式的運作速度。
總的來說,對于了解持久化的過程,我們需要知道:在Linux作業系統中,記憶體被劃分為使用者空間和核心空間,當應用程式中的資料需要寫入檔案時,會依次經過應用程式記憶體、核心緩沖區最終寫入硬碟中,這個過程中涉及到的系統調用有寫入操作write()和強制硬碟同步操作fsync()。
我們知道,在linux系統中,可以通過fork()調用建立出一個與父程序完全相同的子程序拷貝。但是,在fork的過程中,如果父程序所占用的記憶體空間過大,單單完成記憶體資料的拷貝,耗時可能就會很久,這樣會阻塞父程序響應其他指令,這對一個面向客戶的服務端程式來說,是不能接受的。
為了解決這個問題,大佬們提出了寫時複制技術。簡單來說,寫時複制技術是指,在fork的過程中,對應用程式的記憶體不進行整個拷貝,而是在子程序中建立一個指向父程序對應記憶體位址的引用,隻有當子程序讀取記憶體并且發現資料在拷貝之後發生了新的寫入時,才進行實際的拷貝動作。這樣,由于在fork的過程中不需要完成整個資料的拷貝,大大降低了fork()調用的耗時。
但實際上,寫時複制技術并沒有完全解決問題。比如,某些大型應用程式,占用的記憶體空間非常大(例如一個占用幾十G記憶體的Redis執行個體),僅僅完成記憶體位址的指向就會耗時很久。
最後,對于Redis持久化來說,我們隻需要知道,在持久化的過程中會涉及fork調用(RDB方式和AOF重寫時發生),雖然Linux采用了寫時複制技術,在執行個體記憶體占用較大時,fork調用仍舊可能帶來長時間的阻塞。
前面我們簡要讨論了Linux的記憶體空間劃分以及寫時複制(Linux對fork調用的消耗做出的一種優化,即使進行了這種優化,高記憶體占用的執行個體在fork時仍可能長時間阻塞)這兩個背景知識點。下面我們對持久化過程中資料的寫入過程進行分析。
當我們采用某種資料存儲儲存應用資料的時候,資料由應用程式通過網絡發送至資料存儲程式,再由資料存儲程式進行加工計算,然後以一定的格式存儲在硬碟等持久化裝置中。這個過程涉及到網絡調用、程式加工、作業系統寫入檔案等多個步驟,為了更清晰地進行接下來的讨論,我們把這個步驟進行一定的分解。簡要來說,應用資料從産生到被存儲到持久化裝置,經過了如下步驟:
用戶端向資料庫服務端發送寫入或者更新資料的請求,此時資料位于用戶端記憶體中
服務端接收到寫指令,此時資料位于服務端資料庫應用記憶體中(站在服務端伺服器視角,資料位于應用(資料庫)記憶體中,即使用者态記憶體)
資料庫調用系統函數向硬碟寫資料,此時資料位于核心緩沖區中(kernel’s buffer)
作業系統将資料從寫緩沖區轉移到硬碟控制器,此時資料位于硬碟緩沖區(disk cache)
硬碟控制器将資料實際寫入實體媒體中
通常步驟②的實作因資料庫的實作不同而不同,但相同的是,最終都會觸發調用系統寫函數進行資料寫入。步驟③也因不同系統的實作而不同,但在讨論目前問題時,可将其視為單獨的一步而不用關心其細節。
通過上述分解的步驟我們看到,資料先後經過了用戶端(應用程式)記憶體、服務端(資料存儲程式)記憶體、核心緩沖區、硬碟緩沖區,最終被寫入實體媒體中。
前面我們對持久化涉及的資料流轉過程進行了分步讨論。下面我們主要從更加普遍的意義上讨論持久化,主要包括持久化的目的以及如何衡量某應用的持久化能力是否優秀。
要了解一個事物,我們必須得了解這個事物出現的原因。
有一定開發經驗的同學都知道,在計算機世界中,意外無處不在,除了由于各種原因産生的軟體Bug,硬體裝置,例如記憶體、網絡、磁盤等等都可能發生故障。更有甚者,對于一些重要性比較高的應用來說,可能還不得不考慮外部世界的幹擾,例如:由于機房空調故障導緻的伺服器停止運作,市政施工導緻的網絡線纜被挖斷等等。我們在建構應用系統時,不得不考慮這些各種各樣的異常情況,來提高我們系統的可靠性(非功能)。對于資料存儲系統來說,資料是其核心資産,如果因為一些輕微的故障導緻了資料的丢失,那麼這個資料存儲系統不僅不是可靠的,甚至可以說是不可用的。
進行持久化,可以了解為提高資料存儲系統可靠性的一種技術手段,隻有當将資料存儲到非易失性儲存設備上後,才能保證在一定程度的故障面前(自然災害面前,人類創造的事物往往不堪一擊),資料不會丢失。
那麼,對于不同的資料存儲系統,他們都實作了持久化,我們怎麼衡量他們實作的持久化到底好不好呢?前面已經說過,持久化主要解決由于各種故障引起資料丢失的問題,那麼,我們在考慮某種持久化機制的時候,可以從兩個次元對其進行考量:
故障發生後,我的資料會不會丢,會丢失多少。
故障發生後,我是不是可以利用持久化的資料進行資料恢複,這種恢複的成本和效率如何。
這可以通過如下兩個名額來衡量。
故障發生後,我們的資料是否得到了正确的儲存,進而能夠在系統恢複時得到恢複,這是我們關心的首要問題。首先要考慮的問題是,可能發生哪些故障。不考慮機房被卡車撞了或者電纜被鲨魚咬斷這種意外事件,我們可以對可能發生的異常進行如下劃分:
1. 應用級的異常。這種異常是由于諸如資料庫服務被異常關閉,如kill -9等。這時伺服器仍舊正常運轉。那麼這時當上述讨論的第③步完成時,可以認為資料是安全的。因為,即使此後資料庫應用被異常關閉,資料仍舊會被寫入實體媒體中,此後的操作已可以由作業系統獨立完成。
2. 伺服器級異常,例如掉電。這種情況下,隻有當第⑤步完成時,才可以認為資料是真正安全的。
可見,對于持久化來說,關鍵的是上述③、④、⑤三個步驟的執行情況,從另一個角度考慮三個步驟的動作,他們分别表示的含義分别是:
資料從使用者态記憶體向系統态記憶體的轉移頻率(write()操作的調用頻率)
系統多久将資料從系統态記憶體轉移到硬碟控制器中
硬碟控制器多久将資料寫入實體媒體中
在第三步中,資料存儲程式可以控制通過調用系統函數write頻率,但是調用該函數消耗的時間卻無法得到控制。因為write函數的成功傳回,依賴于寫入資料量的大小及硬碟的實際寫入能力,當硬碟無法實際處理寫入請求時,資料會被緩存到寫入緩存中,如果進一步緩存被寫滿,此時write調用将會阻塞,直至可以完成全部資料的寫入時,write調用才會成功傳回。
在第四步中,資料的轉移由硬碟控制器控制,通常該寫入頻率不會太高,因為大量碎片資料的寫入相比一次寫入大資料量更慢。在Linux的預設實作中,寫入間隔是30s。這意味着,當這一步失敗時,最多可能有30s内的資料無法持久化到硬碟中。而在實際中,可以調用系統函數fsync()強制執行該步驟。同樣地,該系統調用在無法成功完成時,也會阻塞使用者程序,同時也會阻塞對目前檔案執行寫入操作的其他程序。
第五步的實作應用層面已經無法控制,這裡我們不加讨論。
綜上讨論,我們提取要點,關于Durability(持久性)能力的讨論歸結為如下兩個問題:
應用級,由通過write()系統調用保證。
系統級,由通過fsync()系統調用保證。
以上,對Durability進行了讨論。除此之外,我們還關注持久化資料的可用性,即當發生異常時,持久化資料是否可以用來恢複現場。這裡有三種可能:
資料結構被損壞,不能恢複;
損壞的資料可以通過一定的工具得到修複;
資料可用,直接加載即可。
現有的資料存儲實作,提供如下幾類資料可用性保證:
當某節點發生異常時,資料可以通過副本(Replica)恢複,因而持久化資料是否可用無關緊要。
資料的持久化通過類似日志的方式實作(比如mysql的binlog)
資料的持久化通過追加模式的檔案實作,這種情況下,如果對檔案的寫入是保證指令級原子性的,則也可以不用考慮資料損壞的情況。除非是在第5步寫入時發生了系統異常。
小結一下,上面我們對持久化的目的-通過将資料儲存到非易失性儲存設備來提高系統的可靠性、持久化的衡量名額-Durability(資料會不會丢、可能會丢多少)和可用性(用持久化資料進行資料恢複是否麻煩)進行了讨論。
衆所周知,Redis提供了兩種持久化方式:記憶體快照方式(RDB)以及追加寫檔案方式(AOF: append only file)。
RDB持久化的相關配置相對比較簡單,主要通過save N(seconds) M(operations)的格式來實作,該配置表示當在N秒内,若至少發生了M次寫入操作,則進行持久化。在實際當中可以配置多個觸發條件,當任意一個觸發條件滿足時,都會觸發持久化操作。
RDB采用記憶體快照的方式完成持久化,當達到觸發條件後,Redis将目前的記憶體全部資料以一定的格式儲存為快照檔案。這種快照檔案是經過壓縮的,其格式非常緊湊,适合用來進行資料備份(例如結合crontab等進行定期的資料備份)。由于是一種格式緊湊的記憶體快照檔案,當采用RDB進行資料恢複時,其效率相比于采用AOF檔案更高。
RDB的實作過程可以簡要概括如下:首先,主程序通過fork建立一個子程序;然後,子程序将資料寫入一個臨時的RDB檔案;最後,當臨時檔案完成寫入後,通過原子操作用臨時檔案替換老的RDB檔案。這裡需要注意的是,很多人認為整個持久化過程會阻塞Redis對用戶端指令的處理,事實上在RDB持久化的過程中僅有在主程序fork子程序的過程中可能造成對用戶端讀寫的阻塞(尤其是當記憶體占用較高時)。
最後,在性能影響方面,RDB的配置通常是數十秒甚至分鐘級的,是以采用RDB時對Redis性能的影響相比AOF更小。
AOF采用追加寫檔案的方式進行持久化,檔案内容是每個寫操作包含的指令和資料内容,具有較高的可讀性。要打開AOF持久化,通過如下配置實作:
AOF的完成依賴fsync調用(第2節,第四步驟),而由于fsync調用會阻塞write調用,是以fsync的調用頻率高低直接影響Redis的性能表現,這裡隻能在持久性和性能間進行取舍。可以通過如下配置設定fsync調用的頻率:
Redis提供了no/everysec/always三個配置項,分别表示任何寫入操作都觸發、每秒觸發、不顯示觸發fsync(由作業系統完成,在Linux下,這個時間間隔通常為30s),預設的配置為everysec。這三種配置依次提供了更高的資料持久化保證,同時也帶來了更加明顯的性能影響。
由于采用追加寫的方式,随着寫入操作的累積,AOF會逐漸增長,可能會占用巨大的空間。為此,Redis實作了AOF重寫機制。AOF重寫的出發點在于,一段時間内多某個key進行若幹次寫操作,都會被記錄到AOF檔案中,而目前的資料僅包含一種狀态,那麼可以将這段時間内的寫入操作進行合并,這樣可以降低AOF檔案的空間占用。AOF重寫相關的配置如下:
與RDB類似,AOF重寫也采用fork子程序的方式,其步驟較RDB稍微複雜一些:
fork 出子程序
子程序在臨時檔案中進行AOF重寫
父程序同時保持兩個動作:
繼續對所有的寫請求,向原AOF檔案寫入,保證資料的安全性
将所有新來的寫請求記錄到一塊單獨的記憶體緩沖區中。
當子程序完成重寫後,父程序收到通知,将記憶體緩沖區中的新的變化追加到臨時檔案中
将兩個檔案進行交換重命名,并向啟用新的AOF檔案。
持久性方面:使用者可以定義當滿足某種條件時即進行快照持久化,通常,考慮到性能問題,這個時間間隔會設定為數十秒甚至分鐘級。是以RDB方式并不能提供很好的持久性,若在兩次持久化間發生故障,可能造成較大規模資料的丢失。
可用性方面:在不考慮首次RDB生成的情況下,首次以後的RDB檔案的生成Redis的實作采用了雙檔案的機制,即在進行RDB時,先生成新的臨時檔案,當新臨時檔案生成完成時,通過原子性的系統函數rename進行重命名。是以,在大部分的情況下(除了首次RDB),Redis都保證RDB檔案是可用的。
AOF方式以追加的方式進行持久化,是以提供了較好的持久化資料可用性。需要注意的是,采用AOF檔案進行資料恢複時,效率不如RDB。
持久性方面,與fsync觸發頻率配置相關。對于常用的everysec選項,Redis至少保證2s的資料持久時間間隔,是以,最差情況下,最多有2s的資料是不可用的。對于always,則是指令級别;而對于no,依賴于作業系統的不同實作,Linux的預設實作中磁盤資料持久化的時間間隔為30s。
綜上,RDB在持久性方面不夠但持久化資料的可用性較好。AOF在持久性和可用性方面均表現良好。
魯迅曾經說過,離開具體應用場景談一項實作的優劣都是耍流氓,持久化機制的選擇也是一樣。總的來說,有四種選項供我們選擇:都不選(裸奔,飛一般的感覺),選擇RDB,選擇AOF,我全都要(鳌拜臉)。針對這四種進行簡要讨論:
都不選。遇到的這種情況的應用場景通常都是:我就緩存個資料,丢了就丢了,資料庫裡面反正還有,我要的是性能你明白我意思吧。不可否認,這是一種非常有效的辦法,但是,這裡存在一個問題:假如某天應用發生了異常,開發同學懷疑是Redis資料有問題導緻的,但禍不單行(禍往往真的就不單行),還沒查到原因呢,Redis重新開機了,這種情況下可能就會變成無頭案。這個時候如果我們打開了RDB,并且通過定時任務或者其他手段對RDB檔案進行了備份,持久化就會變得非常香了。
這種情況可能很多同學還是會說,丢了就丢了,出現這種情況我認了,一開RDB,fork拖慢我的響應,得不償失啊。針對這種想法,首先,墨菲定律在這裡舉起了他的小手。其次,從前面的讨論可以得知,fork影響Redis響應的情況主要是對于單個執行個體存儲的資料量過大的情況。這裡我們推薦可以通過采用更多的小記憶體占用的機器構成多執行個體的叢集,而不是較少的大記憶體機器構成叢集。因為:1. Redis是單線程處理指令的,理論上多台機器能多用個核(畢竟缸多馬力大);2. 一台低配的X86他也不貴啊。
選擇RDB或者選擇AOF。二者擇其一,反而比較簡單。從前面的讨論我們知道,這兩者者的取舍,無非是性能和持久性之間做權衡(沒辦法,隻有付出才能得到),隻要我們應用的場景能夠容忍相應時間的資料丢失,選擇對應的持久化機制即可。
我全都要。實際當中,我們當然希望建構一個足夠健壯的系統,我們可以綜合利用RDB适合用來備份以及恢複效率高和AOF提供的更加好的持久性保證。但是,我們必須關注兩種同時存在的情況下是否會帶來額外的不利因素。從前面的讨論我們知道,不論是RDB還是AOF,都是将資料持久化到硬碟上,但是硬碟的寫入能力是有限的,在兩種持久化方式都打開的情況下,尤其是假如RDB和AOF重寫同時發生,這個時候可能更容易造成達到硬碟的寫入能力瓶頸的情況,如果無法在短時間内完成檔案寫入,那麼後續的fsync和write都可能阻塞。這顯然是我們不願意看到的(生産環境中,我就遇到過系統同時打開兩種持久化的情況下,由于達到硬碟寫入瓶頸而導緻的阻塞)。當然,這并不是說我們不建議同時打開兩種持久化,假設我們擁有較高性能的硬碟,同時Redis面臨的寫入壓力并不是很高,完全可以采用兩種持久化結合的方式來獲得一個更加穩健的系統。
以上,我們對Redis的持久化進行了讨論。主要涉及以下幾個方面的内容:
首先,為了更好的了解持久化,介紹了使用者空間、核心空間以及核心緩沖區幾個Linux記憶體劃分涉及的基礎概念,以及Linux在程序fork過程中涉及的寫時複制技術。這兩點内容主要涉及Redis在持久化過程中資料的轉移過程,以及RDB檔案寫入以及AOF重寫的過程。
其次,我們分步驟讨論了持久化過程中資料在機器上的流轉過程,主要是從使用者态記憶體空間轉移到核心緩沖區再轉移到最終的持久化裝置。在轉移的過程中涉及到應用程式觸發不同的系統調用。
再次,我們讨論了持久化的目的以及如何衡量持久化能力,可以從Durability以及可用性兩個方面進行。
最後,我們對Redis兩種持久化模式參數配置、機制進行了對比介紹,然後采用Durability和可用性進行了對比分析,最終讨論了如何結合實際應用場景選擇持久化機制。在持久化機制的實際選擇時,需要結合應用場景進行具體分析,從性能要求、持久化能力要求以及系統健壯性幾個方面綜合考慮,做出适合實際應用場景的選擇。
本文大量參考了Redis作者對Redis持久化的讨論,參考文檔如下:
【REF】http://oldblog.antirez.com/post/redis-persistence-demystified.html
歡迎關注公衆号:程式員順仔和他的朋友們,回複【資料】,即可獲得多本架構進階電子書籍。