不懂fork和fsync?你怕是學的假Redis持久化機制

熟悉 redis 都知道,redis 持久化有 RDB 和 AOF 兩種,一種是記錄數據,另一種是記錄操作。

不過一方面,爲了學習的縱向拓深、橫向延展,提高思維開闊性和學習態度嚴謹性,以便足以在實際環境中對這些特性運用自如;
另一方面,爲了能和面試官去噴。。

我們必須對持久化機制做深入地探討。

RDB 的問題

我們先從 RDB 說起。
首先,我們都知道,RDB 是對數據的全量複製,我們一般都會在特定時間點對數據進行備份。
比如,每天 12 點準時備份;
或者,業務量更大的公司,每隔一個小時,就對數據進行一次備份。

不過,你覺得這有沒有什麼潛在的問題?

如果你不覺得,那你確實有必要好好探究一下了。

最明顯的問題,就是時點性的問題:
比如,12 點的時候,準時 RDB 全量數據到磁盤。
但是

  • 12 點開始,12 點就立即結束?
  • 持久化涉及硬盤 I/O,那麼速度絕對比內存要慢得多;
    那麼等你真正加載到硬盤,那時間絕對不止 12 點準時了;
  • 那算幾點?
    比如加載到了 13 點,你算 12 點的數據嗎?
    就算是 13 點的數據,可明明 12 點就開始存儲了!
  • 再說,你持久化的時候,需要阻塞 redis 嗎?也就是還可以繼續寫數據嗎?
  • 如果可以,那麼數據就在動態變化,你存儲的數據就不會是 12 點的真實數據,而是 12 點到 13 點整個時段內的數據
  • 如果不可以,選擇阻塞,那麼 redis 服務直接不可用;
    如果都不可用了,那對高併發系統的打擊那是巨大的。

在這裏插入圖片描述

先不談如何解決,但至少首先我們要保證的是,系統可用 !
這時毋庸置疑的。
對於我們的高併發系統而言,大多數互聯網項目,第一要保證的就是可用性;

然後我們再去想,如何保證數據的時點性。

如何保證時點性

首先,我們都知道,RDB 有兩種方式:

  • 一種是 save:阻塞直到數據存儲完畢;
  • 另一種 bgsave:後臺另起一線程去執行寫磁盤操作。

首先,save 直接不管,沒問題吧。
這年頭,哪個互聯網公司敢讓 redis 直接阻塞,去 RDB 的。

我們只看 bgsave。

首先,假設,我們要另起一個線程,來複制我們的 redis,
根據之前提及的,由於 redis 並沒有阻塞,此時還可以正常提供服務,所以就很可能有很多的寫操作。
那麼這時另一個線程寫的數據就會被改變,因此時點性就會遭到破壞。

那麼,爲了保證數據的時點性,準確無誤。(而且 redis 也確實是做到了)
於是,我們想到的方式,一般都是想到,創建後臺線程的同時,將所有數據,拷貝給這一個線程,
這樣,線程之間的數據,就會互不干擾,因此不會出現錯誤。

但是,假設一個 redis 有 6 個 G,一臺機器一共只有 8 個 G,
那麼,redis 該怎麼辦?
此時,要是拷貝數據,整個機器的內存就會直接不夠用了!
在這裏插入圖片描述

就算,內存足夠用,但是,拷貝好幾個 G 的數據,雖然在內存,但是需要花費多少的時間?
雖然內存的帶寬,比起磁盤快了很多,可以達到 G/s,
但是,面對幾個 G 的數據,仍然會有短暫的卡頓現象出現,
這對於高併發的互聯網系統,是影響很大的。
在這裏插入圖片描述

還有一點,就是在內存中拷貝大量的數據,也是需要消耗大量的 CPU 資源的。
而 redis 就是以快作爲核心。
此時,CPU 資源大量消耗,redis 的性能也就會受到影響,
因此,這也是非常不樂觀的。
在這裏插入圖片描述

那麼?
redis 到底是怎麼做的?

寫時複製

其實,對於時點性問題,redis 並沒有做什麼複雜的操作。

這歸功於 Linux 系統的 fork() 函數。

所以,如果你瞭解 Linux 的話,你就會發現,這確實非常簡單。

首先,Linux 中的線程實際上就是一個進程,不過它們共享了一部分資源,
在 Linux 中創建進程,一般會用 fork() 這個系統調用,
這個系統調用,將會創建出一個進程,並且,這個進程和父進程共享數據。
在這裏插入圖片描述

但是,Linux 用了一個很巧妙的辦法,來防止數據衝突:
就是,在一開始,父子進程對這些數據都只是可讀的,
一旦,一個進程發生了寫操作(不管是父進程、還是子進程),
這個數據就會被拷貝到另一個地址空間,然後被該線程引用。
也就是,只有寫的那一些數據纔會被拷貝,而其它大部分的數據,都不會發生拷貝,
因此,性能得以提高。
在這裏插入圖片描述

