天天看點

圖解 Redis | 不就是 AOF 持久化嘛

AOF 日志

試想一下,如果 Redis 每執行一條寫操作指令,就把該指令以追加的方式寫入到一個檔案裡,然後重新開機 Redis 的時候,先去讀取這個檔案裡的指令,并且執行它,這不就相當于恢複了緩存資料了嗎?

這種儲存寫操作指令到日志的持久化方式,就是 Redis 裡的 AOF(Append Only File) 持久化功能,注意隻會記錄寫操作指令,讀操作指令是不會被記錄的,因為沒意義。

在 Redis 中 AOF 持久化功能預設是不開啟的,需要我們修改

redis.conf

配置檔案中的以下參數:

圖解 Redis | 不就是 AOF 持久化嘛

AOF 日志檔案其實就是普通的文本,我們可以通過

cat

指令檢視裡面的内容,不過裡面的内容如果不知道一定的規則的話,可能會看不懂。

我這裡以「set name xiaolin」指令作為例子,Redis 執行了這條指令後,記錄在 AOF 日志裡的内容如下圖:

我這裡給大家解釋下。

*3

」表示目前指令有三個部分,每部分都是以「

$+數字

」開頭,後面緊跟着具體的指令、鍵或值。然後,這裡的「

數字

」表示這部分中的指令、鍵或值一共有多少位元組。例如,「

$3 set

」表示這部分有 3 個位元組,也就是「

set

」指令這個字元串的長度。

不知道大家注意到沒有,Redis 是先執行寫操作指令後,才将該指令記錄到 AOF 日志裡的,這麼做其實有兩個好處。

第一個好處,避免額外的檢查開銷。

因為如果先将寫操作指令記錄到 AOF 日志裡,再執行該指令的話,如果目前的指令文法有問題,那麼如果不進行指令文法檢查,該錯誤的指令記錄到 AOF 日志裡後,Redis 在使用日志恢複資料時,就可能會出錯。

而如果先執行寫操作指令再記錄日志的話,隻有在該指令執行成功後,才将指令記錄到 AOF 日志裡,這樣就不用額外的檢查開銷,保證記錄在 AOF 日志裡的指令都是可執行并且正确的。

第二個好處,不會阻塞目前寫操作指令的執行,因為當寫操作指令執行成功後,才會将指令記錄到 AOF 日志。

當然,AOF 持久化功能也不是沒有潛在風險。

第一個風險,執行寫操作指令和記錄日志是兩個過程,那當 Redis 在還沒來得及将指令寫入到硬碟時,伺服器發生當機了,這個資料就會有丢失的風險。

第二個風險,前面說道,由于寫操作指令執行成功後才記錄到 AOF 日志,是以不會阻塞目前寫操作指令的執行,但是可能會給「下一個」指令帶來阻塞風險。

因為将指令寫入到日志的這個操作也是在主程序完成的(執行指令也是在主程序),也就是說這兩個操作是同步的。

圖解 Redis | 不就是 AOF 持久化嘛

如果在将日志内容寫入到硬碟時,伺服器的硬碟的 I/O 壓力太大,就會導緻寫硬碟的速度很慢,進而阻塞住了,也就會導緻後續的指令無法執行。

認真分析一下,其實這兩個風險都有一個共性,都跟「 AOF 日志寫回硬碟的時機」有關。

三種寫回政策

Redis 寫入 AOF 日志的過程,如下圖:

圖解 Redis | 不就是 AOF 持久化嘛

我先來具體說說:

  1. Redis 執行完寫操作指令後,會将指令追加到

    server.aof_buf

    緩沖區;
  2. 然後通過 write() 系統調用,将 aof_buf 緩沖區的資料寫入到 AOF 檔案,此時資料并沒有寫入到硬碟,而是拷貝到了核心緩沖區 page cache,等待核心将資料寫入硬碟;
  3. 具體核心緩沖區的資料什麼時候寫入到硬碟,由核心決定。

Redis 提供了 3 種寫回硬碟的政策,控制的就是上面說的第三步的過程。

redis.conf

配置檔案中的

appendfsync

