Redis存儲揭祕(翻譯)

Redis存儲揭祕(翻譯)

原文地址: http://oldblog.antirez.com/post/redis-persistence-demystified.htmlhtml

我在Redis的部分工做是閱讀博客,論壇消息以及推特上關於Redis的搜索。對於一個開發者來講,社區用戶以及非用戶的對他開發的產品的見解很是重要。個人感觸是Redis的持久化被人誤解很是多。mysql

在這篇博客中,我會努力的作到公正:不安利Redis,不跳過可能讓Redis有負面影響的細節。我想要提供一個清晰的,好理解的Redis存儲的流程圖,它有多可靠,以及和其餘數據庫的對比。linux

##操做系統和磁盤redis

首先咱們能夠探討一下數據庫如何作到耐久性。爲此,咱們能夠模擬一下一個簡單的寫入操做。sql

  • 1: 客戶端發送一個寫命令到數據庫(數據存儲客戶端的內存中)。
  • 2:數據庫接收了這條命令(存儲到了數據庫的內存)。
  • 3;數據庫調用system call(系統級調用),嘗試寫入到硬盤(這一步實際上寫入到了操做系統內核的緩衝區)。
  • 4:操做系統將緩衝區的數據寫入到磁盤控制器(數據轉移到磁盤緩衝區)。
  • 5:磁盤控制器將數據真實寫入到物理介質(硬盤,閃存……)

注意:上述步驟是很是簡化的,真實環境的緩存更爲複雜。數據庫

第二步在數據庫中,每每被實現爲一個複雜的緩存系統,有時候寫入會在不一樣的線程或是進程執行(接收線程和IO線程分離)。以咱們的視角,數據庫早晚會將數據寫入到磁盤。換言之,在內存中的數據會在某一時刻傳輸到操做系統內核(第三步)。緩存

第三步中有一個較大的遺漏。由於真實的操做系統更爲複雜,它實現了多個不一樣的緩存層。一般的,有文件系統緩存(在linux中被稱爲page chche,頁面緩衝),以及一個小一些的寫緩衝會緩存將要寫入到硬盤的數據。使用特定的API能夠繞開這兩層(例如linux中的O_DIRECT和O_SYNC參數)。以咱們的視角,咱們能夠認爲有一層不透明的緩存層(咱們不明確實現細節)。這已經足夠說明,若是數據庫實現了本身的緩存系統,頁面緩衝通常會被禁用。由於數據庫和內核會在同時作一樣的事情(引發不良後果)。寫緩衝通常會開啓,由於頻繁的提交到磁盤對於大部分軟件來講太慢了。安全

在真正的實現中,數據庫不會老是調用系統調用來同步寫緩衝到磁盤,僅在必須的時候調用。服務器

在何時,咱們的寫是安全的

若是錯誤發生在數據庫系統(管理員殺掉進程或者崩潰),若是第三步調用成功了(已經進入到頁面緩衝),就能夠認爲數據已經安全寫入了。這一步以後即便數據庫崩潰,操做系統內核仍會將數據安全的轉移到磁盤控制器。數據結構

若是咱們考慮更嚴峻的事件,例如斷電,那麼只有第五步寫入磁盤成功後,數據纔是安全的。

步驟三、四、5中針對數據安全性的的重要階段,咱們能夠總結一下。

  • 數據庫間隔多久調用一次系統調用,將數據從用戶空間寫入到內核(即從數據庫的內存到操做系統緩衝)。
  • 系統內核間隔多久將數據刷入到磁盤控制器。
  • 磁盤控制器多久將數據寫入到物理介質。

注意 上文提到的磁盤控制器,主要是表達緩存的行爲,不管是控制器仍是磁盤自己。在耐久性要求很高的環境中,系統管理員通常會禁用這一層緩存。

默認的,磁盤控制器會穿透緩存直接寫(即,僅有讀緩存,沒有寫緩存)。若是有斷電保護設備,那麼啓用寫緩衝是安全的。

操做系統API

以數據庫開發人員的角度看,數據真正到達物理介質前的數據流轉是頗有意思的,但更有意思的是,API提供的各類控制。

從第三步開始說明。咱們可使用write系統調用將數據傳入系統內核緩存,從這點上看,咱們藉助系統API能夠良好的控制行爲。然而咱們並不清楚這個系統調用須要執行多久纔會返回成功。內核的寫入緩衝有大小限制,若是磁盤不能應對寫入帶寬,內核寫入緩衝會達到極限值,這時系統會阻塞咱們的寫入。當磁盤能夠接收新的數據,write系統調用纔會返回。全部的操做執行完,數據寫入到物理介質。

