快取資料丟了,原來是Redis持久化沒玩明白

ITPUB社群發表於2022-12-22

我們都知道Redis是微服務架構中重要的基礎資料庫中介軟體,透過Redis可以將資料庫中的資料快取到記憶體中,當服務端有資料查詢請求的時候,可以直接從記憶體中獲取資料。如此,一方面服務端可以獲得比較快的資料請求響應,另一方面降低了後端關聯式資料庫的業務請求壓力。但是正所謂尺有所短,寸有所長,Redis最大的優勢就是記憶體資料也是最大的劣勢,因為一旦伺服器當機或者伺服器重啟,記憶體中快取的資料也會丟失。針對這樣的場景,Redis提供了三種資料持久化機制,分別是AOF、RDB以及混合持久化來應對這種異常情況。本文主要從Redis實現持久化遇到的問題出發,站在設計者的角度思考相關問題的解決思路。

快取資料丟了,原來是Redis持久化沒玩明白

AOF持久化

AOF持久化方式,即Append Only File,Redis透過記錄執行修改操作命令這種記小本本的方式進行記憶體資料持久化。當需要透過AOF日誌進行恢復資料時,Redis服務端啟動後可以從日誌檔案中回放執行命令來實現記憶體資料恢復。當然了,AOF日誌中記錄的都是修改的命令,查詢命令不會修改資料所以不需要進行記錄。

可能大家都比較熟悉WAL(Write Ahead Log),即日誌預寫機制,它是資料庫非常常用的確保資料操作原子性以及永續性的技術手段。拿Mysql舉栗子,Mysql的WAL體現在undo log以及redo log等這些日誌檔案中,資料庫在執行修改操作的時候並不是立刻將資料更新到磁碟上,而是先記錄在日誌中,主要目的是如果出現異常,可以直接從redo log中進行資料恢復,也就是說讓Mysql知道上次意外發生的時候操作到底有沒有成功,另外還可以將Mysql的隨機寫轉換為順序寫,提升IO效能。但是AOF卻不同,它是在Redis將資料寫入記憶體之後,再將相關的操作命令寫入AOF檔案中。

快取資料丟了,原來是Redis持久化沒玩明白

那麼問題來了,為什麼Redis要採取這種獨特的資料記錄方式,而不是業界常用的WAL的方式呢?其實可以從以下兩個層面思考原因。

(1)AOF檔案中儲存了執行快取的命令,以便於保證在需要恢復資料的時候可以進行命令重放恢復資料,因此需要保證執行命令的合法性,而透過先快取資料再進行命令追加日誌的方式可以確保追加到AOF檔案中的的命令都是合法有效的,redis在恢復資料的時候不需要再去檢查命令是否有效,進一步提升記憶體資料恢復的效率。

(2)另外由於是在修改操作命令之後進行日誌記錄,日誌記錄的時候需要進行磁碟IO操作,因此不會阻塞當前的修改命令。

AOF檔案內容是什麼?

在搞清楚Redis為什麼採用AOF檔案記錄修改命令之後,我們再來看看AOF檔案中到底包含了些內容。
redis> SET mufeng handsome
OK

Redis客戶端與服務端之間採用RESP協議進行通訊,它是一種應用層協議,對於Redis這種以效率為追求目標的中介軟體,通訊協議必定要簡單高效。就上面一條快取操作命令來說:set mufeng handsome 對應的RESP報文就是*3$3set$6mufeng$8handsome,為了方便檢視進行了手動換行。

快取資料丟了,原來是Redis持久化沒玩明白

我們來拆解下報文中各個屬性的含義,“*3”代表本次操作命令將由三個分佈組成,每一部分都是透過"$數字"的形式作為起始,後面為對應的命令、鍵或者值。如此處的"$6"就表示後面的命令是一個6個位元組的鍵值。所以,appenonly.aof檔案中實際儲存的就是這種格式的內容。

快取資料丟了,原來是Redis持久化沒玩明白

AOF有沒有丟資料的風險?