配置項可以有以下 3 種參數可填:

  • Always,這個單詞的意思是「總是」,是以它的意思是每次寫操作指令執行完後,同步将 AOF 日志資料寫回硬碟;
  • Everysec,這個單詞的意思是「每秒」,是以它的意思是每次寫操作指令執行完後,先将指令寫入到 AOF 檔案的核心緩沖區,然後每隔一秒将緩沖區裡的内容寫回到硬碟;
  • No,意味着不由 Redis 控制寫回硬碟的時機,轉交給作業系統控制寫回的時機,也就是每次寫操作指令執行完後,先将指令寫入到 AOF 檔案的核心緩沖區,再由作業系統決定何時将緩沖區内容寫回硬碟。

這 3 種寫回政策都無法能完美解決「主程序阻塞」和「減少資料丢失」的問題,因為兩個問題是對立的,偏向于一邊的話,就會要犧牲另外一邊,原因如下:

  • Always 政策的話,可以最大程度保證資料不丢失,但是由于它每執行一條寫操作指令就同步将 AOF 内容寫回硬碟,是以是不可避免會影響主程序的性能;
  • No 政策的話,是交由作業系統來決定何時将 AOF 日志内容寫回硬碟,相比于 Always 政策性能較好,但是作業系統寫回硬碟的時機是不可預知的,如果 AOF 日志内容沒有寫回硬碟,一旦伺服器當機,就會丢失不定數量的資料。
  • Everysec 政策的話,是折中的一種方式,避免了 Always 政策的性能開銷,也比 No 政策更能避免資料丢失,當然如果上一秒的寫操作指令日志沒有寫回到硬碟,發生了當機,這一秒内的資料自然也會丢失。

大家根據自己的業務場景進行選擇:

  • 如果要高性能,就選擇 No 政策;
  • 如果要高可靠,就選擇 Always 政策;
  • 如果允許資料丢失一點,但又想性能高,就選擇 Everysec 政策。

我也把這 3 個寫回政策的優缺點總結成了一張表格:

圖解 Redis | 不就是 AOF 持久化嘛

大家知道這三種政策是怎麼實作的嗎?

深入到源碼後,你就會發現這三種政策隻是在控制

fsync()

函數的調用時機。

當應用程式向檔案寫入資料時,核心通常先将資料複制到核心緩沖區中,然後排入隊列,然後由核心決定何時寫入硬碟。

圖解 Redis | 不就是 AOF 持久化嘛

如果想要應用程式向檔案寫入資料後,能立馬将資料同步到硬碟,就可以調用

fsync()

函數,這樣核心就會将核心緩沖區的資料直接寫入到硬碟,等到硬碟寫操作完成後,該函數才會傳回。

  • Always 政策就是每次寫入 AOF 檔案資料後,就執行 fsync() 函數;
  • Everysec 政策就會建立一個異步任務來執行 fsync() 函數;
  • No 政策就是永不執行 fsync() 函數;

AOF 重寫機制

AOF 日志是一個檔案,随着執行的寫操作指令越來越多,檔案的大小會越來越大。

如果當 AOF 日志檔案過大就會帶來性能問題,比如重新開機 Redis 後,需要讀 AOF 檔案的内容以恢複資料,如果檔案過大,整個恢複的過程就會很慢。

是以,Redis 為了避免 AOF 檔案越寫越大,提供了 AOF 重寫機制,當 AOF 檔案的大小超過所設定的門檻值後,Redis 就會啟用 AOF 重寫機制,來壓縮 AOF 檔案。

AOF 重寫機制是在重寫時,讀取目前資料庫中的所有鍵值對,然後将每一個鍵值對用一條指令記錄到「新的 AOF 檔案」,等到全部記錄完後,就将新的 AOF 檔案替換掉現有的 AOF 檔案。

舉個例子,在沒有使用重寫機制前,假設前後執行了「set name xiaolin」和「set name xiaolincoding」這兩個指令的話,就會将這兩個指令記錄到 AOF 檔案。

圖解 Redis | 不就是 AOF 持久化嘛

但是在使用重寫機制後,就會讀取 name 最新的 value(鍵值對) ,然後用一條 「set name xiaolincoding」指令記錄到新的 AOF 檔案,之前的第一個指令就沒有必要記錄了,因為它屬于「曆史」指令,沒有作用了。這樣一來,一個鍵值對在重寫日志中隻用一條指令就行了。

重寫工作完成後,就會将新的 AOF 檔案覆寫現有的 AOF 檔案,這就相當于壓縮了 AOF 檔案,使得 AOF 檔案體積變小了。

然後,在通過 AOF 日志恢複資料時,隻用執行這條指令,就可以直接完成這個鍵值對的寫入了。

