Redis學習筆記--Redis持久化

OkidoGreen發表於2017-03-30

Redis是一個支援持久化的記憶體資料庫,也就是說redis需要經常將記憶體中的資料同步到磁碟來保證持久化。redis支援四種持久化方式,一是 Snapshotting(快照)也是預設方式;二是Append-only file(縮寫aof)的方式;三是虛擬記憶體方式;四是diskstore方式。下面分別介紹之。

(一)Snapshotting

       快照是預設的持久化方式。這種方式是就是將記憶體中資料以快照的方式寫入到二進位制檔案中,預設的檔名為dump.rdb。可以通過配置設定自動做快照持久化的方式。我們可以配置redisn秒內如果超過mkey被修改就自動做快照,下面是預設的快照儲存配置:

save 900 1  #900秒內如果超過1個key被修改,則發起快照儲存
save 300 10 #300秒內容如超過10個key被修改,則發起快照儲存
save 60 10000


快照儲存過程:

       1. redis呼叫fork,現在有了子程式和父程式。
       2. 父程式繼續處理client請求,子程式負責將記憶體內容寫入到臨時檔案。由於os的寫時複製機制(copy on write)父子程式會共享相同的物理頁面,當父程式處理寫請求時os會為父程式要修改的頁面建立副本,而不是寫共享的頁面。所以子程式的地址空間內的資料是fork時刻整個資料庫的一個快照。
       3. 當子程式將快照寫入臨時檔案完畢後,用臨時檔案替換原來的快照檔案,然後子程式退出(fork一個程式入內在也被複制了,即記憶體會是原來的兩倍)。

       client 也可以使用save或者bgsave命令通知redis做一次快照持久化。save操作是在主執行緒中儲存快照的,由於redis是用一個主執行緒來處理所有 client的請求,這種方式會阻塞所有client請求。所以不推薦使用。另一點需要注意的是,每次快照持久化都是將記憶體資料完整寫入到磁碟一次,並不是增量的只同步髒資料。如果資料量大的話,而且寫操作比較多,必然會引起大量的磁碟io操作,可能會嚴重影響效能。
       另外由於快照方式是在一定間隔時間做一次的,所以如果redis意外down掉的話,就會丟失最後一次快照後的所有修改。如果應用要求不能丟失任何修改的話,可以採用aof持久化方式。下面介紹:

(二)Append-only file

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將使用與快照類似的方式將記憶體中的資料以命令的方式儲存到臨時檔案中,最後替換原來的檔案。具體過程如下:

       1.  redis呼叫fork ,現在有父子兩個程式
       2. 子程式根據記憶體中的資料庫快照,往臨時檔案中寫入重建資料庫狀態的命令
       3. 父程式繼續處理client請求,除了把寫命令寫入到原來的aof檔案中。同時把收到的寫命令快取起來。這樣就能保證如果子程式重寫失敗的話並不會出問題。
       4. 當子程式把快照內容寫入已命令方式寫到臨時檔案中後,子程式發訊號通知父程式。然後父程式把快取的寫命令也寫入到臨時檔案。
       5. 現在父程式可以使用臨時檔案替換老的aof檔案,並重新命名,後面收到的寫命令也開始往新的aof檔案中追加。

       需要注意到是重寫aof檔案的操作,並沒有讀取舊的aof檔案,而是將整個記憶體中的資料庫內容用命令的方式重寫了一個新的aof檔案,這點和快照有點類似。

 