而這一點的實現,還歸功於操作系統的內存映射機制。
其實,我們的用戶進程,它們看到的內存地址,都不是真正的內存地址,而是操作系統給虛幻出來的一個地址。
這樣,操作系統,就可以對進程的內存進行控制和管理;
而在編寫程序時,也不用考慮實際的物理地址,從而可以更自由,甚至使用比實際更大的內存空間。
在這裏插入圖片描述

而在 fork() 進程時,子進程的虛擬地址空間,完全可以和父進程的虛擬地址空間一樣,
而等到寫操作產生時,虛擬地址空間還可以不用改變,而只是改變了實際的物理內存地址空間,
這對於進程來說,是透明的。
在這裏插入圖片描述

合理利用 RDB

既然 RDB 的原理,我們明白了,
但是,RDB 存在什麼問題嗎?或者說有什麼缺點嗎?

首先就是不支持拉鍊。
就是 RDB 文件永遠只有一個,這樣,一旦生成新版本,就版本就會丟棄,
所以這時候,就要我們人爲地去把 RDB 文件拷貝出去。
不過,也正因爲其簡單,也不管歷史版本,所以速度也就快。

第二,由於 RDB 是持久化,所以肯定會花費時間。
假設,一個 redis,我們整一臺好機器,給它來個一百多兩百多 G,
那麼,光一個 RDB 持久化,就可能花掉很久很久的時間。
所以,我們一般給一個 redis 的內存,不會太大,幾個 G 就差不多了,
這樣,redis 就會更輕盈,做什麼都會特別快。
這樣,速度的特性就能更好地發揮出來。

第三,就是 RDB 特有的一個特點。
就是,因爲只在時點存儲數據,比如每隔兩小時,
那麼,假設 9 點做一次 RDB,10 點 redis 掛了,那麼,就會丟失一小時的數據。
這是時點性間隔存儲無法保證的,就是不解決丟數據的事。

不過,RDB 也是有很多優點的。
第一,就是 RDB 是 redis 數據的非常緊湊的單文件時間點表示。
因此 RDB 文件非常適合備份。
比如,你可能希望在最近的 24 小時內每小時存檔一次 RDB 文件,並在 30 天之內每天保存一次 RDB 快照,
這樣,就可以在災難情況下輕鬆還原數據集的不同版本。

第二,RDB 對 redis 的性能影響較小,它做的僅僅只是創建出一個子進程,由子進程來進行持久化,
而 redis 工作進程,完全不用去關心任何持久化的事。

第三,也是 RDB 的最顯著的優勢,就是恢復速度快。
因爲是存儲的全二進制數據,因此,只需要讀到內存,就可以直接使用了。
而不像 AOF,是記錄的指令,因此恢復就要一條條指令進行恢復。

AOF

AOF 這個名字很好理解,就是 append only file,就是指向文件追加,
就是把 redis 的寫操作記錄到文件中。

這麼一聽就會感覺到有個好處,就是 redis 的每個寫,都會進行追加,那麼丟失數據就會相對少。

第二個常識就是,redis 當中,RDB 和 AOF 功能可以同時開啓,但是!!
如果開啓了 AOF,那麼就只會用 AOF 恢復。

也就是說,服務器重啓的時候,恢復的數據一定是從 AOF 來的,也就是 RDB 的數據不會被用來恢復。

好了,現在 AOF 你可以大概理解是什麼了。

無限增長問題

現在,我提出一個問題:
假設,我現在有一個 redis,運行了 10 年,且持久化方案是開啓了 AOF,
那麼,請問,10 年之後,redis 掛了。
那麼,AOF 有多大?
第二,恢復要多久?

首先,這個 10 年,redis 一直在進行增刪改的操作,
那麼,10 年之後,這個 AOF,就要包含記錄着這 10 年,它所有做過的寫操作。
那麼,這個 AOF,是不是得非常大?

假設,有一個哥們,很無聊,每天就是不斷地做着同一個操作:
創建 key
刪除 key

那麼,10 年過後,這個 AOF 就記錄着大量的操作,雖然說都相同,但是記錄都在,所以這個 AOF 就會非常大。

第二,那麼恢復的時候,redis 會不會溢出?
假設,這個 AOF 記了 10 年,已經 10T 的大小了,那麼恢復的時候,會不會溢出?

其實,這個應該很好理解,雖然 AOF 很大,但是,所有的操作,都是在 redis 內存空間足夠的情況下寫進去的;
也就是,只要之前 redis 內存足夠,那麼,恢復的時候,雖然看起來 AOF 很大,但是,實際上恢復出最終的數據,還是 redis 之前內存中存在的數據;
所以,只要之前的 redis 內存沒問題,足夠,那麼現在也不可能溢出。

第三,就是一個很關鍵的問題,恢復要多久?
再回到剛纔這個例子,10 年一共就一條數據,創建刪除、創建、刪除……
但是,雖然一條數據,但是,AOF 記錄的是寫的操作,它不知道最終結果是什麼,
所以每一個命令,它都要去復現,操作一遍。
所以,就相當於把之前 10 年 redis 的所有寫操作重新做一遍,這樣就變成了最終的數據。