第四步,這一步,系統內核將數據傳入磁盤控制器。默認的,系統會限制寫入頻率,由於傳輸更大的塊寫入更快。例如,在Linux中,30秒以後寫入纔會被真正提交。這意味着,若是有故障,最近30秒的數據有可能丟失。

系統API提供了一系列系統調用來強制將內核緩衝寫入到硬盤,最有名的是fsync系統調用(更多信息能夠搜索msync,fdatasync)。數據庫使用fysnc將內核中的數據提交到硬盤,可是能夠猜想到,這是一個很是昂貴的操做:若是fsync被調用而且內核緩衝有數據的時候,會當即執行執行一個寫操做。fsync會一直阻塞寫入的進程,在Linux上,fsync還會阻塞寫入同一個文件的其餘線程。

哪些是咱們沒法控制的

目前爲止,咱們學習到能夠控制第三、4步,第5步能夠麼?正式的說,咱們經過系統API沒法控制。也許一些內核的實現能夠通知驅動提交數據到物理介質,或者爲了速度考慮,磁盤控制器本身記錄數據但不當即提交,而是過幾個毫秒或更久再提交。這已經脫離了咱們的控制。

在接下來的文章中,咱們會簡化到兩種數據安全級別:

  • 經過write系統調用,將數據寫入到內核緩衝區。即便用戶系統(例如數據庫)故障,仍保證數據安全性。
  • 經過fsync系統調用,將數據提交到磁盤。提供了完整的數據安全性,操做系統故障,斷電,仍保證數據安全性。咱們明確的知道這個沒有保障性,由於磁盤控制器還有緩存。咱們不對此作考慮,是由於對於大部分數據庫系統這是不變的。另外,系統管理員可使用一些特殊的工具來控制磁盤設備的寫入行爲。

注意 不是全部的數據庫都使用系統API。一些專有的數據庫使用一些內核模塊能夠直接和硬件設備交互。然而,面臨的問題是相似的。你可使用用戶空間的內存,內核緩存,或早或晚將數據寫入到磁盤來保障安全性。Oracle就是使用內核模塊的一個例子。

數據損壞

前面的文章中,咱們從幾個方面分析了確保數據安全寫入到磁盤的的一些問題:應用程序(用戶空間),系統內核。然而這並非困擾耐久性的惟一問題。另外一個問題以下:數據庫在災難後是否能夠恢復?或者內部的結構由於某種行爲被破壞,致使它不能被正確讀取,須要恢復步驟來重建結構。

不少SQL和NoSQL數據庫實現了基於磁盤的樹形結構來保存數據和索引。這種數據結構在寫入的時候會被調整。若是在寫入過程當中,系統中止工做了,那麼樹形結構還能保持正確麼?

通常的,這裏有3種數據故障安全級別:

  • 數據庫不關心寫入磁盤故障,要求用戶使用一個副本作數據恢復。或者提供工具來嘗試重建內部結構。
  • 數據庫記錄操做日誌,在故障後經過日誌重放保證一致性。
  • 數據庫從不修改已經寫入的數據,僅在後面調價,因此沒有數據損壞。

如今咱們擁有了全部的知識,用來評估數據庫系統持久層的可靠性。如今去檢驗Redis在這一點上的表現。Redis提供了兩種不一樣的持久化模式,咱們會一一的檢驗。

Snapshotting 快照

Redis快照是最簡單的Redis持久化模式。它將再如下場景產生某一時刻的快照,前一個快照時間點已經超過2分鐘,同時有100個寫入。這個觸發條件能夠用戶自定義,能夠修改配置文件重啓,也能夠在運行期配置。快照會生成一個.rdb文件,文件中包含了整個數據集。

快照的耐久性被用戶定義的觸發條件所限制。若是數據每隔15分鐘存儲一次,若是Redis進程崩潰或者更嚴重的時間,那麼至多有15分鐘的數據會丟失。從這點來看Redis的MULTI/EXEC事務或許完整存儲到快照,或許徹底沒有存儲。

RDB文件不會發生數據損壞。首先它啓動子進程將Redis內存中的數據寫入到鏡像,而後在文件後面追加。一個新的rdb快照被建立成臨時文件,子進程寫入成功後,經過原子性的重命名到目標文件(僅在調用fsync系統調用持久化到磁盤後發生)。

