Python–Redis實戰:第四章:資料安全與效能保障:第2節:快照持久化

Mark發表於2019-02-16

上一篇文章:Python–Redis實戰:第四章:資料安全與效能保障:第1節:持久化選項
下一篇文章:Python–Redis實戰:第四章:資料安全與效能保障:第3節:AOF持久化

Redis可以通過建立快照來獲得儲存在記憶體裡面的資料在某個時間點上的副本。在建立快照之後,使用者可以對快照進行備份,可以將快照複製到其它伺服器從而建立具有相同資料的伺服器副本,還可以將快照留在原地以便重啟伺服器時使用。

根據配置,快照將被寫入dbfilename選項指定的檔案裡面,並儲存在dir選項指定的路徑上面。如果在新的快照檔案建立文筆之前,Redis、系統或者硬體上三者之中的任意一個崩潰了,那麼Redis將丟失最近一次建立快照之後寫入的所有資料。

舉個例子,假設Redis目前在記憶體裡面儲存了10GB的資料,上一個快照是在下午2:35開始建立的,並且已經建立成果。下午3:06時,Redis又開始建立新的快照,並且在下午3:08快照檔案建立完畢之前,有35個鍵進行了更新。如果在下午3:06至3:08期間,系統發生崩潰,導致Redis無法完成新快照的建立工作,那麼Redis將丟失下午2:35之後寫入的所有資料。

建立快照的辦法有以下幾種:

  • 客戶端可以通過向Redis傳送bgsave命令建立一個快照。對於支援bgsave命令的平臺來說(基本上所有的平臺都支援,除了Windows平臺),Redis會呼叫fork來建立一個子程式,然後子程式負責將快照寫入硬碟,而父程式則繼續處理命令請求。

fork:當一個程式建立子程式的時候,底層的作業系統會建立該程式的一個副本。在Unix和類Unix系統上面,建立子程式的操作會進行如下優化:在剛開始的時候,父程式共享相同的記憶體,直到父程式或者子程式對記憶體進行了寫入之後,對唄寫入記憶體的共享才會結束。

  • 客戶端還可以通過向Redis發singsave命令來建立一個快照,接到save命令的Redis伺服器在快照建立完畢之前將不再響應任何其他命令。save命令並不常用,我們通常只會在沒有足夠記憶體去執行bgsave命令的情況下,又或者即使等待持久化操作執行完畢也無所謂的情況下,才會使用這個命令。
  • 如果使用者設定了save配置選項,比如save 60 10000,那麼從Redis最近一次建立快照之後開始算起,當【60秒之內有10 000次寫入】這個條件滿足時,Redis就會自動觸發bgsave命令。如果使用者設定了多個save配置選項,那麼當任意一個save的配置選項所設定的條件被滿足時,Redis就會觸發一次bgsave命令。
  • 當Redis通過shutdown命令接收到關閉伺服器的請求時,或者接受到標準term訊號時,會執行一個save命令,阻塞所有客戶端,不再執行客戶端傳送的任何命令,並在save命令執行完畢之後關閉伺服器。
  • 當一個Redis伺服器連線另一個Redis伺服器,並向對方傳送sync命令來開始一次複製操作的時候,如果主伺服器目前沒有在執行bgsave操作,或者主伺服器並非剛剛執行完bgsave操作,那麼主伺服器就會執行bgsave命令。

在只使用快照持久化來儲存資料時,一定要記住:如果系統真的發生崩潰,使用者將丟失最近一次生成快照之後更改的所有資料。因此,快照持久化只適用於那些丟失一部分資料也不會造成問題的應用程式,而不能接受這種資料損失的應用程式則可以考慮使用AOF持久化。接下來將展示幾個使用快照持久化的場景,讀者可以從中學習到如何通過修改配置來獲得資金想要的快照持久化的行為。

個人開發

在個人開發伺服器上面,我主要考慮的是儘可能地降低快照持久化帶來的資源消耗。基於這個原因以及對自己硬體的信任,我只設定了save 900 1這一條規則。其中save 選項告知Redis,它應該根據這個選項提供的兩個值來執行bgsave操作。在這個規則設定下,如果伺服器距離上次成功生成快照超過了900秒(15分鐘),並且在執行期間執行了至少一次寫入操作,那麼Redis久自動開始一次新的bgsave操作。

如果你打算在生產伺服器中使用快照持久化並儲存大量資料,那麼你的開發伺服器最好能夠執行在與生產伺服器相同或者相似的硬體上面,並在這兩個伺服器上使用相同的save選項、儲存相似的資料集並處理相似的負載量。把開發環境設定得儘量貼近生產環境,有助於判斷快照是否生成的過於頻繁或者稀少。過於頻繁會浪費資源,過於稀少則帶有丟失大量資料的隱患。

對日誌進行聚合計算

在對日誌檔案進行聚合計算或者對頁面瀏覽量進行分析的時候,我們需要唯一考慮的就是:如果Redis因為崩潰而未能成功建立新的快照,那麼我們能夠承受丟失多長時間以內產生的新資料。如果丟失一個小時之內產生的資料是可以接受的,那麼可以使用配置值save 3600 1(3600秒為1小時)。在決定好了持久化配置值之後,另一個需要解決的問題就是如何恢復因為故障而被終端的日誌處理操作。

在進行資料恢復時,首先要做的就是弄清楚我們丟失了哪些資料。為了弄明白這一點,我們需要在處理日誌的同時記錄被處理日誌的相關資訊。

