天天看點

緩存資料丢了,原來是Redis持久化沒玩明白

作者:dbaplus社群

我們都知道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操作,是以不會阻塞目前的修改指令。

1.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持久化沒玩明白

2.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】,在可靠性以及性能方面相對平衡一點。

3.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持久化沒玩明白

4.AOF模式優點

  • AOF的持久化政策更加豐富些,可以根據實際業務需要進行配置,是以相對來說在資料可靠性方面要更加有優勢一點。
  • AOF檔案内容比較好了解,更加友善了解業務緩存資料。

5.AOF模式缺點

  • 通常情況下,同樣的緩存資料,AOF檔案比RDB檔案大小要大一些。
  • 在檔案恢複場景下,AOF要比DRB恢複資料慢一些。

二、RDB持久化

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

1.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 != -1) return 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持久化沒玩明白

2.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檔案發送給從節點。

3.RDB有沒有丢資料的風險?

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

場景1:

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

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

緩存資料丢了,原來是Redis持久化沒玩明白

場景2:

随着業務的不斷發展,記憶體中的資料必定會越來越大,是以在fork子程序來生成RDB檔案的過程中,需要複制的資料會同樣越來越多,耗費的時間也會越來越多,進而阻塞主程序的時間也會越來越多。如果出現長時間阻塞主程序的情況,那麼Redis執行個體必定無法響應用戶端的資料操作請求,最終導緻記憶體資料沒有進行及時更新,進而出現丢失緩存資料的風險。

4.RDB模式優點

  • 相比AOF在恢複資料的時候需要一條條回放操作指令,通過RDB檔案恢複資料效率更高;
  • 适合全量備份記憶體資料場景;
  • 同樣規模的記憶體資料,RDB檔案資料更加緊湊,磁盤空間占用更小;
  • 可以根據不同的時間間隔儲存RDB檔案,在恢複資料的時候可以更加靈活地選擇對應版本資料進行恢複。

5.RDB模式缺點

  • 由于RDB資料儲存存在一定的時間間隔,是以存在丢失緩存資料的風險;
  • 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機制的優點,在資料可靠性以及資料恢複效率上面達到了很好的平衡。

2.混合持久化模式缺點

  • 由于Redis從4.0版本才開始支援混合持久化,如果目前平台中的Redis版本低于4.0,那麼就無法使用這個持久化機制,是以相容性不夠友好。

四、總結

本文主要分析了Redis AOF、RDB以及混合持久化的記憶體資料持久化的機制原理,同時分析了兩種持久化方式的優點以及缺點。我想隻有了解了中間件的特性機制原理,知道了特性的長處以及不足我們才能設計适合我們平台的緩存資料持久化政策,進而提升平台的穩定性。

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

作者丨夢堯技術

來源丨公衆号: 慕楓技術筆記(ID:lulideguang)

dbaplus社群歡迎廣大技術人員投稿,投稿郵箱:[email protected]

關于我們

dbaplus社群是圍繞Database、BigData、AIOps的企業級專業社群。資深大咖、技術幹貨,每天精品原創文章推送,每周線上技術分享,每月線下技術沙龍,每季度Gdevops&DAMS行業大會。

關注公衆号【dbaplus社群】,擷取更多原創技術文章和精選工具下載下傳

繼續閱讀