redis009---持久化

Redis 的持久化機(jī)制有兩種,第一種是快照,第二種是 AOF 日志??煺帐且淮稳總浞?,AOF 日志是連續(xù)的增量備份??煺帐莾?nèi)存數(shù)據(jù)的二進(jìn)制序列化形式,在存儲上非常緊湊,而 AOF 日志記錄的是內(nèi)存數(shù)據(jù)修改的指令記錄文本。AOF 日志在長期的運(yùn)行過程中會變的無比龐大,數(shù)據(jù)庫重啟時(shí)需要加載 AOF 日志進(jìn)行指令重放,這個(gè)時(shí)間就會無比漫長。所以需要定期進(jìn)行 AOF 重寫,給 AOF 日志進(jìn)行瘦身。


快照原理

我們知道 Redis 是單線程程序,這個(gè)線程要同時(shí)負(fù)責(zé)多個(gè)客戶端套接字的并發(fā)讀寫操作和內(nèi)存數(shù)據(jù)結(jié)構(gòu)的邏輯讀寫。
在服務(wù)線上請求的同時(shí),Redis 還需要進(jìn)行內(nèi)存快照,內(nèi)存快照要求 Redis 必須進(jìn)行文件 IO 操作,可文件 IO 操作是不能使用多路復(fù)用 API。
這意味著單線程同時(shí)在服務(wù)線上的請求還要進(jìn)行文件 IO 操作,文件 IO 操作會嚴(yán)重拖垮服務(wù)器請求的性能。還有個(gè)重要的問題是為了不阻塞線上的業(yè)務(wù),就需要邊持久化邊響應(yīng)客戶端請求。持久化的同時(shí),內(nèi)存數(shù)據(jù)結(jié)構(gòu)還在改變,比如一個(gè)大型的 hash 字典正在持久化,結(jié)果一個(gè)請求過來把它給刪掉了,還沒持久化完呢,這尼瑪要怎么搞?

那該怎么辦呢?

Redis 使用操作系統(tǒng)的多進(jìn)程 COW(Copy On Write) 機(jī)制來實(shí)現(xiàn)快照持久化,這個(gè)機(jī)制很有意思,也很少人知道。多進(jìn)程 COW 也是鑒定程序員知識廣度的一個(gè)重要指標(biāo)。

fork( 多進(jìn)程)

Redis 在持久化時(shí)會調(diào)用 glibc 的函數(shù) fork 產(chǎn)生一個(gè)子進(jìn)程,快照持久化完全交給子進(jìn)程來處理,父進(jìn)程繼續(xù)處理客戶端請求。子進(jìn)程剛剛產(chǎn)生時(shí),它和父進(jìn)程共享內(nèi)存里面的代碼段和數(shù)據(jù)段。這時(shí)你可以將父子進(jìn)程想像成一個(gè)連體嬰兒,共享身體。這是 Linux 操作系統(tǒng)的機(jī)制,為了節(jié)約內(nèi)存資源,所以盡可能讓它們共享起來。在進(jìn)程分離的一瞬間,內(nèi)存的增長幾乎沒有明顯變化
用 Python 語言描述進(jìn)程分離的邏輯如下。fork 函數(shù)會在父子進(jìn)程同時(shí)返回,在父進(jìn)程里返回子進(jìn)程的 pid,在子進(jìn)程里返回零。如果操作系統(tǒng)內(nèi)存資源不足,pid 就會是負(fù)數(shù),表示 fork 失敗。

pid = os.fork()
if pid > 0:
handle_client_requests() # 父進(jìn)程繼續(xù)處理客戶端請求
if pid == 0:
handle_snapshot_write() # 子進(jìn)程處理快照寫磁盤
if pid < 0:
# fork error

子進(jìn)程做數(shù)據(jù)持久化,它不會修改現(xiàn)有的內(nèi)存數(shù)據(jù)結(jié)構(gòu),它只是對數(shù)據(jù)結(jié)構(gòu)進(jìn)行遍歷讀取,然后序列化寫到磁盤中。但是父進(jìn)程不一樣,它必須持續(xù)服務(wù)客戶端請求,然后對內(nèi)存數(shù)據(jù)結(jié)構(gòu)進(jìn)行不間斷的修改。
這個(gè)時(shí)候就會使用操作系統(tǒng)的 COW 機(jī)制來進(jìn)行數(shù)據(jù)段頁面的分離。數(shù)據(jù)段是由很多操作系統(tǒng)的頁面組合而成,當(dāng)父進(jìn)程對其中一個(gè)頁面的數(shù)據(jù)進(jìn)行修改時(shí),會將被共享的頁面復(fù)制一份分離出來,然后對這個(gè)復(fù)制的頁面進(jìn)行修改。這時(shí)子進(jìn)程相應(yīng)的頁面是沒有變化的,還是進(jìn)程產(chǎn)生時(shí)那一瞬間的數(shù)據(jù)。



