Redis內存優化、持久化以及主從復制
Redis 數據庫內存優化參數的配置,每種持久化方式的利與弊以及主從復制的原理以及配置
一、常用內存優化手段與參數
redis的性能如何是完全依賴于內存的,所以我們需要知道如何來控制和節省內存。
首先最重要的一點是不要開啟Redis的VM選項,即虛擬內存功能,這個本來是作為 Redis存儲超出物理內存數據的一種數據在內存與磁盤換入換出的一個持久化策略,但是其內存管理成本非常的高,所以要關閉VM功能,請檢查你的 redis.conf文件中 vm-enabled 為 no。
其次最好設置下redis.conf中的maxmemory選項,該選項是告訴Redis當使用了多少物理內存后就開始拒絕后續的寫入請求,該參數能很好的保護好你的Redis不會因為使用了過多的物理內存而導致swap,最終嚴重影響性能甚至崩潰。
另外Redis為不同數據類型分別提供了一組參數來控制內存使用,我們知道 RedisHash是value內部為一個HashMap,如果該Map的成員數比較少,則會采用類似一維線性的緊湊格式來存儲該Map,即省去了大量指針的內存開銷,這個參數控制對應在redis.conf配置文件中下面2項:
hash-max-zipmap-entries 64
hash-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-max-intset-entries 512
說明:set數據類型內部數據如果全部是數值型,且包含多少節點以下會采用緊湊格式存儲。
Redis內部實現沒有對內存分配方面做過多的優化,在一定程度上會存在內存碎片,不過大多數情況下這個不會成為Redis的性能瓶頸,不過如果在Redis內部存儲的大部分數據是數值型的話,Redis內部采用了一個 sharedinteger的方式來省去分配內存的開銷,即在系統啟動時先分配一個從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的方式也同時帶來了另一個問題。持久化文件會變的越來越大。例如我們調用 incrtest命令100次,文件中必須保存全部的100條命令,其實有99條都是多余的。因為要恢復數據庫的狀態其實文件中保存一條set test100就夠了。為了壓縮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不可讀,方便監控腳本發現題。
- 如果是master--slave(master)--slave這種主從模式的話和master --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的持久化使用了BufferIO造成的,所謂Buffer IO是指Redis對持久化文件的寫入和讀取操作都會使用物理內存的Page Cache,而大多數數據庫系統會使用DirectIO來繞過這層PageCache并自行維護一個數據的Cache,而當Redis的持久化文件過大 (尤其是快照文件),并對其進行讀寫時,磁盤文件中的數據都會被加載到物理內存中作為操作系統對該文件的一層Cache,而這層Cache的數據與 Redis內存中管理的數據實際是重復存儲的,雖然內核在物理內存緊張時會做 PageCache的剔除工作,但內核可能認為某塊PageCache更重要,而讓你的進程開始Swap,這時你的系統就會開始出現不穩定或者崩潰了。經驗是當你的Redis物理內存使用超過內存總容量的3/5時就會開始比較危險了。
1、 快照的方式持久化到磁盤
自動持久化規則配置
save 900 1
save 300 10
save 60 10000
上面的配置規則意思如下:
# In the example below the behaviour will be to save:
# after 900 sec (15 min) if at least 1 key changed
# after 300 sec (5 min) if at least 10 keys changed
# after 60 sec if at least 10000 keys changed
redis也可以關閉自動持久化,注釋掉這些save配置,或者save“”
如果后臺保存到磁盤發生錯誤,將停止寫操作.
stop-writes-on-bgsave-erroryes
使用LZF壓縮rdb文件,這會耗CPU,但是可以減少磁盤占用.
rdbcompression yes
保存rdb和加載rdb文件的時候檢驗,可以防止錯誤,但是要付出約10%的性能,可以關閉他,提高性能。
rdbchecksum yes
導出的rdb文件名
dbfilename dump.rdb
設置工作目錄, rdb文件會寫到該目錄,append only file也會存儲在該目錄下.
dir ./
Redis自動快照保存到磁盤或者調用bgsave,是后臺進程完成的,其他客戶端仍然和可以讀寫redis服務器,后臺保存快照到磁盤會占用大量內存。調用save保存內存中的數據到磁盤,將阻塞客戶端請求,直到保存完畢。
調用shutdown命令,Redis服務器會先調用save,所有數據持久化到磁盤之后才會真正退出。
對于數據丟失的問題:
如果服務器crash,從上一次快照之后的數據將全部丟失。所以在設置保存規則的時候,要根據實際業務設置允許的范圍。
如果對于數據敏感的業務,在程序中要使用恰當的日志,在服務器crash之后,通過日志恢復數據。
2、 Append-only file 的方式持久化
另外一種方式為遞增的方式,將會引起數據變化的操作,持久化到文件中, 重啟redis的時候,通過操作命令,恢復數據.
每次執行寫操作命令之后,都會將數據寫到server.aofbuf中。
# appendfsync always
appendfsync everysec
# appendfsync no
當配置為always的時候,每次server.aofbuf中的數據寫入到文件之后,才會返回給客戶端,這樣可以保證數據不丟,但是頻繁的IO操作,會降低性能。
everysec每秒寫一次,這可能會丟失一秒內的操作。
aof最大的問題就是隨著時間appendfile會變的很大,所以我們需要bgrewriteaof命令重新整理文件,只保留最新的kv數據。