是以,重寫機制的妙處在于,盡管某個鍵值對被多條寫指令反複修改,最終也隻需要根據這個「鍵值對」目前的最新狀态,然後用一條指令去記錄鍵值對,代替之前記錄這個鍵值對的多條指令,這樣就減少了 AOF 檔案中的指令數量。最後在重寫工作完成後,将新的 AOF 檔案覆寫現有的 AOF 檔案。

這裡說一下為什麼重寫 AOF 的時候,不直接複用現有的 AOF 檔案,而是先寫到新的 AOF 檔案再覆寫過去。

因為如果 AOF 重寫過程中失敗了,現有的 AOF 檔案就會造成污染,可能無法用于恢複使用。

是以 AOF 重寫過程,先重寫到新的 AOF 檔案,重寫失敗的話,就直接删除這個檔案就好,不會對現有的 AOF 檔案造成影響。

AOF 背景重寫

寫入 AOF 日志的操作雖然是在主程序完成的,因為它寫入的内容不多,是以一般不太影響指令的操作。

但是在觸發 AOF 重寫時,比如當 AOF 檔案大于 64M 時,就會對 AOF 檔案進行重寫,這時是需要讀取所有緩存的鍵值對資料,并為每個鍵值對生成一條指令,然後将其寫入到新的 AOF 檔案,重寫完後,就把現在的 AOF 檔案替換掉。

這個過程其實是很耗時的,是以重寫的操作不能放在主程序裡。

是以,Redis 的重寫 AOF 過程是由背景子程序 bgrewriteaof 來完成的,這麼做可以達到兩個好處:

  • 子程序進行 AOF 重寫期間,主程序可以繼續處理指令請求,進而避免阻塞主程序;
  • 子程序帶有主程序的資料副本(資料副本怎麼産生的後面會說),這裡使用子程序而不是線程,因為如果是使用線程,多線程之間會共享記憶體,那麼在修改共享記憶體資料的時候,需要通過加鎖來保證資料的安全,而這樣就會降低性能。而使用子程序,建立子程序時,父子程序是共享記憶體資料的,不過這個共享的記憶體隻能以隻讀的方式,而當父子程序任意一方修改了該共享記憶體,就會發生「寫時複制」,于是父子程序就有了獨立的資料副本,就不用加鎖來保證資料安全。

子程序是怎麼擁有主程序一樣的資料副本的呢?

主程序在通過

fork

系統調用生成 bgrewriteaof 子程序時,作業系統會把主程序的「頁表」複制一份給子程序,這個頁表記錄着虛拟位址和實體位址映射關系,而不會複制實體記憶體,也就是說,兩者的虛拟空間不同,但其對應的實體空間是同一個。

圖解 Redis | 不就是 AOF 持久化嘛

這樣一來,子程序就共享了父程序的實體記憶體資料了,這樣能夠節約實體記憶體資源,頁表對應的頁表項的屬性會标記該實體記憶體的權限為隻讀。

不過,當父程序或者子程序在向這個記憶體發起寫操作時,CPU 就會觸發缺頁中斷,這個缺頁中斷是由于違反權限導緻的,然後作業系統會在「缺頁異常處理函數」裡進行實體記憶體的複制,并重新設定其記憶體映射關系,将父子程序的記憶體讀寫權限設定為可讀寫,最後才會對記憶體進行寫操作,這個過程被稱為「寫時複制(Copy On Write)」。

圖解 Redis | 不就是 AOF 持久化嘛

寫時複制顧名思義,在發生寫操作的時候,作業系統才會去複制實體記憶體,這樣是為了防止 fork 建立子程序時,由于實體記憶體資料的複制時間過長而導緻父程序長時間阻塞的問題。

當然,作業系統複制父程序頁表的時候,父程序也是阻塞中的,不過頁表的大小相比實際的實體記憶體小很多,是以通常複制頁表的過程是比較快的。

不過,如果父程序的記憶體資料非常大,那自然頁表也會很大,這時父程序在通過 fork 建立子程序的時候,阻塞的時間也越久。

是以,有兩個階段會導緻阻塞父程序:

  • 建立子程序的途中,由于要複制父程序的頁表等資料結構,阻塞的時間跟頁表的大小有關,頁表越大,阻塞的時間也越長;
  • 建立完子程序後,如果子程序或者父程序修改了共享資料,就會發生寫時複制,這期間會拷貝實體記憶體,如果記憶體越大,自然阻塞的時間也越長;

