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

小林coding發表於2021-05-27

AOF 日誌

試想一下,如果 Redis 每執行一條寫操作命令,就把該命令以追加的方式寫入到一個檔案裡,然後重啟 Redis 的時候,先去讀取這個檔案裡的命令,並且執行它,這不就相當於恢復了快取資料了嗎?

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

在 Redis 中 AOF 持久化功能預設是不開啟的,需要我們修改 redis.conf 配置檔案中的以下引數:

AOF 日誌檔案其實就是普通的文字,我們可以通過 cat 命令檢視裡面的內容,不過裡面的內容如果不知道一定的規則的話,可能會看不懂。

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

我這裡給大家解釋下。

*3」表示當前命令有三個部分,每部分都是以「$+數字」開頭,後面緊跟著具體的命令、鍵或值。然後,這裡的「數字」表示這部分中的命令、鍵或值一共有多少位元組。例如,「$3 set」表示這部分有 3 個位元組,也就是「set」命令這個字串的長度。

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

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

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

而如果先執行寫操作命令再記錄日誌的話,只有在該命令執行成功後,才將命令記錄到 AOF 日誌裡,這樣就不用額外的檢查開銷,保證記錄在 AOF 日誌裡的命令都是可執行並且正確的。

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

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

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

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

因為將命令寫入到日誌的這個操作也是在主程式完成的(執行命令也是在主程式),也就是說這兩個操作是同步的。

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

認真分析一下,其實這兩個風險都有一個共性,都跟「 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 個寫回策略的優缺點總結成了一張表格:

大家知道這三種策略是怎麼實現的嗎?

深入到原始碼後,你就會發現這三種策略只是在控制 fsync() 函式的呼叫時機。

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

如果想要應用程式向檔案寫入資料後,能立馬將資料同步到硬碟,就可以呼叫 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 檔案。

但是在使用重寫機制後,就會讀取 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 子程式時,作業系統會把主程式的「頁表」複製一份給子程式,這個頁表記錄著虛擬地址和實體地址對映關係,而不會複製實體記憶體,也就是說,兩者的虛擬空間不同,但其對應的物理空間是同一個。

這樣一來,子程式就共享了父程式的實體記憶體資料了,這樣能夠節約實體記憶體資源,頁表對應的頁表項的屬性會標記該實體記憶體的許可權為只讀

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

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

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

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

所以,有兩個階段會導致阻塞父程式:

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

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

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

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

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

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

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

在重寫 AOF 期間,當 Redis 執行完一個寫命令之後,它會同時將這個寫命令寫入到 「AOF 緩衝區」和 「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原始碼分析》

相關文章