上文說到Redis透過AOF檔案實現記憶體資料持久化,那麼是不是就代表快取資料儲存就萬無一失了?這樣的持久化方式還有沒有資料丟失的風險呢?大家可以設想一下假設在操作完Redis之後,還沒來得及將命令寫入AOF檔案就當機了,那麼這個操作命令就會丟失,對應的快取資料最新值也會丟失。因為即便當機異常恢復之後,也沒辦法從AOF檔案中執行丟失的操作命令了。因此,寫入AOF緩衝區的資料什麼時候進行持久化落盤,直接決定著AOF持久化方式快取資料丟失的風險大小。

三種AOF落盤策略

針對AOF快取中的資料在什麼時機寫入磁碟,Redis提供了三種AOF日誌寫入策略供使用者進行選擇,透過後臺執行緒執行不同時機的AOF檔案資料同步操作,在redis.conf配置檔案中的配置項appendfsync可以進行配置。

【appendfsync:no】
Redis不用管AOF緩衝區的資料什麼時候寫入磁碟,將AOF緩衝區同步資料的操作權交給作業系統,作業系統決定什麼時候將緩衝區的資料寫入磁碟中。
【appendfsync:everysec】
當Redis將資料寫入AOF緩衝區後,每隔1s將緩衝區的資料進行磁碟寫入。
【appendfsync:always】

每執行一個修改命令,都需要修改的命令進行落盤操作。

雖然Redis提供了這三種AOF日誌落盤策略供使用者進行選擇,但是這三種策略實際上各有優缺點。
【appendfsync:no】如果設定了由作業系統進行AOF緩衝區資料寫入,那麼就相當於寫資料的時機完全交由作業系統來決定,此時redis對於緩衝區資料並不可以控制。
【appendfsync:everysec】如果設定成每隔一秒進行快取資料寫入,雖然不會像同步寫入那樣存在一定的效能消耗,但是由於存在一秒的時間間隔,如果在此期間出現伺服器當機,那麼就會損失這一秒的快取資料。
【appendfsync:always】雖然可以基本實現資料不丟失,但是由於每次進行記憶體資料修改都要進行落盤操作,因此在一定程度上影響主執行緒效能。

具體採取怎樣的配置策略還是要根據實際的業務場景來決定,一般推薦使用第二種配置策略【appendfsync:everysec】,在可靠性以及效能方面相對平衡一點。

AOF檔案會越來越大嗎?

在瞭解了AOF日誌磁碟寫入時機之後,我們繼續來思考下一個問題。無論採取什麼樣的同步資料策略,最終都是要將修改命令寫入AOF檔案中,因此隨著時間的推移,這個檔案必定會越來越大。那麼如果檔案變得很大之後,無論是檔案資料新寫入還是Redis透過AOF檔案進行資料恢復,大檔案的操作都會造成IO效能損耗。假如你是Redis的設計者,如果遇到這種情況你會怎麼進行設計最佳化呢?我想無非有兩個最佳化思路,一個是化整為零,一個是想辦法縮小大檔案。

化整為零

當單個檔案過大時,我們很容易想到的最佳化方法就是將這個大檔案拆分為若干個小檔案。這就好比系統中一旦出現過千萬資料庫表的時候,我們就要結合實際的業務場景考慮要不要進行分庫分表了。所以如果單個AOF檔案太大,那麼是不是可以考慮將其按照固定大小進行拆分,這樣可以避免單個AOF檔案過大的問題。那麼Redis小於7.0版本為什麼沒有采用這種方案呢?主要是這種方案並不符合Redis追求簡單高效的設計思想。假設採用了這種資料分塊的方式,那必定需要實現檔案大小檢測、檔案建立、檔案索引維護等等一系列技術細節問題,對於低版本的Redis來說這些都太繁瑣了,還不如一個AOF檔案來的爽快。

PS:在最新的Redis 7.0版本中,Redis已經支援多AOF檔案分片機制,原始的單個AOF檔案會被拆分為一個基礎檔案以及多個增量檔案。新版本中之所以開始支援多檔案儲存,我想也是隨著業務發展記憶體資料可能會很龐大,Redis設計者發現如果還是使用單檔案儲存,大AOF檔案操作以及資料恢復都是一個挑戰。