隨著父進(jìn)程修改操作的持續(xù)進(jìn)行,越來越多的共享頁面被分離出來,內(nèi)存就會持續(xù)增長。但是也不會超過原有數(shù)據(jù)內(nèi)存的 2 倍大小。另外一個(gè) Redis 實(shí)例里冷數(shù)據(jù)占的比例往往是比較高的,所以很少會出現(xiàn)所有的頁面都會被分離,被分離的往往只有其中一部分頁面。每個(gè)頁面的大小只有 4K,一個(gè) Redis 實(shí)例里面一般都會有成千上萬的頁面。
子進(jìn)程因?yàn)閿?shù)據(jù)沒有變化,它能看到的內(nèi)存里的數(shù)據(jù)在進(jìn)程產(chǎn)生的一瞬間就凝固了,再也不會改變,這也是為什么 Redis 的持久化叫「快照」的原因。接下來子進(jìn)程就可以非常安心的遍歷數(shù)據(jù)了進(jìn)行序列化寫磁盤了。

AOF 原理

AOF 日志存儲的是 Redis 服務(wù)器的順序指令序列,AOF 日志只記錄對內(nèi)存進(jìn)行修改的指令記錄。
假設(shè) AOF 日志記錄了自 Redis 實(shí)例創(chuàng)建以來所有的修改性指令序列,那么就可以通過對一個(gè)空的 Redis 實(shí)例順序執(zhí)行所有的指令,也就是「重放」,來恢復(fù) Redis 當(dāng)前實(shí)例的內(nèi)存數(shù)據(jù)結(jié)構(gòu)的狀態(tài)。
Redis 會在收到客戶端修改指令后,先進(jìn)行參數(shù)校驗(yàn),如果沒問題,就立即將該指令文本存儲到 AOF 日志中,也就是先存到磁盤,然后再執(zhí)行指令。這樣即使遇到突發(fā)宕機(jī),已經(jīng)存儲到 AOF 日志的指令進(jìn)行重放一下就可以恢復(fù)到宕機(jī)前的狀態(tài)。
Redis 在長期運(yùn)行的過程中,AOF 的日志會越變越長。如果實(shí)例宕機(jī)重啟,重放整個(gè)AOF 日志會非常耗時(shí),導(dǎo)致長時(shí)間 Redis 無法對外提供服務(wù)。所以需要對 AOF 日志瘦身。

AOF 重寫

Redis 提供了 bgrewriteaof 指令用于對 AOF 日志進(jìn)行瘦身。其原理就是開辟一個(gè)子進(jìn)程對內(nèi)存進(jìn)行遍歷轉(zhuǎn)換成一系列 Redis 的操作指令,序列化到一個(gè)新的 AOF 日志文件中。序列化完畢后再將操作期間發(fā)生的增量 AOF 日志追加到這個(gè)新的 AOF 日志文件中,追加完畢后就立即替代舊的 AOF 日志文件了,瘦身工作就完成了。

fsync

AOF 日志是以文件的形式存在的,當(dāng)程序?qū)?AOF 日志文件進(jìn)行寫操作時(shí),實(shí)際上是將內(nèi)容寫到了內(nèi)核為文件描述符分配的一個(gè)內(nèi)存緩存中,然后內(nèi)核會異步將臟數(shù)據(jù)刷回到磁盤的。這就意味著如果機(jī)器突然宕機(jī),AOF 日志內(nèi)容可能還沒有來得及完全刷到磁盤中,這個(gè)時(shí)候就會出現(xiàn)日志丟失。那該怎么辦?
Linux 的 glibc 提供了 fsync(int fd)函數(shù)可以將指定文件的內(nèi)容強(qiáng)制從內(nèi)核緩存刷到磁盤。只要 Redis 進(jìn)程實(shí)時(shí)調(diào)用 fsync 函數(shù)就可以保證 aof 日志不丟失。但是 fsync 是一個(gè)磁盤 IO 操作,它很慢!如果 Redis 執(zhí)行一條指令就要 fsync 一次,那么 Redis 高性能的地位就不保了。所以在生產(chǎn)環(huán)境的服務(wù)器中,Redis 通常是每隔 1s 左右執(zhí)行一次 fsync 操作,周期 1s是可以配置的。這是在數(shù)據(jù)安全性和性能之間做了一個(gè)折中,在保持高性能的同時(shí),盡可能使得數(shù)據(jù)少丟失。
Redis 同樣也提供了另外兩種策略,一個(gè)是永不 fsync——讓操作系統(tǒng)來決定合適同步磁盤,很不安全,另一個(gè)是來一個(gè)指令就 fsync 一次——非常慢。但是在生產(chǎn)環(huán)境基本不會使用,了解一下即可。

