redis持久化,主從及資料備份
現在在專案裡已經大量使用redis了,為了提高redis的效能和可靠性我們需要知道和做到以下幾件事:
常用記憶體優化手段與引數
redis的效能如何是完全依賴於記憶體的,所以我們需要知道如何來控制和節省記憶體。
首先最重要的一點是不要開啟Redis的VM選項,即虛擬記憶體功能,這個本來是作為Redis儲存超出實體記憶體資料的一種資料在記憶體與磁碟換入換出的一個持久化策略,但是其記憶體管理成本非常的高,所以要關閉VM功能,請檢查你的redis.conf檔案中 vm-enabled 為 no。
其次最好設定下redis.conf中的maxmemory選項,該選項是告訴Redis當使用了多少實體記憶體後就開始拒絕後續的寫入請求,該引數能很好的保護好你的Redis不會因為使用了過多的實體記憶體而導致swap,最終嚴重影響效能甚至崩潰。
另外Redis為不同資料型別分別提供了一組引數來控制記憶體使用,我們知道Redis Hash是value內部為一個HashMap,如果該Map的成員數比較少,則會採用類似一維線性的緊湊格式來儲存該Map, 即省去了大量指標的記憶體開銷,這個引數控制對應在redis.conf配置檔案中下面2項:
hash-max-zipmap-entries 64hash-max-zipmap-value 512
含義是當value這個Map內部不超過多少個成員時會採用線性緊湊格式儲存,預設是64,即value內部有64個以下的成員就是使用線性緊湊儲存,超過該值自動轉成真正的HashMap。
hash-max-zipmap-value 含義是當 value這個Map內部的每個成員值長度不超過多少位元組就會採用線性緊湊儲存來節省空間。
以上2個條件任意一個條件超過設定值都會轉換成真正的HashMap,也就不會再節省記憶體了,那麼這個值是不是設定的越大越好呢,答案當然是否定的,HashMap的優勢就是查詢和操作的時間複雜度都是O(1)的,而放棄Hash採用一維儲存則是O(n)的時間複雜度,如果成員數量很少,則影響不大,否則會嚴重影響效能,所以要權衡好這個值的設定,總體上還是最根本的時間成本和空間成本上的權衡。
同樣類似的引數還有:
list-max-ziplist-entries 512說明:list資料型別多少節點以下會採用去指標的緊湊儲存格式。
list-max-ziplist-value 64說明:list資料型別節點值大小小於多少位元組會採用緊湊儲存格式。
說明:set資料型別內部資料如果全部是數值型,且包含多少節點以下會採用緊湊格式儲存。
Redis內部實現沒有對記憶體分配方面做過多的優化,在一定程度上會存在記憶體碎片,不過大多數情況下這個不會成為Redis的效能瓶頸,不過如果在Redis內部儲存的大部分資料是數值型的話,Redis內部採用了一個shared integer的方式來省去分配記憶體的開銷,即在系統啟動時先分配一個從1~n 那麼多個數值物件放在一個池子中,如果儲存的資料恰好是這個數值範圍內的資料,則直接從池子裡取出該物件,並且通過引用計數的方式來共享,這樣在系統儲存了大量數值下,也能一定程度上節省記憶體並且提高效能,這個引數值n的設定需要修改原始碼中的一行巨集定義REDIS_SHARED_INTEGERS,該值預設是10000,可以根據自己的需要進行修改,修改後重新編譯就可以了。
持久化
redis是一個支援持久化的記憶體資料庫,也就是說redis需要經常將記憶體中的資料同步到磁碟來保證持久化。redis支援兩種持久化方式,一種是 Snapshotting(快照)也是預設方式,另一種是Append-only file(縮寫aof)的方式。
snapshotting
快照是預設的持久化方式。這種方式是就是將記憶體中資料以快照的方式寫入到二進位制檔案中,預設的檔名為dump.rdb。可以通過配置設定自動做快照持久化的方式。我們可以配置redis在n秒內如果超過m個key被修改就自動做快照,下面是預設的快照儲存配置:
save 900 1 #900秒內如果超過1個key被修改,則發起快照儲存
save 300 10 #300秒內容如超過10個key被修改,則發起快照儲存
save 60 10000 #60秒內容如超過10000個key被修改,則發起快照儲存
也可以命令列的方式讓redis進行snapshotting:redis-cli -h ip -p port bgsave
儲存快照有save和bgsave兩個命令,save操作是在主執行緒中儲存快照的,由於redis是用一個主執行緒來處理所有 client的請求,這種方式會阻塞所有client請求,所以不推薦使用。
快照生成過程大致如下:
- redis呼叫fork,現在有了子程序和父程序;
- 父程序繼續處理client請求,子程序負責將記憶體內容寫入到臨時檔案。由於os的寫時複製機制(copy on write)父子程序會共享相同的物理頁面,當父程序處理寫請求時os會為父程序要修改的頁面建立副本,而不是寫共享的頁面。所以子程序的地址空間內的資料是fork時刻整個資料庫的一個快照;
- 當子程序將快照寫入臨時檔案完畢後,用臨時檔案替換原來的快照檔案,然後子程序退出。
同時snapshotting也有不足的,因為兩次快照操作之間是有時間間隔的,一旦資料庫出現問題,那麼快照檔案中儲存的資料並不是全新的,從上次快照檔案生成到Redis停機這段時間的資料全部丟掉了。如果業務對資料準確性要求極高的話,就得采用aof持久化機制了。
aof
aof 比快照方式有更好的持久化性,是由於在使用aof持久化方式時,redis會將每一個收到的寫命令都通過write函式追加到檔案中(預設是 appendonly.aof)。當redis重啟時會通過重新執行檔案中儲存的寫命令來在記憶體中重建整個資料庫的內容。當然由於os會在核心中快取 write做的修改,所以可能不是立即寫到磁碟上。這樣aof方式的持久化也還是有可能會丟失部分修改。不過我們可以通過配置檔案告訴redis我們想要通過fsync函式強制os寫入到磁碟的時機。有三種方式如下(預設是:每秒fsync一次):
appendonly yes //啟用aof持久化方式
# appendfsync always //每次收到寫命令就立即強制寫入磁碟,最慢的,但是保證完全的持久化,不推薦使用
appendfsync everysec //每秒鐘強制寫入磁碟一次,在效能和持久化方面做了很好的折中,推薦
# appendfsync no //完全依賴os,效能最好,持久化沒保證
aof 的方式也同時帶來了另一個問題。持久化檔案會變的越來越大。例如我們呼叫incr test命令100次,檔案中必須儲存全部的100條命令,其實有99條都是多餘的。因為要恢復資料庫的狀態其實檔案中儲存一條set test 100就夠了。為了壓縮aof的持久化檔案。redis提供了bgrewriteaof命令。收到此命令redis將使用與快照類似的方式將記憶體中的資料 以命令的方式儲存到臨時檔案中,最後替換原來的檔案。bgrewriteaof命令如下:
redis-cli -h ip -p port bgrewriteaof
bgrewriteaof命令執行過程如下:- redis呼叫fork ,現在有父子兩個程序;
- 子程序根據記憶體中的資料庫快照,往臨時檔案中寫入重建資料庫狀態的命令;
- 父程序繼續處理client請求,除了把寫命令寫入到原來的aof檔案中。同時把收到的寫命令快取起來。這樣就能保證如果子程序重寫失敗的話並不會出問題;
- 當子程序把快照內容寫入以命令方式寫到臨時檔案中後,子程序發訊號通知父程序。然後父程序把快取的寫命令也寫入到臨時檔案;
- 現在父程序可以使用臨時檔案替換老的aof檔案,並重命名,後面收到的寫命令也開始往新的aof檔案中追加。
這兩種持久化方式有各自的特點,快照相對效能影響不大,但一旦崩潰,資料量丟失較大,而aof資料安全性較高,但效能影響較大,這就得根據業務特點自行選擇了。
主從複製
redis的主從複製策略是通過其持久化的rdb檔案來實現的,其過程是先dump出rdb檔案,將rdb檔案全量傳輸給slave,然後再將dump後的操作實時同步到slave中。
要使用主從功能需要在slave端進行簡單的配置:slaveof master_ip master_port #如果這臺機器是臺redis slave,可以開啟這個設定。
slave-serve-stale-data no #如果slave 無法與master 同步,設定成slave不可讀,方便監控指令碼發現問題。
配置好之後啟動slave端就可以進行主從複製了,主從複製的過程大致如下:
- Slave端在配置檔案中添加了slaveof指令,於是Slave啟動時讀取配置檔案,初始狀態為REDIS_REPL_CONNECT;
- Slave端在定時任務serverCron(Redis內部的定時器觸發事件)中連線Master,傳送sync命令,然後阻塞等待master傳送回其記憶體快照檔案(最新版的Redis已經不需要讓Slave阻塞);
- Master端收到sync命令簡單判斷是否有正在進行的記憶體快照子程序,沒有則立即開始記憶體快照,有則等待其結束,當快照完成後會將該檔案傳送給Slave端;
- Slave端接收Master發來的記憶體快照檔案,儲存到本地,待接收完成後,清空記憶體表,重新讀取Master發來的記憶體快照檔案,重建整個記憶體表資料結構,並最終狀態置位為 REDIS_REPL_CONNECTED狀態,Slave狀態機流轉完成;
- Master端在傳送快照檔案過程中,接收的任何會改變資料集的命令都會暫時先儲存在Slave網路連線的傳送快取佇列裡(list資料結構),待快照完成後,依次發給Slave,之後收到的命令相同處理,並將狀態置位為 REDIS_REPL_ONLINE。
從以上的複製過程中可以發現,Slave從庫在連線Master主庫時,Master會進行記憶體快照,然後把整個快照檔案發給Slave,也就是沒有象MySQL那樣有複製位置的概念,即無增量複製,如果一個master連線多個slave,就會比較影響master效能了。
資料備份策略
具體的備份策略是可以很靈活的,比如可以大致如下:
- 為了提高master的效能關閉master的持久化機制,即不進行快照也不進行aof,而是在凌晨訪問量低的時候定時的用bgsave命令進行快照,並將快照檔案儲存到備份伺服器上;
- slave端開啟aof機制,並定時的用bgrewriteaof 進行資料壓縮,將壓縮後的資料檔案儲存到備份伺服器上;
- 定時的檢查master與slave上的資料是否一致;
- 當master出問題並需要恢復時,如果採用master的備份快照恢復直接將備份的dump.rdb拷貝到相應路徑下重啟即可;如果要從slave端恢復,需要在slave端執行一次快照,然後將快照檔案拷貝到master路徑下然後重啟即可。不過有一點需要注意的是,master重啟時slave端資料會被沖掉,所以slave端要在master重啟前做好備份。
持久化磁碟IO方式及其帶來的問題
有Redis線上運維經驗的人會發現Redis在實體記憶體使用比較多,但還沒有超過實際實體記憶體總容量時就會發生不穩定甚至崩潰的問題,有人認為是基於快照方式持久化的fork系統呼叫造成記憶體佔用加倍而導致的,這種觀點是不準確的,因為fork 呼叫的copy-on-write機制是基於作業系統頁這個單位的,也就是隻有有寫入的髒頁會被複制,但是一般的系統不會在短時間內所有的頁都發生了寫入而導致複製,那麼是什麼原因導致Redis崩潰的呢?
答案是Redis的持久化使用了Buffer IO造成的,所謂Buffer IO是指Redis對持久化檔案的寫入和讀取操作都會使用實體記憶體的Page Cache,而大多數資料庫系統會使用Direct IO來繞過這層Page Cache並自行維護一個數據的Cache,而當Redis的持久化檔案過大(尤其是快照檔案),並對其進行讀寫時,磁碟檔案中的資料都會被載入到實體記憶體中作為作業系統對該檔案的一層Cache,而這層Cache的資料與Redis記憶體中管理的資料實際是重複儲存的,雖然核心在實體記憶體緊張時會做Page Cache的剔除工作,但核心可能認為某塊Page Cache更重要,而讓你的程序開始Swap,這時你的系統就會開始出現不穩定或者崩潰了。經驗是當你的Redis實體記憶體使用超過記憶體總容量的3/5時就會開始比較危險了。