快取資料丟了,原來是Redis持久化沒玩明白

AOF重寫

既然進行檔案切割太繁瑣了,那麼就單個AOF檔案來說怎麼才能減小檔案大小呢?那就要從AOF檔案的記錄內容入手,透過上文我們瞭解到AOF檔案中實際儲存了修改記憶體資料的操作命令,因此我們在分析完這些操作命令之後發現,當多條命令操作同一個key的時候,實際我們需要的是最新的一條操作命令,除此之外的歷史操作命令我們並不需要關心。比如【set mufeng handsome】、【set mufeng cool】,如果先後執行了這兩個命令,那麼在最終恢復資料的時候,只要恢復【set mufeng cool】即可。因此AOF重寫的本質就是合併命令,也就是說將多條對同一key進行操作的命令進行合併,實際就是使用最新的key值操作命令來代替之前所有關於這個key值的命令。
Redis透過fork子程式來完成AOF檔案重寫,因此在講AOF重寫過程之前,我們需要先了解下什麼是fork子程式的原理,這樣更加有利於我們後面瞭解AOF檔案重寫的過程。

什麼是fork?

fork函式是linux核心提供給使用者建立程式的API,應用程式透過呼叫fork函式建立子程式,這個子程式可以和原來父程式幹同樣的事情,也可以和原來主程式幹不同的事情,這主要取決於對應的引數。這個過程就好比孫悟空拔了一根自己的猴毛變出來一個和自己一模一樣的孫悟空。

因此在fork子程式的過程之中,子程式複製了父程式的程式碼段、資料段、堆疊、頁表等,同時子程式擁有獨立的虛擬記憶體空間(當然是從父程式那裡複製過來的)。如下所示,實際上fork()最終呼叫的是核心copy_process方法複製程式。

快取資料丟了,原來是Redis持久化沒玩明白

父程式fork子程式的時候,子程式擁有獨立的虛擬記憶體空間,那麼對應的實體記憶體空間是不是也是獨立的呢?我們都知道在計算機中,記憶體屬於非常寶貴的系統資源,所以大佬們在設計的時候都儘可能的減少記憶體空間佔用從而提高系統資源利用率。fork子程式過程中用到的Copy-On-Write就是典型的記憶體資源管理最佳化機制,如果子程式只是讀取資料不進行任何的資料寫入,那麼就和父程式公用記憶體空間。當子程式需要進行資料寫入的時候,發現沒有內控空間可以寫入,此時會觸發一個系統中斷來分配記憶體空間給子程式進行資料寫入。


快取資料丟了,原來是Redis持久化沒玩明白

什麼時機觸發AOF重寫?
執行bgrewriteaof 命令
當我們在客戶端手動執行bgrewriteaof 命令後,可以觸發AOF檔案進行重寫,對應Redis原始碼中進行重寫的bgrewriteaofCommand 函式會檢測檢測是否滿足進行重寫的條件,主要檢測以下兩個條件:

【Condition1】:檢測當前是否存在已經在執行的AOF重寫子程式,如果存在的話Redis將不再執行AOF檔案重寫。

【Condition2】:檢測當前是否存在已經在建立RDB檔案的子程式,如果存在的話Redis將AOF檔案重寫任務置為待排程狀態,後續如果滿足了重寫條件,則繼續執行AOF檔案重寫任務。

也就是說,Redis檢測到當前既沒有AOF重寫子程式也沒有RDB檔案建立子程式,那麼就可以進行AOF檔案重寫。對應原始碼如下:

//of_child_pid(aof rewrite程式pid)、rdb_child_pid(rdb dump程式pid)
void bgrewriteaofCommand(redisClient *c) {
    if (server.aof_child_pid != -1) {
        //如果正在aof rewrite,返回錯誤資訊
        addReplyError(c,"Background append only file rewriting already in progress");
    } else if (server.rdb_child_pid != -1) {
        //如果正在rdb dump,為了避免磁碟壓力,將aof重寫計劃狀態置為1,後期再進行rewrite;
        server.aof_rewrite_scheduled = 1;
        addReplyStatus(c,"Background append only file rewriting scheduled");
    }
    //如果當前沒有aof rewrite和rdb dump在進行,則呼叫rewriteAppendOnlyFileBackground開始aof rewrite。
    else if (rewriteAppendOnlyFileBackground() == REDIS_OK) {
        addReplyStatus(c,"Background append only file rewriting started");
    } else {
        //出現異常返回錯誤。
        addReply(c,shared.err);
    }
}

超出配置閾值

如果Redis例項開啟了AOF配置,同時配置了auto-aof-rewrite-percentage以及auto-aof-rewrite-min-size,如果超出了閾值會觸發AOF重寫。

  //沒有rdb子程式、沒有aof重寫子程式、aof檔案設定了閾值以及aof檔案大小絕對值超過閾值
  if (server.rdb_child_pid == -1 &&
         server.aof_child_pid == -1 &&
         server.aof_rewrite_perc &&
         server.aof_current_size > server.aof_rewrite_min_size)
     {
        long long base = server.aof_rewrite_base_size ?
                        server.aof_rewrite_base_size : 1;
        long long growth = (server.aof_current_size*100/base) - 100;
        //超過閾值則進行重寫
        if (growth >= server.aof_rewrite_perc) {
            serverLog(LL_NOTICE,"Starting automatic rewriting of AOF on %lld%% growth",growth);
            rewriteAppendOnlyFileBackground();
        }
     }

aof_rewrite_scheduled被設定為待排程狀態

bgrewriteaofCommand函式中,如果當前正在執行RDB dump操作,那麼對應的aof待排程aof_rewrite_scheduled狀態就會被置為1,當前RDB dump完成之後,會繼續執行AOF重寫操作。
AOF重寫過程是怎樣的?
透過上文的描述,我們知道了Redis觸發AOF重寫的時機,那麼當觸發重寫之後的具體業務是怎樣的呢?我們一起看下AOF重寫的大致流程:

(1)Redis主程式首先檢查是不是存在rdb dump程式或者aof重寫程式正在執行,如果不存在Redis主程式fork子程式進行aof檔案重寫;

(2)fork出來的子程式和原來的Redis主程式擁有同樣的記憶體資料,子程式遍歷此時的記憶體資料同時將記憶體資料寫入到臨時的AOF檔案中;

(3)主程式此時仍然可以接收客戶端請求,同時將新的快取操作寫入aof_buf以及aof_rewrite_buf中,根據對應的同步策略,將buf中的資料分別寫入舊AOF檔案以及臨時AOF檔案中;

(4)重寫完成之後,臨時AOF檔案將替換原有的老的AOF檔案,從而完成整個AOF重寫。


快取資料丟了,原來是Redis持久化沒玩明白

AOF模式優點
1、AOF的持久化策略更加豐富些,可以根據實際業務需要進行配置,因此相對來說在資料可靠性方面要更加有優勢一點。
2、AOF檔案內容比較好理解,更加方便理解業務快取資料。
AOF模式缺點
1、通常情況下,同樣的快取資料,AOF檔案比RDB檔案大小要大一些。

2、在檔案恢復場景下,AOF要比DRB恢復資料慢一些。

RDB持久化

RDB(Redis Data Base),所謂的Redis記憶體資料快照就是某一時刻Redis存於記憶體中的所有快取資料,這就好比用手機相機拍照,記錄當時的美好畫面。Redis可以實現在固定時間間隔後將記憶體中的快取資料持久化儲存起來。這樣即便是伺服器當機或者重啟了,只要RDB快照檔案還存在,快照檔案中對應的快取資料就不會丟失,Redis重新啟動後會重新載入RDB檔案到記憶體中,快速恢復快取資料,透過這樣的方式保障了快取資料的可靠性。

RDB檔案生成過程