Redis快照提供不了足夠的耐久性,若是沒法忍受幾分鐘的數據丟失。這種存儲模式僅適合數據丟失影響不大的場景。

然而,即便使用更加先進的「AOF」模式,咱們仍建議開啓快照模式。由於快照很是適合用來作備份,發送數據到遠程數據中心作災難恢復,或者將數據回滾到一箇舊版本。

特別要提一點,RDB快照被用來作主從同步。

  • RDB有一個附加的好處,對於給定的數據庫規模,無論在數據庫上執行什麼動做,系統上的IO數據是固定的。這個特性是大部分傳統的數據庫沒有的(包括Redis其餘的持久化,AOF)。

##追加文件

AOF是Redis主要的持久化選項。它的執行很是簡單:每一次對內存數據形成修改的寫入操做被執行,記錄該操做。日誌格式和客戶端提交給Redis的格式是一致的,因此AOF能夠經過netcat傳輸到另外一個Redis實例。或者若是須要,能夠簡單的將內容解析出來。Redis重啓的時候,會重放aof文件的內容來重建數據。

爲了描述AOF是如何工做的,我作了一個簡單的實驗。安裝Redis實例,經過如下方式啓動:

./redis-server --appendonly yes

執行一些寫入操做:


redis 127.0.0.1:6379> set key1 Hello

OK

redis 127.0.0.1:6379> append key1 " World!"

(integer) 12

redis 127.0.0.1:6379> del key1

(integer) 1

redis 127.0.0.1:6379> del non_existing_key

(integer) 0


開始的3個操做會修改數據,第四個不會,由於沒有對應的key。下面是aof文件的格式範例:


$ cat appendonly.aof

*2

$6

SELECT

$1

0

*3

$3

set

$4

key1

$5

Hello

*3

$6

append

$4

key1

$7

World!

*2

$3

del

$4

key1


如你所見,最後的刪除沒有體現,由於它沒有形成數據的修改。

就這麼簡單,被接收到的命令會被寫入到AOF,只要它修改了數據。然而,不是全部的命令會被記錄。舉個例子,在lists上面的阻塞操做會被記錄成非阻塞的命令,由於這個對於數據的影響是一致的。一樣的,INCRBYFLOAT被記錄成SET,使用計算後的最終結果作記錄,因此重載AOF文件的時候,不會由於架構不一樣產生不一樣的結果。

如今咱們知道Redis AOF僅追加文件,因此沒有數據損壞。然而這個使人滿意的特性也會成爲一個問題:在上述例子中,DEL操做的數據沒有記錄,可是仍然浪費了一些空間。AOF文件是持續增加的,如何避免它過大?

AOF重寫

當AOF過大的時候,Redis會嘗試在臨時文件重寫。重寫操做不是讀取舊的文件,而是直接讀取內存中的數據,因此Redis能夠建立儘量小的AOF文件。而且在寫一個新的文件的時候不須要從磁盤讀取。

當重寫結束後,臨時文件會經過fsync命令同步到到磁盤,替換到原先老的AOF文件。

你可能會疑惑,當重寫進程在執行的時候,正好有數據寫入到Redis,這時會發生什麼。新的數據會寫入一份到老的AOF文件,同時寫入一份到內存緩衝區,當新的AOF文件已經重寫完成後,將新的數據寫入。最後將老的AOF替換成新的。

如你所見,全部的數據僅追加在末尾,當咱們重寫AOF文件的時候,咱們仍將這些數據寫入到老的文件,直到新AOF文件建立成功。這意味着咱們的分析過程能夠不考慮AOF的重寫。真實的問題是,咱們多久調用一次write,以及多久調用一次fsync。

AOF重寫使用順序的IO操做,因此整個dump進程很是高效(沒有隨機的IO讀寫)。這個在生成RDB文件中也是一樣的。幾乎沒有隨機IO是一個數據庫中很是稀有的特性,大部分緣由是Redis僅從內存中讀取數據,不須要在磁盤上組織數據結構提供隨機訪問。磁盤文件僅用來重啓時候經過順序加載來恢復。

AOF耐久性

這一整篇文章是爲了這個段落。

Redis AOF使用用戶態緩衝數據。每一次當咱們返回到時間循環的時候,一般會將數據刷新到磁盤,使用write系統調用寫入到AOF文件。實際上有3中不一樣的配置來控制write和fsync的行爲。

這個配置由appendfsync參數控制,有3中不一樣的值:no、everysec、always。這個配置能夠在線讀取,或者經過CONFIG SET在線修改。因此能夠隨時修改而不須要關閉Redis實例。