運(yùn)維

快照是通過開啟子進(jìn)程的方式進(jìn)行的,它是一個(gè)比較耗資源的操作。
1、遍歷整個(gè)內(nèi)存,大塊寫磁盤會加重系統(tǒng)負(fù)載
2、AOF 的 fsync 是一個(gè)耗時(shí)的 IO 操作,它會降低 Redis 性能,同時(shí)也會增加系統(tǒng) IO 負(fù)擔(dān)
所以通常 Redis 的主節(jié)點(diǎn)是不會進(jìn)行持久化操作,持久化操作主要在從節(jié)點(diǎn)進(jìn)行。從節(jié)點(diǎn)是備份節(jié)點(diǎn),沒有來自客戶端請求的壓力,它的操作系統(tǒng)資源往往比較充沛。但是如果出現(xiàn)網(wǎng)絡(luò)分區(qū),從節(jié)點(diǎn)長期連不上主節(jié)點(diǎn),就會出現(xiàn)數(shù)據(jù)不一致的問題,特別是在網(wǎng)絡(luò)分區(qū)出現(xiàn)的情況下又不小心主節(jié)點(diǎn)宕機(jī)了,那么數(shù)據(jù)就會丟失,所以在生產(chǎn)環(huán)境要做好實(shí)時(shí)監(jiān)控工作,保證網(wǎng)絡(luò)暢通或者能快速修復(fù)。另外還應(yīng)該再增加一個(gè)從節(jié)點(diǎn)以降低網(wǎng)絡(luò)分區(qū)的概率,只要有一個(gè)從節(jié)點(diǎn)數(shù)據(jù)同步正常,數(shù)據(jù)也就不會輕易丟失。

Redis 4.0 混合持久化

重啟 Redis 時(shí),我們很少使用 rdb 來恢復(fù)內(nèi)存狀態(tài),因?yàn)闀G失大量數(shù)據(jù)。我們通常使用 AOF 日志重放,但是重放 AOF 日志性能相對 rdb 來說要慢很多,這樣在 Redis 實(shí)例很大的情況下,啟動(dòng)需要花費(fèi)很長的時(shí)間。
Redis 4.0 為了解決這個(gè)問題,帶來了一個(gè)新的持久化選項(xiàng)——混合持久化。將 rdb 文件的內(nèi)容和增量的 AOF 日志文件存在一起。這里的 AOF 日志不再是全量的日志,而是自持久化開始到持久化結(jié)束的這段時(shí)間發(fā)生的增量 AOF 日志,通常這部分 AOF 日志很小。



于是在 Redis 重啟的時(shí)候,可以先加載 rdb 的內(nèi)容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重啟效率因此大幅得到提升。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 一、Redis高可用概述 在介紹Redis高可用之前,先說明一下在Redis的語境中高可用的含義。 我們知道,在w...
    空語閱讀 1,608評論 0 2
  • 企業(yè)級redis集群架構(gòu)的特點(diǎn) 海量數(shù)據(jù) 高并發(fā) 高可用 要達(dá)到高可用,持久化是不可減少的,持久化主要是做災(zāi)難恢復(fù)...
    lucode閱讀 2,221評論 0 7
  • Redis 提供了多種不同級別的持久化方式: 了解 RDB 持久化和 AOF 持久化之間的異同是非常重要的, 以下...
    笑Skr人啊閱讀 473評論 0 1
  • 日子悄悄地過去,毫無生氣的樹木好像一夜間枝繁葉茂,嫩綠的葉子看的人心曠神怡。在幾場春雨雷電的洗禮后,太陽不再溫柔,...
    天馬流星辰閱讀 203評論 1 2
  • 非原創(chuàng),侵權(quán)刪。 如果你不知道梟雄兩個(gè)字是什么意思,看劉備不管用,得看杜月笙。而梟雄和普通黑社會頭子,最大的區(qū)別就...
    霖寶管家閱讀 1,190評論 1 2