當前位置:
首頁 > 科技 > 深入淺出 Redis 持久化機制

深入淺出 Redis 持久化機制

作者簡介:錢文品(老錢),互聯網分散式高並發技術十年老兵,目前任掌閱科技服務端技術專家。熟練使用 Java、Python、Golang 等多種計算機語言,開發過遊戲,製作過網站,寫過消息推送系統和MySQL 中間件,實現過開源的 ORM 框架、Web 框架、RPC 框架等

我們都知道 Redis 的數據全部在內存里,如果突然宕機,數據就會全部丟失,因此必須有一種機制來保證 Redis 的數據不會因為故障而丟失,這種機制就是 Redis 的持久化機制。

如圖 2-3 所示,Redis 的持久化機制有兩種,第一種是快照,第二種是 AOF 日誌。快照是一次全量備份,AOF 日誌是連續的增量備份。快照是內存數據的二進位序列化形式,在存儲上非常緊湊,而 AOF 日誌記錄的是內存數據修改的指令記錄文本。AOF 日誌在長期的運行過程中會變得無比龐大,資料庫重啟時需要載入 AOF 日誌進行指令重放,這個時間就會無比漫長,所以需要定期進行 AOF 重寫,給 AOF 日誌進行瘦身。

深入淺出 Redis 持久化機制

2.3.1 快照原理

我們知道 Redis 是單線程程序,這個線程要同時負責多個客戶端套接字的並發讀寫操作和內存數據結構的邏輯讀寫。

在服務線上請求的同時,Redis 還需要進行內存快照,內存快照要求 Redis 必須進行文件 IO 操作,可文件 IO 操作是不能使用多路復用 API。

這意味著單線程在服務線上請求的同時,還要進行文件 IO 操作,而文件 IO 操作會嚴重拖累伺服器請求的性能。

還有個重要的問題,為了不阻塞線上的業務,Redis 就需要一邊持久化,一邊響應客戶端的請求。持久化的同時,內存數據結構還在改變,比如一個大型的 hash 字典正在持久化,結果一個請求過來把它給刪掉了,可是還沒持久化完呢,這該怎麼辦呢?

Redis 使用操作系統的多進程 COW(Copy On Write)機制來實現快照持久化,這個機制很有意思,也很少人知道。多進程 COW 也是鑒定程序員知識廣度的一個重要指標。

2.3.2 fork(多進程)

Redis 在持久化時會調用 glibc 的函數 fork 產生一個子進程,快照持久化完全交給子進程來處理,父進程繼續處理客戶端請求。子進程剛剛產生時,它和父進程共享內存裡面的代碼段和數據段。這時你可以把父子進程想像成一個連體嬰兒,它們在共享身體。這是 Linux 操作系統的機制,為了節約內存資源,所以儘可能讓它們共享起來。在進程分離的一瞬間,內存的增長几乎沒有明顯變化。

用 Python 語言描述進程分離的邏輯如下。fork 函數會在父子進程同時返回,在父進程里返回子進程的 pid,在子進程里返回零。如果操作系統的內存資源不足,pid 就會是負數,表示 fork 失敗。

pid = os.fork
if pid > 0:
handle_client_requests # 父進程繼續處理客戶端請求
if pid == 0:
handle_snapshot_write # 子進程處理快照寫磁碟
if pid < 0:
# fork error

子進程做數據持久化,不會修改現有的內存數據結構,它只是對數據結構進行遍歷讀取,然後序列化寫到磁碟中。但是父進程不一樣,它必須持續服務客戶端請求,然後對內存數據結構進行不間斷的修改。

這個時候就會使用操作系統的 COW 機制來進行數據段頁面的分離。如果 2-4 所示,數據段是由很多操作系統的頁面組合而成,當父進程對其中一個頁面的數據進行修改時,會將被共享的頁面複製一份分離出來,然後對這個複製的頁面進行修改。這時子進程相應的頁面是沒有變化的,還是進程產生時那一瞬間的數據。

深入淺出 Redis 持久化機制

隨著父進程修改操作的持續進行,越來越多的共享頁面被分離出來,內存就會持續增長。但是也不會超過原有數據內存的 2 倍大小。另外一個 Redis 實例里冷數據占的比例往往是比較高的,所以很少會出現所有的頁面都會被分離,被分離的往往只有其中一部分頁面。每個頁面的大小只有 4KB,一個 Redis 實例裡面一般都會有成千上萬個頁面。

子進程因為數據沒有變化,它能看到的內存里的數據在進程產生的一瞬間就凝固了,再也不會改變,這也是為什麼 Redis 的持久化叫「快照」的原因。接下來子進程就可以非常安心地遍曆數據,進行序列化寫磁碟了。

2.3.3 AOF 原理

AOF 日誌存儲的是 Redis 伺服器的順序指令序列,AOF 日誌只記錄對內存進行修改的指令記錄。

假設 AOF 日誌記錄了自 Redis 實例創建以來所有的修改性指令序列,那麼就可以通過對一個空的 Redis 實例順序執行所有的指令——也就是「重放」,來恢復 Redis 當前實例的內存數據結構的狀態。