appendfsync no

這個配置Redis徹底不執行fsync。可是,它會確認客戶端沒有使用管道:(一次請求/響應服務器能實現處理新的請求即便舊的請求還未被響應 這樣就能夠將多個命令發送到服務器,而不用等待回覆,最後在一個步驟中讀取該答覆)。指令結果會在命令成功執行後返回:已經經過write系統調用將數據寫入到系統緩存。

由於fsync沒有被調用,數據什麼時候被持久化到磁盤須要看內核的實現,好比在Linux上每30秒刷新一次。

appendfsync everysec

這個配置下,每隔一秒鐘,數據會經過write寫入到內核緩衝,而後經過fsync刷新到磁盤。 通常的,write操做在返回到事件線程的時候,幾乎都會執行,可是這不能保證。

可是,若是磁盤不能應對寫入速度,後臺的fsync調用所花的時間超過1秒鐘,Redis可能會延遲1秒鐘再寫入到系統緩衝(爲了住址寫入操做阻塞主進程。由於後臺fsync線程寫入的是同一個文件)。若是2秒時間過去,fsync未能終止,Redis最終執行一個阻塞式的寫入,無論花費多少代價。

因此在這個模式下,Redis能保證在最壞的狀況下,2秒鐘以內寫入的全部數據都會被寫入到系統緩衝,而後持久化到磁盤。在通常的場景下,每一秒鐘數據都會被提交。

appendfsync always

在這種模式下,而且不使用管道,在返回給客戶端響應以前,數據會寫入到AOF文件而且經過fsync刷新到磁盤。

這個是你能獲取到的關於耐久性的最高保障,可是它比其餘模式都要更慢。

默認的Redis配置是appendfsync everysec,它提供了速度(幾乎和appendfsync no同樣快)和耐久性的平衡。


當Redis的appendfsync配置成always的時候,它採起羣組提交的形式。這意味着,寫入的時候,Redis不是每次都提交,它可以將這些操做組合成一個write+fsync的操做。

在實踐中,這意味着你能夠用幾百個客戶端同時操做redis:fsync操做會被分解 - 因此即便在這種模式下,Redis能夠支持幾千個TPS,即便磁盤每秒只能支持100~200次寫入。

對於傳統數據庫來講,這個特性一般很難實現,可是Redis讓它變得更加簡單。

爲何採用pipeline會不同

用不一樣的方式處理客戶端使用管道的緣由是,使用pipeline的客戶端爲了速度,犧牲了響應性。開啓了pipeline後,指令會批量提交,那麼Redis就不能在下一個指令被執行前知道前一個指令的結果。對於pipeline客戶端來講,在響應前提交寫入是沒有必要的,客戶端需求的是速度。然而即便客戶端使用管道,write和fsync常常在返回到事件循環的時候發生。

AOF和Redis事務

AOF保證MULTI/EXEC事務語義上的正確性,它發現文件末尾有損壞的事務的時候,會拒絕加載。Redis有工具能夠整理AOF文件,去除掉末尾損壞的事務

注意:由於AOF只是經過單獨的write寫入(沒有同步到磁盤),不完整的事務只會出如今磁盤已經被寫滿的時候。

和PostrgreSQL對比

AOF默認配置的耐久性如何?

  • 最壞狀況:它保證write和fsync在兩秒內執行。
  • 通常狀況:給客戶端響應以前已經write,每秒鐘fsync一次。

有趣的是在這種模式下,Redis依然很是快。這裏有幾個緣由,一方面是fsync經過後臺線程執行,另外一方面是redis是文件追加的形式寫,這個是一個很大的提高。

然而,若是你須要最大程度的數據安全性保障,並且負載不高,那麼爲了達到最好的耐久性,應該配置fsync always。

這點和PostgreSQL相好比何?PostgreSQL被認爲是一個很好很可靠的數據庫。

讓咱們一塊兒閱讀PostgreSQL的文檔(注意:我只應用有趣的片斷,你能夠在這裏查看完整的PostgreSQL文檔)


fsync(boolean)

若是開啓這個參數,PostgreSQL會嘗試去保證修改被寫入到物理介質,經過fsync或者相似的方式(好比wal_sync_method)。這個能夠保證數據庫集羣能夠在操做系統或者軟件宕機後恢復到一致的狀態。

在不少場景中,不重要的事務能夠關閉同步提交,能夠提供更好的性能,不存在數據損壞的風險。