那麼,所以,這樣一定很慢對不對?
假設記了 10 年,要是一直是不停地在寫,那麼,恢復,重新寫一遍所有的操作,那麼,說不定也要個好幾年對不對?

所以,這就凸顯除了 AOF 的缺點:

  • 體量無限增大
  • 恢復速度慢

當然,其實體量大也是一個優點對不對,因爲全,丟失數據少。

AOF重寫

所以,很多軟件,都會對日誌下手,
因爲,日誌的優點保住,還是不錯的,就是可以儘量不丟失數據,
並且,同時克服一下它的缺點,就是把體量變小,恢復變快。

所以,redis 的也採取了一定的做法:

在 4.0 以前,有一個機制叫做重寫。
比如,回到之前的例子,不斷創建 key,刪除 key;
那麼,你可以發現,這樣的操作,都可以抵消,對不對。
再比如,假設有一個 list,你不斷往裏面 push 1W 個 v,
那麼,是不是就可以只寫一條 push 語句,然後讓它執行 1W 次就 ok?

所以,總結一下,就是:
刪除抵消的,合併重複的。

所以,現在也知道了,最終得到的也是一個純指令的 AOF 文件,
雖然指令被削減了一部分,但是純指令,還是得一條條去恢復,所以效率還是有些低的。

所以,後來,redis 偷偷地學習了 hdfs 的優點,
就是,從 4.0 版本開始,AOF 會包含 RDB 全量,然後追加新的寫操作。
而包含一個 RDB 全量之後,就可以直接把數據給導入內存即可,不用一步一步的操作。
而追加的一部分寫操作,又可以保證數據的全。

所以在重寫的時候,會先把老的數據,以 RDB 的形式,存到 AOF 文件中,
然後,再把增量的,以指令的方式存入 AOF。

所以,AOF 就會包含二進制數據和增量的日誌,於是就成了一個混合體。
於是,這麼改進,AOF 就把兩個優點都佔有了:

  • 既有 RDB 的快;
  • 又有 AOF 的全。

fsync的間隔

這樣,明白了 AOF 之後,我們繼續回頭。
redis 既然是一個內存級 kv 數據庫,那麼,這時寫操作就會觸發 I/O,
那這樣的話,就會影響 redis 的寫速度,就會變慢。

所以,redis 給了 3 個級別:

  • NO
  • ALWAYS
  • everysec

首先,如果你還不知道 fsync 是什麼,那我得先簡單描述一下:
傳統的 UNIX 實現在內核中設有緩衝區高速緩存或頁面高速緩存,大多數磁盤 I/O 都通過緩衝進行。
當將數據寫入文件時,內核通常先將該數據複製到其中一個緩衝區中,如果該緩衝區尚未寫滿,則並不將其排入輸出隊列,而是等待其寫滿或者當內核需要重用該緩衝區以便存放其他磁盤塊數據時,再將該緩衝排入輸出隊列,然後待其到達隊首時,才進行實際的 I/O 操作。
這種輸出方式被稱爲延遲寫(delayed write)

延遲寫減少了磁盤讀寫次數,但是卻降低了文件內容的更新速度,於是,就可能使得寫到文件中的數據在一段時間內並沒有寫到磁盤上。
這樣,當系統發生故障時,這種延遲可能造成文件更新內容的丟失。

所以,redis 給出的三個級別,就是讓我們在數據可能丟失的數量、和性能之間,做出一個權衡。

假設,我們採用 NO,從不手動將數據刷入磁盤,
無疑,由於不用一直寫磁盤,所以 redis 的性能,一定是最高的。
但是,這樣,就只有在緩衝區滿了的時候,纔會刷數據到磁盤。
於是,在宕機的時候,會丟失的數據,則會有很多。

假設,我們採用 ALWAYS,每次寫,都將指令追加到 AOF。
這樣,我們最多最多,丟失一條數據(就是最後哪一條還沒來得及寫進磁盤的時候)。
但是,由於每筆操作都往磁盤刷寫,那性能一定是會受到很大的影響。

所以,很多時候,會傾向於使用 everysec,每秒。
這樣的話,由於不會次次寫磁盤,所以對性能的影響還不至於那麼大,
而且,時間間隔也只有一秒,即使丟失,影響也不會太大。
所以,這往往作爲一個折中方案。

寫在最後

其實,很多時候,我們在學習知識,不能夠浮於表面,要理清其中的原理細節,才能把這些知識掌握好。

比如,光是 Redis 的持久化,RDB 涉及到 Linux 的 fork() 原理;
AOF 的三種 fsync 等級,也涉及到了緩衝區、延遲寫。

如果你對這些知識點不明白,那麼,你就會對持久化的命令一知半解,
於是,就難以去很好地發揮其特性和優勢。

當然,不僅僅是 Redis 的持久化,很多時候,我們都會涉及到一些看似沒有,但在底層又確確實實存在的問題。
比如線程的調度,系統調用,網絡原理等等,這些都會或多或少影響到我們的程序。

因此,我們需要知其然,並知其所以然,這樣,才能真正使用好我們的技術。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章