Redis 會在收到客戶端修改指令後,進行參數校驗、邏輯處理,如果沒問題,就立即將該指令文本存儲到 AOF 日誌中,也就是說,先執行指令才將日誌存檔。這點不同於 leveldb、hbase 等存儲引擎,它們都是先存儲日誌再做邏輯處理。

Redis 在長期運行的過程中,AOF 的日誌會越變越長。如果實例宕機重啟,重放整個 AOF 日誌會非常耗時,導致長時間 Redis 無法對外提供服務。所以需要對 AOF 日誌瘦身。

2.3.4 AOF 重寫

Redis 提供了 bgrewriteaof 指令用於對 AOF 日誌進行瘦身。其原理就是開闢一個子進程對內存進行遍歷,轉換成一系列 Redis 的操作指令,序列化到一個新的 AOF 日誌文件中。序列化完畢後再將操作期間發生的增量 AOF 日誌追加到這個新的 AOF 日誌文件中,追加完畢後就立即替代舊的 AOF 日誌文件了,瘦身工作就完成了。

2.3.5 fsync

AOF 日誌是以文件的形式存在的,當程序對 AOF 日誌文件進行寫操作時,實際上是將內容寫到了內核為文件描述符分配的一個內存緩存中,然後內核會非同步將臟數據刷回到磁碟的。

這就意味著如果機器突然宕機,AOF 日誌內容可能還沒有來得及完全刷到磁碟中,這個時候就會出現日誌丟失。那該怎麼辦?

Linux 的 glibc 提供了 fsync(int fd) 函數可以將指定文件的內容強制從內核緩存刷到磁碟。只要 Redis 進程實時調用 fsync 函數就可以保證 AOF 日誌不丟失。但是 fsync 是一個磁碟 IO 操作,它很慢!如果 Redis 執行一條指令就要 fsync 一次,那麼 Redis 高性能的地位就不保了。

所以在生產環境的伺服器中,Redis 通常是每隔 1s 左右執行一次 fsync 操作,這個 1s 的周期是可以配置的。這是在數據安全性和性能之間做的一個折中,在保持高性能的同時,儘可能使得數據少丟失。

Redis 同樣也提供了另外兩種策略,一個是永不 fsync——讓操作系統來決定何時同步磁碟,這樣做很不安全,另一個是來一個指令就 fsync 一次——結果導致非常慢。這兩種策略在生產環境中基本很少使用,了解一下即可。

2.3.6 運維

快照是通過開啟子進程的方式進行的,它是一個比較耗資源的操作。

1.遍歷整個內存,大塊寫磁碟會加重系統負載。

2.AOF 的 fsync 是一個耗時的 IO 操作,它會降低 Redis 性能,同時也會增加系統 IO 負擔。

所以通常 Redis 的主節點是不會進行持久化操作,持久化操作主要在從節點進行。從節點是備份節點,沒有來自客戶端請求的壓力,它的操作系統資源往往比較充沛。

但是如果出現網路分區,從節點長期連不上主節點,就會出現數據不一致的問題,特別是在網路分區出現的情況下,主節點一旦不小心宕機了,那麼數據就會丟失,所以在生產環境要做好實時監控工作,保證網路暢通或者能快速修復。另外還應該再增加一個從節點以降低網路分區的概率,只要有一個從節點數據同步正常,數據也就不會輕易丟失。

2.3.7 Redis 4.0 混合持久化

重啟 Redis 時,我們很少使用 rdb 來恢復內存狀態,因為會丟失大量數據。我們通常使用 AOF 日誌重放,但是重放 AOF 日誌的性能相對 rdb 來說要慢很多,這樣在 Redis 實例很大的情況下,啟動需要花費很長的時間。

Redis 4.0 為了解決這個問題,帶來了一個新的持久化選項——混合持久化。如圖 2-5 所示,將 rdb 文件的內容和增量的 AOF 日誌文件存在一起。這裡的 AOF 日誌不再是全量的日誌,而是自持久化開始到持久化結束的這段時間發生的增量 AOF 日誌,通常這部分 AOF 日誌很小。

深入淺出 Redis 持久化機制

於是在 Redis 重啟的時候,可以先載入 rdb 的內容,然後再重放增量 AOF 日誌就可以完全替代之前的 AOF 全量文件重放,重啟效率因此得到大幅提升。

本文節選自全彩印熱銷書籍《Redis 深度歷險》原理篇第 2 章節

為答謝「高可用架構」的廣大粉絲們,號主與本書作者安排了送書抽獎活動,一次送出 5本《Redis 深度歷險》贈予有緣人。在本文留言區留言得贊數量前5名即可獲獎。

高可用架構

改變互聯網的構建方式

長按二維碼 關注「高可用架構」

對於沒能抽中獎的小夥伴們可以長按下圖中的二維碼直接購買

限時 6.3折包郵


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 高可用架構 的精彩文章:

Hystrix已經停止開發,官方推薦替代項目Resilience4j簡介

TAG:高可用架構 |