因此PostgreSQL須要fsync防止數據損壞。幸虧Redis有AOF,咱們不存在數據損壞的問題。讓咱們看下一個參數,這個和Redis的fsync策略更爲接近,儘管名字不一樣。


synchronous_commit (enum)

這個參數指定在返回給客戶端「success」以前,事務提交是否須要等待WAL記錄寫入到磁盤。正確的參數值是on、local、off。默認的安全的配置是on。當參數爲off的時候,返回給客戶端的是成功,可是事務此時尚未安全的寫入到磁盤,若是服務宕機,不能保證安全性(最大的延遲是3個wal_writer_delay)。不像fsync,這個參數設置成off不會致使數據庫的磁盤存在不一致的風險:操做系統或者數據庫宕機會引發最近提交的事務丟失,可是數據庫的狀態和拋棄這些事務的狀態是一致的。


這裏有不少類似點,咱們能夠參照來調優Redis。基本的,PostgreSQL的兄弟會告訴你,設麼是速度?禁用同步的提交多是一個好主意。就像Redis中同樣:想要速度?不要配置appendfsync always。

若是在PostgreSQL中禁用同步提交,那麼和使用Redis的appendfsync everysec很想,由於PostgreSQL默認的wal_writer_delay 延遲200毫秒,文檔中寫明瞭須要將這個延遲乘以3纔是真實的延遲,也就是600毫秒,很是接近Redis默認的1秒。


Mysql的InnoDb有相似的參數可供用戶調優。從文檔中能夠看到:

若是innodb_flush_log_at_trx_commit的配置項爲0,日誌緩衝每隔一秒鐘將數據寫入到文件並刷新到磁盤。這個配置下事務提交不會當即提交到磁盤。當配置值爲1的時候,日誌緩衝在每次事務提交的時候都會刷新到磁盤。當配置值爲2的時候,每次事務提交都會寫入到文件,可是不肯定刷新到硬盤的時間。然而,當配置值爲2的時候,仍是會每秒刷新到磁盤。須要注意的是,由於進程調度的問題,每一秒刷新不是百分百肯定的。

你能夠點擊這裏查看更多。

長話短說:雖然Redis是一個內存數據庫,可是它提供了和基於磁盤的數據庫差很少良好的耐久性。

從更實際的角度去看,Redis提供了AOF和RDB,它們能夠同時開啓(這個也是推薦配置)。同時開啓能夠提供高效的操做和耐久性。

這裏咱們討論的Redis耐久性,不僅僅適用於將Redis做爲數據存儲,也適用於作中間件,由於它提供了良好的耐久性。


因爲我的能力有限,在翻譯過程當中不免有一些疏漏,能夠在博客下面留言,或者聯繫個人郵箱moyiguke@hotmail.com修改。

原文有一些地方直譯較爲難懂,在儘可能保持原意的基礎上作了一些修改。


一些總結

持久化的核心點:write,fsync。write在語義上是寫入到文件,可是操做系統作了優化,不會直接往磁盤去寫,而是保存在頁面緩衝(page cache)中。fsync這條指令會強制將頁面緩衝的數據往磁盤寫入,可是磁盤並不會當即固化,亦它也有一層緩衝。不過磁盤的緩衝咱們通常不去考慮,簡單的認爲fsync保證了數據寫入到物理介質了。

再談爲何會有多層緩衝。考慮這樣的場景,若是持續性的寫入到磁盤,IO會一直處於高負荷,並且負載到達必定程度,磁盤沒法提供寫入。這時爲了避免丟失數據,仍須要有內存進行數據的緩衝。這是第一點,緩衝沒法避免。另外一點,緩衝能夠帶來寫入性能的提高。在內存中,將小段的數據組合成了大段的數據,而後一次性同步到磁盤,減小了不少的磁盤訪問。

這篇文章主要講了數據安全性以及耐久性。數據安全即數據落地到磁盤,耐久性即能作到什麼程度的數據同步。這兩點實際上有部分重疊,數據安全和耐久性的評價標準都是持久化到磁盤,經過fsync或者相似的實現。舉一個例子,若是某個內存數據庫宕機後沒法保留數據,啓動後是全新的,那麼它是不安全的,不耐久的。Redis在AOF默認配置下,重啓後能夠恢復數據,至多丟失兩秒數據,那麼它是安全的,耐久性是平均一秒數據的丟失。

若是在閱讀的時候,仍有一些難以理解的地方,歡迎留言給我,我將盡力解答。

相關文章
相關標籤/搜索