下面程式碼展示了一個用於處理新日誌的函式,該函式有3個引數,他們分別是:一個Redis連結;一個儲存日誌檔案的路徑;待處理日誌檔案中各個行(line)的回撥函式。這個函式可以在處理日誌檔案的同時,記錄被處理的日誌檔案的名字以及偏移量。

import os

import redis  # 匯入redis包包

#日誌處理函式接收的其中以惡搞引數為回撥函式
#這個回撥函式接收一個Redis連線和一個日誌行作為引數,
#並通過呼叫流水線呢物件的方法來執行Redis命令
def process_log(conn,path,callback):
    #獲取檔案當前的處理進度
    current_file,offset=conn.mget(`progress.file`,`progress.position`)
    pipe=conn.pipeline()

    #通過使用閉包(closur)來減少重複程式碼
    def update_progress():
        pipe.mset({`progress.file`:fname,`progress:position`:offset})
        #這個語句負責執行實際的日誌更細操作,並將日誌檔案的名字和目前的處理器進度記錄到Redis裡面
        pipe.execute()

    #有序的遍歷各個日誌檔案
    for fname in sorted(os.listdir(path)):
        #略過所有已處理的日誌檔案
        if fname <current_file:
            continue

        inp=open(os.path.join(path,fname),`rb`)
        #在接著處理一個因為系統崩潰而未能完成處理的日誌檔案時,略過已處理的內容
        if fname==current_file:
            inp.seek(int(offset,10))
        else:
            offset=0

        current_file=None

        #列舉函式遍歷一個由檔案行足哼的序列,並返回任意多個二元組

        #每個二元祖包含了行號lno和行資料line,其中行號從0開始
        for lno,line in enumerate(inp):
            #處理日誌
            callback(pipe,line)
            #更細已處理內容的偏移量
            offset+=int(offset)+len(line)

            #每當處理完1000個日誌行或者處理完 整個日誌檔案的時候,都更新一次檔案的處理進度
            if not (lno+1) %1000:
                update_progress()
        update_progress()

        inp.close()

通過將日誌的處理進度記錄到Redis裡面,城西可以在系統崩潰之後,根據進度記錄繼續執行之前未完成的處理工作。而通過事務流水線,程式保證日誌的處理結果和處理進度總是會同時被記錄到快照檔案裡面。

大資料

在Redis儲存的資料量只有幾個GB的時候,使用快照來儲存資料是沒有問題的。Redis會建立子程式並將資料儲存到硬碟裡面,生成快照需要的時間比你讀這句話的時間還要短。三隨著Redis佔用的記憶體越來越多,bgsave在建立子程式時耗費的時間越來越多。如果Redis的記憶體佔用量達到數十個GB,並且剩餘的空閒記憶體並不多,或者Redis執行在虛擬機器上面,那麼執行bgsave可能會導致系統長時間地停頓,也可能引發系統大量地使用虛擬記憶體,從而導致Redis的效能降低至無法使用的程度。

執行bgsave而導致的挺短時間有多長取決於Redis所在的系統:對於真實的硬體,vmware虛擬機器或者KVM虛擬機器來說,Redis進行每佔用一個GB的記憶體,建立該程式的子程式所需的時間就要增加10~20毫秒,而對於Xen虛擬機器來說,根究配置的不同,Redis程式每佔用一個GB的記憶體,建立該程式的子程式所需的時間就要增加200~300毫秒。因此,如果我們的Redis程式佔用了20GB的記憶體,那麼在標準硬體上執行bgsave所建立的子程式將導致Reeis停頓200~400毫秒,如果我們使用的是Xen虛擬機器(亞馬遜EC2和其他幾個雲端計算供應商都使用這種虛擬機器),那麼相同的建立子程式操作將導致Redis停頓4~6秒。使用者必須考慮自己的應用程式能否接受這種停頓。

為了防止Redeis因為建立子程式而出現停頓,我們可以考慮關閉自動儲存呢,轉而通過手動傳送bgsave或者save來進行持久化。手動傳送bgsave一樣會引起停頓,唯一不同的是使用者可以控制停頓出現的時間。另一方面,雖然save會一直阻塞Redis知道快照生產完畢,但是因為它不需要建立子程式,所以就不會想bgsave一樣因為建立子程式而導致Redis停頓;並且因為沒有子程式在爭搶資源,所以save建立快照的速度比bgsave建立快照的速度要快一些。

根據個人經驗,在一臺擁有68GB記憶體的Xen虛擬機器上面,對一個佔用50GB記憶體的Redis伺服器執行bgsave命令的話,光是建立子程式就需要花費15秒以上,而生成快照則需要花費15~20分鐘;但使用save值需要3~5分鐘就可以完成快照的生成工作。因為的應用程式只需要每天生成一次快照,所以我寫了一個指令碼,讓它在每天凌晨3點停止所有客戶端對Redis的訪問,呼叫save命令並等待該命令執行完畢,之後備份剛剛生成的快照檔案,並通知客戶端繼續執行操作。

如果使用者能夠妥善地處理快照持久化可能會帶來的大量資料丟失,那麼快照持久化對使用者來說將使一個不錯的選擇,但對於很多用於程式來說,丟失15分鐘、1小時甚至更長時間的資料都是不可接受的,在這種情況情況下,我們可以使用AOF持久化來將儲存在記憶體裡面的資料儘快的儲存到硬碟裡面。

上一篇文章:Python–Redis實戰:第四章:資料安全與效能保障:第1節:持久化選項
下一篇文章:Python–Redis實戰:第四章:資料安全與效能保障:第3節:AOF持久化

相關文章