(三)虛擬記憶體方式(desprecated

首先說明:在Redis-2.4後虛擬記憶體功能已經被deprecated了,原因如下:

1slow restart重啟太慢

2slow saving儲存資料太慢

3slow replication上面兩條導致 replication 太慢

4complex code程式碼過於複雜

下面還是介紹一下redis的虛擬記憶體。

redis的虛擬記憶體與os的虛擬記憶體不是一碼事,但是思路和目的都是相同的。就是暫時把不經常訪問的資料從記憶體交換到磁碟中,從而騰出寶貴的記憶體空間用於其他需要訪問的資料。尤其是對於redis這樣的記憶體資料庫,記憶體總是不夠用的。除了可以將資料分割到多個redis server外。另外的能夠提高資料庫容量的辦法就是使用vm把那些不經常訪問的資料交換的磁碟上。如果我們的儲存的資料總是有少部分資料被經常訪問,大部分資料很少被訪問,對於網站來說確實總是隻有少量使用者經常活躍。當少量資料被經常訪問時,使用vm不但能提高單臺redis server資料庫的容量,而且也不會對效能造成太多影響。

        redis沒有使用os提供的虛擬記憶體機制而是自己在使用者態實現了自己的虛擬記憶體機制,作者在自己的blog專門解釋了其中原因。

http://antirez.com/post/redis-virtual-memory-story.html
主要的理由有兩點:
       1. os 的虛擬記憶體是已4k頁面為最小單位進行交換的。而redis的大多數物件都遠小於4k,所以一個os頁面上可能有多個redis物件。另外redis的集合物件型別如list,set可能存在與多個os頁面上。最終可能造成只有10%key被經常訪問,但是所有os頁面都會被os認為是活躍的,這樣只有記憶體真正耗盡時os才會交換頁面。
       2.相比於os的交換方式。redis可以將被交換到磁碟的物件進行壓縮,儲存到磁碟的物件可以去除指標和物件後設資料資訊。一般壓縮後的物件會比記憶體中的物件小10倍。這樣redisvm會比os vm能少做很多io操作。

       下面是vm相關配置:

slaveof 192.168.1.1 6379  #指定master的ip和埠

vm-enabled yes          #開啟vm功能
vm-swap-file /tmp/redis.swap   #交換出來的value儲存的檔案路徑/tmp/redis.swap
vm-max-memory 1000000  #redis使用的最大記憶體上限,超過上限後redis開始交換value到磁碟檔案中
vm-page-size 32        #每個頁面的大小32個位元組
vm-pages 134217728     #最多使用在檔案中使用多少頁面,交換檔案的大小 = vm-page-size * vm-pages
vm-max-threads 4       #用於執行value物件換入換出的工作執行緒數量,0表示不使用工作執行緒(後面介紹)

 

       redisvm在設計上為了保證key的查詢速度,只會將value交換到swap檔案中。所以如果是記憶體問題是由於太多value很小的key造成的,那麼vm並不能解決。和os一樣redis也是按頁面來交換物件的。redis規定同一個頁面只能儲存一個物件。但是一個物件可以儲存在多個頁面中。

redis使用的記憶體沒超過vm-max-memory之前是不會交換任何value的。當超過最大記憶體限制後,redis會選擇較老的物件。如果兩個物件一樣老會優先交換比較大的物件,精確的公式swappability = age*log(size_in_memory)。對於vm-page-size的設定應該根據自己的應用將頁面的大小設定為可以容納大多數物件的大小。太大了會浪費磁碟空間,太小了會造成交換檔案出現碎片。對於交換檔案中的每個頁面,redis會在記憶體中對應一個1bit值來記錄頁面的空閒狀態。所以像上面配置中頁面數量(vm-pages 134217728 )會佔用16M記憶體用來記錄頁面空閒狀態。vm-max-threads表示用做交換任務的執行緒數量。如果大於0推薦設為伺服器的cpu core的數量。如果是0則交換過程在主執行緒進行。

引數配置討論完後,在來簡單介紹下vm是如何工作的:
vm-max-threads設為0(Blocking VM)

換出:
       主執行緒定期檢查發現記憶體超出最大上限後,會直接已阻塞的方式,將選中的物件儲存到swap檔案中,並釋放物件佔用的記憶體,此過程會一直重複直到下面條件滿足
       1.記憶體使用降到最大限制以下
       2.swap檔案滿了
       3.幾乎全部的物件都被交換到磁碟了
換入:
              當有client請求value被換出的key時。主執行緒會以阻塞的方式從檔案中載入對應的value物件,載入時此時會阻塞所有client。然後處理client的請求

vm-max-threads大於0(Threaded VM)
換出:
       當主執行緒檢測到使用記憶體超過最大上限,會將選中的要交換的物件資訊放到一個佇列中交由工作執行緒後臺處理,主執行緒會繼續處理client請求。
換入:
       如果有client請求的key被換出了,主執行緒先阻塞發出命令的client,然後將載入物件的資訊放到一個佇列中,讓工作執行緒去載入。載入完畢後工作執行緒通知主執行緒。主執行緒再執行client的命令。這種方式只阻塞請求value被換出keyclient

       
總的來說blocking vm的方式總的效能會好一些,因為不需要執行緒同步,建立執行緒和恢復被阻塞的client等開銷。但是也相應的犧牲了響應性。threaded vm的方式主執行緒不會阻塞在磁碟io上,所以響應性更好。如果我們的應用不太經常發生換入換出,而且也不太在意有點延遲的話則推薦使用blocking vm的方式。

關於redis vm的更詳細介紹可以參考下面連結:
       http://antirez.com/post/redis-virtual-memory-story.html
       http://redis.io/topics/internals-vm

 

(四)diskstore方式

diskstore方式是作者放棄了虛擬記憶體方式後選擇的一種新的實現方式,也就是傳統的B-tree的方式。具體細節是:

1) 讀操作,使用read through以及LRU方式。記憶體中不存在的資料從磁碟拉取並放入記憶體,記憶體中放不下的資料採用LRU淘汰。