觸發重寫機制後,主程序就會建立重寫 AOF 的子程序,此時父子程序共享實體記憶體,重寫子程序隻會對這個記憶體進行隻讀,重寫 AOF 子程序會讀取資料庫裡的所有資料,并逐一把記憶體資料的鍵值對轉換成一條指令,再将指令記錄到重寫日志(新的 AOF 檔案)。

但是子程序重寫過程中,主程序依然可以正常處理指令。

如果此時主程序修改了已經存在 key-value,就會發生寫時複制,注意這裡隻會複制主程序修改的實體記憶體資料,沒修改實體記憶體還是與子程序共享的。

是以如果這個階段修改的是一個 bigkey,也就是資料量比較大的 key-value 的時候,這時複制的實體記憶體資料的過程就會比較耗時,有阻塞主程序的風險。

還有個問題,重寫 AOF 日志過程中,如果主程序修改了已經存在 key-value,此時這個 key-value 資料在子程序的記憶體資料就跟主程序的記憶體資料不一緻了,這時要怎麼辦呢?

為了解決這種資料不一緻問題,Redis 設定了一個 AOF 重寫緩沖區,這個緩沖區在建立 bgrewriteaof 子程序之後開始使用。

在重寫 AOF 期間,當 Redis 執行完一個寫指令之後,它會同時将這個寫指令寫入到 「AOF 緩沖區」和 「AOF 重寫緩沖區」。

圖解 Redis | 不就是 AOF 持久化嘛

在這裡插入圖檔描述

也就是說,在 bgrewriteaof 子程序執行 AOF 重寫期間,主程序需要執行以下三個工作:

  • 執行用戶端發來的指令;
  • 将執行後的寫指令追加到 「AOF 緩沖區」;
  • 将執行後的寫指令追加到 「AOF 重寫緩沖區」;

當子程序完成 AOF 重寫工作(掃描資料庫中所有資料,逐一把記憶體資料的鍵值對轉換成一條指令,再将指令記錄到重寫日志)後,會向主程序發送一條信号,信号是程序間通訊的一種方式,且是異步的。

主程序收到該信号後,會調用一個信号處理函數,該函數主要做以下工作:

  • 将 AOF 重寫緩沖區中的所有内容追加到新的 AOF 的檔案中,使得新舊兩個 AOF 檔案所儲存的資料庫狀态一緻;
  • 新的 AOF 的檔案進行改名,覆寫現有的 AOF 檔案。

信号函數執行完後,主程序就可以繼續像往常一樣處理指令了。

在整個 AOF 背景重寫過程中,除了發生寫時複制會對主程序造成阻塞,還有信号處理函數執行時也會對主程序造成阻塞,在其他時候,AOF 背景重寫都不會阻塞主程序。

總結

這次小林給大家介紹了 Redis 持久化技術中的 AOF 方法,這個方法是每執行一條寫操作指令,就将該指令以追加的方式寫入到 AOF 檔案,然後在恢複時,以逐一執行指令的方式來進行資料恢複。

Redis 提供了三種将 AOF 日志寫回硬碟的政策,分别是 Always、Everysec 和 No,這三種政策在可靠性上是從高到低,而在性能上則是從低到高。

随着執行的指令越多,AOF 檔案的體積自然也會越來越大,為了避免日志檔案過大, Redis 提供了 AOF 重寫機制,它會直接掃描資料中所有的鍵值對資料,然後為每一個鍵值對生成一條寫操作指令,接着将該指令寫入到新的 AOF 檔案,重寫完成後,就替換掉現有的 AOF 日志。重寫的過程是由背景子程序完成的,這樣可以使得主程序可以繼續正常處理指令。

用 AOF 日志的方式來恢複資料其實是很慢的,因為 Redis 執行指令由單線程負責的,而 AOF 日志恢複資料的方式是順序執行日志裡的每一條指令,如果 AOF 日志很大,這個「重放」的過程就會很慢了。

參考資料

  • 《Redis設計與實作》
  • 《Redis核心技術與實戰-極客時間》
  • 《Redis源碼分析》
圖解 Redis | 不就是 AOF 持久化嘛

關注公衆号:「小林coding」 ,回複「我要學習」即可免費獲得「伺服器 Linux C/C++ 」成長路程(書籍資料 + 思維導圖)