我們以bgsave為例子來看下Redis生成RDB檔案的大致過程是怎樣的。
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
    pid_t childpid;
    long long start;

 // 如果已經存在aof重寫子程式以及rdb生成子程式則直接返回錯誤
    if (server.aof_child_pid != -1 || server.rdb_child_pid != -1return C_ERR;
    ...
    // fork子程式進行RDB檔案生成
    if ((childpid = fork()) == 0) {
        ...
        // 生成RDB檔案
        retval = rdbSave(filename,rsi);
        if (retval == C_OK) {
            size_t private_dirty = zmalloc_get_private_dirty(-1);

            if (private_dirty) {
                serverLog(LL_NOTICE,
                    "RDB: %zu MB of memory used by copy-on-write",
                    private_dirty/(1024*1024));
            }

            server.child_info_data.cow_size = private_dirty;
            // 通知父程式RDB檔案生成完畢
            sendChildInfo(CHILD_INFO_TYPE_RDB);
        }
        //子程式退出
        exitFromChild((retval == C_OK) ? 0 : 1);
    } else {
       //父程式業務邏輯
        ...
    }
    return C_OK; 
}
(1)Redis主程式首先判斷當前是否存在已經在執行的aof重寫子程式以及rdb檔案生成子程式,如果存在的話則直接進行返回。為什麼要進行這樣的判斷呢?主要還是從伺服器效能方面進行考量,如果伺服器有多個子執行緒在進行RDB持久化操作,那麼必定會對磁碟造成比較大的IO壓力,如果伺服器中還部署了其他服務甚至會影響其他服務的正常執行。

(2)Redis主程式fork子程式進行RDB檔案生成操作,在fork的過程中,此時的Redis主程式是阻塞的,不能響應客戶端請求,子程式fork完成之後可以繼續響應客戶端請求。

(3)fork出來的子程式遍歷記憶體資料進行RDB檔案生成操作。

(4)如果此時客戶端的請求需要修改快取資料,那麼如上面fork子程式的原理,透過COW機制,作業系統會開闢新的記憶體空間給Redis主程式進行新的快取資料寫入。

(5)子程式快照資料生成完成之後,替換原來老的RDB檔案。

快取資料丟了,原來是Redis持久化沒玩明白


RDB觸發時機

Redis主要支援兩種持久化操作來生成RDB檔案,分別是save、bsave命令方式手動生成以及在配置檔案中配置時間間隔自動進行RDB檔案生成。

手動命令觸發

客戶端連線到redis之後我們可以透過save以及bsave命令進行RDB檔案的立即建立,兩者的區別如下:

save:透過主執行緒觸發,會阻塞Redis業務,如果記憶體資料比較多的話,會導致長時間不能響應外部請求;

bsave:客戶端執行bsave命令進行RDB持久化,Redis主執行緒會fork子執行緒出來進行RDB檔案持久化操作,這樣避免了主執行緒的阻塞即便正在持久化操作依然可以響應外部資料快取請求。

不過這裡值得注意的是,雖然fork子程式之後不會阻塞主程式,但是在fork的過程中會阻塞主程式,尤其是在記憶體資料比較大的時候,阻塞主程式的時間會更長。

配置自動觸發

另外在Redis的配置檔案redis.conf中,我們可以配置按照一定的時間間隔來進行RDB持久化操作。如下配置:

save 900 1
save 300 10
save 60 10000

其他的觸發RDB檔案生成的操作這裡不再贅述了,像從節點執行全量資料同步的時候,也會觸發主節點生成RDB檔案傳送給從節點。

RDB有沒有丟資料的風險?

大家不妨思考下透過RDB檔案進行快取資料持久化會有什麼問題?存不存在丟失快取資料的風險?這種方式看上去是個還不錯的持久化解決方案,但是實際上隱藏著一些丟失快取資料的風險。為什麼這麼說呢?透過分析RDB檔案生成的機制我們可以發現有兩個地方存在快取資料丟失的可能性。
場景1:

由於Redis儲存RDB快照檔案的策略是按照配置的時間間隔進行持久化儲存,也就是每隔一個時間間隔Redis就會儲存一個RDB檔案。因此在記憶體資料有更新但是RDB儲存時間尚未到來的這段時間如果存在伺服器當機或者伺服器重啟的情況,此時記憶體的資料就會存在丟失的風險,因為Redis還沒來得及將資料持久化到RDB檔案中。

場景1中最大的問題就RDB檔案持久化存在時間間隔,而這個時間間隔導致了新增的快取資料在丟失的風險。那麼是不是將時間間隔降低到最小就可以了,比如一秒鐘,即使在這一秒鐘期間出現異常情況,那快取資料也只是丟掉這一秒鐘的快取資料,相對來說資料丟失的情況可控一點。但是問題是如果真的每隔1s就儲存一個RDB檔案到伺服器磁碟中,那不論是對Redis本身還是Redis所在的伺服器磁碟IO都是一種負擔。


快取資料丟了,原來是Redis持久化沒玩明白

場景2:
隨著業務的不斷髮展,記憶體中的資料必定會越來越大,因此在fork子程式來生成RDB檔案的過程中,需要複製的資料會同樣越來越多,耗費的時間也會越來越多,進而阻塞主程式的時間也會越來越多。如果出現時間阻塞主程式的情況,那麼Redis例項必定無法響應客戶端的資料操作請求,最終導致記憶體資料沒有進行及時更新,從而出現丟失快取資料的風險。

RDB模式優點

1、相比AOF在恢復資料的時候需要一條條回放操作命令,透過RDB檔案恢復資料效率更高;
2、適合全量備份記憶體資料場景。
3、同樣規模的記憶體資料,RDB檔案資料更加緊湊,磁碟空間佔用更小。

4、可以根據不同的時間間隔儲存RDB檔案,在恢復資料的時候可以更加靈活地選擇對應版本資料進行恢復。

RDB模式缺點

1、由於RDB資料儲存存在一定的時間間隔,因此存在丟失快取資料的風險;

2、fork子程式進行RDB檔案生成,由於是一次性生成一個記憶體快照檔案,對於伺服器磁碟IO以及Redis本身來說都屬於重操作,可能會對伺服器的磁碟IO造成壓力。

混合持久化

既然AOF以及RDB持久化都有這樣或者那樣的不足,那麼有沒有一種持久化方案可以兼顧二者的優點來揚長避短呢?從4.0版本開始,Redis支援混合持久化的方式來兼顧效率以及資料可靠性。在Redis配置檔案redis.conf中配置混合持久化:

aof‐use‐rdb‐preamble yes

如果配置了混合持久化,那麼Redis主程式在fork子程式進行持久化操作的時候,原先的將記憶體資料轉換為操作命令的過程將替換為使用進行AOF重寫時對應的RDB檔案內容直接放入到重寫後的臨時檔案中,後面再有新的操作命令,都追加到臨時aof檔案中,重寫完成後使用臨時aof檔案替換舊的檔案。

快取資料丟了,原來是Redis持久化沒玩明白

混合持久化模式優點

1、同時擁有RDB以及AOF機制的點,在資料可靠性以及資料恢復效率上面達到了很好的平衡。

混合持久化模式缺點

1、由於Redis從4.0版本才開始支援混合持久化,如果當前平臺中的Redis版本低於4.0,那麼就無法使用這個持久化機制,因此相容性不夠友好;

總結

本文主要分析了Redis AOF、RDB以及混合持久化的記憶體資料持久化的機制原理,同時分析了兩種持久化方式的優點以及缺點。我想只有理解了中介軟體的特性機制原理,知道了特性的長處以及不足我們才能設計適合我們平臺的快取資料持久化策略,從而提升平臺的穩定性。

另外在一些優秀中介軟體的學習和使用過程中,我們不能僅僅停留在會用的層面,更應該深入底層領會其架構和實現機制的設計思路,只有搞明白設計思路,時刻站在設計者的角度來看待遇到的問題,那麼在我們的實際工作中,如果遇到類似的問題我們可以借鑑這些優秀中介軟體的解決思路來進行問題分析。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2929066/,如需轉載,請註明出處,否則將追究法律責任。

相關文章