2) 寫操作,採用另外spawn一個執行緒單獨處理,寫執行緒通常是非同步的,當然也可以把cache-flush-delay配置設成0,Redis儘量保證即時寫入。但是在很多場合延遲寫會有更好的效能,比如一些計數器用Redis儲存,在短時間如果某個計數反覆被修改,Redis只需要將最終的結果寫入磁碟。這種做法作者叫per key persistence。由於寫入會按key合併,因此和snapshot還是有差異,disk store並不能保證時間一致性。

由於寫操作是單執行緒,即使cache-flush-delay設成0,多個client同時寫則需要排隊等待,如果佇列容量超過cache-max-memory Redis設計會進入等待狀態,造成呼叫方卡住。

Google Group上有熱心網友迅速完成了壓力測試,當記憶體用完之後,set每秒處理速度從25k下降到10k再到後來幾乎卡住。 雖然通過增加cache-flush-delay可以提高相同key重複寫入效能;通過增加cache-max-memory可以應對臨時峰值寫入。但是diskstore寫入瓶頸最終還是在IO。

3) rdb 和新 diskstore 格式關係
rdb是傳統Redis記憶體方式的儲存格式,diskstore是另外一種格式,那兩者關係如何?

·         通過BGSAVE可以隨時將diskstore格式另存為rdb格式,而且rdb格式還用於Redis複製以及不同儲存方式之間的中間格式。

·         通過工具可以將rdb格式轉換成diskstore格式。

當然,diskstore原理很美好,但是目前還處於alpha版本,也只是一個簡單demo,diskstore.c加上註釋只有300行,實現的方法就是將每個value作為一個獨立檔案儲存,檔名是key的hash值。因此diskstore需要將來有一個更高效穩定的實現才能用於生產環境。但由於有清晰的介面設計,diskstore.c也很容易換成一種B-Tree的實現。很多開發者也在積極探討使用bdb或者innodb來替換預設diskstore.c的可行性。

 

下面介紹一下Diskstore演算法

其實DiskStore類似於Hash演算法,首先通過SHA1演算法把Key轉化成一個40個字元的Hash值,然後把Hash值的前兩位作為一級目錄,然後把Hash值的三四位作為二級目錄,最後把Hash值作為檔名,類似於“/0b/ee/0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33”形式。演算法如下:

dsKeyToPath(key):

char path[1024];

char *hashKey = sha1(key);

path[0] = hashKey[0];

path[1] = hashKey[1];

path[2] = '/';

path[3] = hashKey[2];

path[4] = hashKey[3];

path[5] = '/';

memcpy(path + 6, hashKey, 40);

return path;

 

儲存演算法(如key == apple):

dsSet(key, value, expireTime):

    // d0be2dc421be4fcd0172e5afceea3970e2f3d940

char *hashKey = sha1(key);

 

// d0/be/d0be2dc421be4fcd0172e5afceea3970e2f3d940

char *path = dsKeyToPath(hashKey);

FILE *fp = fopen(path, "w");

rdbSaveKeyValuePair(fp, key, value, expireTime);

fclose(fp)

 

獲取演算法:

dsGet(key):

char *hashKey = sha1(key);

char *path = dsKeyToPath(hashKey);

FILE *fp = fopen(path, "r");

robj *val = rdbLoadObject(fp);

return val;

 

不過DiskStore有個缺點,就是有可能發生兩個不同的Key生成一個相同的SHA1 Hash值,這樣就有可能出現丟失資料的問題。不過這種情況發生的機率比較少,所以是可以接受的。根據作者的意圖,未來可能使用B+tree來替換這種高度依賴檔案系統的實現方法。

 

還可以看:http://www.hoterran.info/redis_persistence

相關文章