1. 程式人生 > >Pika的設計及實現

Pika的設計及實現

pika是360奇虎公司開源的一款類redis儲存系統,主要解決的是使用者使用 Redis 的記憶體大小超過 50G、80G 等等這樣的情況,會遇到啟動恢復時間長,一主多從代價大,硬體成本貴,緩衝區容易寫滿等問題。

Pika 就是針對這些場景的一個解決方案:

  • Pika 的單執行緒的效能肯定不如 Redis,Pika 是多執行緒的結構,因此線上程數比較多的情況下,某些資料結構的效能可以優於 Redis;
  • Pika 肯定不是完全優於 Redis 的方案,只是在某些場景下面更適合,DBA 可以根據業務的場景挑選合適的方案。

Pika架構

下圖是Pika的實現框架:

network proxy(網路代理) redis protocol(redis 協議)

主要組成

  • 網路模組 pink,對網路程式設計的封裝,使用者實現一個高效能的 server 只需要實現對應的 DealMessage 函式即可,支援單執行緒模型、多執行緒 worker 模型;
  • 執行緒模組,見下文;
  • 儲存引擎 nemo,基於 Rocksdb 修改,封裝 Hash, List, Set, Zset 等資料結構;
  • 日誌模組 binlog,解決了同步緩衝區太小的問題;

執行緒模組

Pika 基於 pink 對執行緒進行封裝,使用多個工作執行緒來進行讀寫操作,由底層 nemo 引擎來保證執行緒安全,執行緒分為 11 種:

  1. PikaServer:主執行緒
  2. DispatchThread:監聽 1 個埠,接收使用者連線請求
  3. ClientWorker:存在多個(使用者配置),每個執行緒裡有若干個使用者客戶端的連線,負責接收處理使用者命令並返回結果,每個執行緒執行寫命令後,追加到 binlog 中
  4. Trysync:嘗試與 master 建立首次連線,並在以後出現故障後發起重連
  5. ReplicaSender:存在多個(動態建立銷燬,本 master 節點掛多少個 slave 節點就有多少個),每個執行緒根據 slave 節點發來的同步偏移量,從 binlog 指定的偏移開始實時同步命令給 slave 節點
  6. ReplicaReceiver:存在 1 個(動態建立銷燬,一個 slave 節點同時只能有一個 master),將使用者指定或當前的偏移量傳送給 master 節點並開始接收執行 master 實時發來的同步命令,在本地使用和 master 完全一致的偏移量來追加 binlog
  7. SlavePing:slave 用來向 master 傳送心跳進行存活檢測
  8. HeartBeat:master 用來接收所有 slave 傳送來的心跳並恢復進行存活檢測
  9. bgsave:後臺 dump 執行緒
  10. scan:後臺掃描 keyspace 執行緒
  11. purge:後臺刪除 binlog 執行緒

nemo儲存引擎

nemo本質上是對rocksdb的改造和封裝,使其支援多資料結構的儲存(rocksdb只支援kv儲存)。總的來說,nemo支援五種資料結構型別的儲存:KV鍵值對、Hash結構、List結構、Set結構和ZSet結構。因為rocksdb的儲存方式只有kv一種結構,所以以上所說的5種資料結構的儲存最終都要落盤到rocksdb的kv儲存方式上。

1、KV鍵值對

KV儲存沒有新增額外的元資訊,只是在value的結尾加上8個位元組的附加資訊(前4個位元組表示version,後 4個位元組表示ttl)作為最後落盤kv的值部分。具體如下圖:

version欄位用於對該鍵值對進行標記,以便後續的處理,如刪除一個鍵值對時,可以在該version進行標記,後續再進行真正的刪除,這樣可以減少刪除操作所導致的服務阻塞時間。

2、Hash結構

對於每一個Hash儲存,它包括hash鍵(key),hash鍵下的域名(field)和儲存的值 (value)。nemo的儲存方式是將key和field組合成為一個新的key,將這個新生成的key與所要儲存的value組成最終落盤的kv鍵值對。同時,對於每一個hash鍵,nemo還為它添加了一個儲存元資訊的落盤kv,它儲存的是對應hash鍵下的所有域值對的個數。下面的是具體的實現方式:

每個hash鍵的元資訊的落盤kv的儲存格式:

前面的橫條代表的儲存每個hash鍵的落盤kv鍵值對的鍵部分,它有兩欄位組成:

  • 第一個欄位是一個’H’字元,表示這儲存時hash鍵的元資訊;
  • 第二個欄位是對應的hash鍵的字串內容;

後面的橫條代表的該元資訊的值,它表示對應的hash鍵中的域值對(field-value)的數量,大小為8個位元組(型別是int64_t)。

每個hash鍵、field、value到落盤kv的對映轉換:

前面的橫條對應落盤kv鍵值對的鍵部分:

  • 第一個欄位是一個字元’h’,表示的是hash結構的key;
  • 第二個欄位是hash鍵的字串長度,用一個位元組(uint8_t型別)來表示;
  • 第三個欄位是hash鍵的內容,因為第二個欄位是一個位元組,所以這裡限定hash鍵的最大字串長度是254個位元組;
  • 第四個欄位是field的內容。

後面的橫條代表的是落盤kv鍵值對的值部分,和KV結構儲存一樣,它是存入的value值加上8個位元組的version欄位和8個位元組的ttl欄位得到的。 

3、List結構

每個List結構的底層儲存也是採用連結串列結構來完成的。對於每個List鍵,它的每個元素都落盤為一個kv鍵值對,作為一個連結串列的一個節點,稱為元素節點。和hash一樣,每個List鍵也擁有自己的元資訊。

每個元資訊的落盤kv的儲存格式 

前面橫條表示儲存元資訊的落盤kv的鍵部分,和前面的hash結構是類似的;

後面的橫條表示儲存List鍵的元資訊,它有四個欄位,從前到後分別為該List鍵內的元素個數、最左邊的元素節點的sequence(相當於連結串列頭)、最右邊的元素節點的sequence(相當於連結串列尾)、下一個要插入元素節點所應該使用的sequence。

每個元素節點對應的落盤kv儲存格式

前面橫條代表的是最終落盤kv結構的鍵部分,總共4個欄位,前面三個字元段分別為一個字元’l’(表明是List結構的結存),List鍵的字串長度(1個位元組)、List鍵的字串內容(最多254個位元組),第四個欄位是該元素節點所對應的索引值,用8個位元組表示(int64_t型別),對於每個元素節點,這個索引(sequence)都是唯一的,是其他元素節點訪問該元素節點的唯一媒介;往一個空的List鍵內新增一個元素節點時,該新增的元素節點的sequence為1,下次一次新增的元素節點的sequence為2,依次順序遞增,即使中間有元素被刪除了,被刪除的元素的sequence也不會被之後新插入的元素節點使用,這就保證了每個元素節點的sequence都是唯一的。

後面的橫條代表的是具體落盤kv結構的,它有5個欄位,後面的三個欄位分別為存入的value值、version、ttl,這和前面的hash結構儲存是類似的;前兩個欄位分別表示的是前一個元素節點的sequence、和後一個元素節點的sequence、通過這兩個sequence,就可以知道前一個元素節點和後一個元素節點的羅盤kv的鍵內容,從而實現了一個雙向連結串列的結構。

4、Set結構

每個Set鍵的元資訊對應的落盤kv儲存格式

每個元素節點對應的落盤kv儲存格式 

值的部分只有version和ttl,沒有value欄位。

5、ZSet結構

ZSet儲存結構是一個有序Set,所以對於每個元素,增加了一個落盤kv,在這個增加的羅盤 kv的鍵部分,把該元素對應的score值整合進去,這樣便於依據Score值進行排序(因為從rocksdb內拿出的資料時按鍵排序的),下面是落盤kv的儲存形式。

儲存元資訊的落盤kv的儲存格式 

score值在value部分的落盤kv儲存格式 

score值在key部分的落盤kv儲存格式 

score是從double型別轉變過來的int64_t型別,這樣做是為了可以讓原來的浮點型的score直接參與到字串的排序當中(浮點型的儲存格式與字串的比較方式不相容)。

日誌模組

Pika 的主從同步是使用 Binlog 來完成的:master 執行完一條寫命令就將命令追加到 Binlog 中,ReplicaSender 將這條命令從 Binlog 中讀出來傳送給 slave,slave 的 ReplicaReceiver 收到該命令,執行,並追加到自己的 Binlog 中。

binlog 本質是順序寫檔案,通過 Index + offset 進行同步點檢查,支援全同步 + 增量同步;

當發生主從切換以後,slave 僅需要將自己當前的 Binlog Index + offset 傳送給 master,master 找到後從該偏移量開始同步後續命令。

為了防止讀檔案中寫錯一個位元組則導致整個檔案不可用,所以Pika採用了類似 leveldb log 的格式來進行儲存,具體如下:

主從同步

先說下slave的連線狀態:

  • No Connect,不嘗試成為任何其他節點的slave;
  • Connect,Slaveof後嘗試成為某個節點的slave,傳送trysnc命令和同步點;
  • Connecting,收到master回覆可以slaveof,嘗試跟master建立心跳;
  • Connected, 心跳建立成功;
  • WaitSync,不斷檢測是否DBSync完成,完成後更新DB併發起新的slaveof;

全同步

Pika 支援 master/slave 的複製方式,通過 slave 端的 slaveof 命令激發:

  1. salve 端處理 slaveof 命令,將當前狀態變為 slave,改變連線狀態;
  2. slave的trysync執行緒向 master 發起 trysync,同時將要同步點傳給 master;
  3. master處理trysync命令,發起對slave的同步過程,從同步點開始順序傳送 binlog 或進行全同步;

pika同步依賴於binlog,binlog 檔案會自動或手動刪除,當同步點對應的 binlog 檔案不存在時,需要通過全同步進行資料同步。

需要進行全同步時,master 會將 db 檔案 dump 後傳送給 slave(通過 rsync 的 deamon 模式實現 db 檔案的傳輸),實現邏輯:

  1. slave 向 master 傳送trysnc命令(此時需要開啟rsync後臺服務);
  2. master 發現需要全同步時,判斷是否有備份檔案可用,如果沒有先 dump 一份;
  3. master 通過 rsync 向 slave 傳送 dump 出的檔案;
  4. slave 用收到的檔案替換自己的 db;
  5. slave 用最新的偏移量再次發起 trysnc;
  6. 完成同步;

 Slave 的流程:

 

 Master 的流程: 

增量同步

一主多從的結構master節點也可以給多個slave複用一個Binlog,只不過不同的slave在binglog中有自己的偏移量而已,master執行完一條寫命令就將命令追加到Binlog中,ReplicaSender將這條命令從Binlog中讀出來傳送給slave,slave的ReplicaReceiver收到該命令,執行,並追加到自己的Binlog中。

主要模組:

  • WorkerThread:接受和處理使用者的命令;
  • BinlogSenderThread:負責順序地向對應的從節點發送在需要同步的命令;
  • BinlogReceiverModule: 負責接受主節點發送過來的同步命令
  • Binglog:用於順序的記錄需要同步的命令

主要的工作過程:

  1. 當WorkerThread接收到客戶端的命令,按照執行順序,新增到Binlog裡;
  2. BinglogSenderThread判斷它所負責的從節點在主節點的Binlog裡是否有需要同步的命令,若有則傳送給從節點;
  3. BinglogReceiverModule模組則做以下三件事情:
    • 接收主節點的BinlogSenderThread傳送過來的同步命令;
    • 把接收到的命令應用到本地的資料上;
    • 把接收到的命令新增到本地Binlog裡 至此,一條命令從主節點到從節點的同步過程完成;      

下圖是BinLogReceiverModule(在原始碼中沒有這個物件,這裡是為了說明方便,抽象出來的)的組成,從圖中可以看出BinlogReceiverModule由一個BinlogReceiverThread和多個BinlogBGWorker組成。

  • BinlogReceiverThread: 負責接受由主節點傳送過來的命令,並分發給各個BinlogBGWorker,若當前的節點是隻讀狀態(不能接受客戶端的同步命令),則在這個階段寫Binlog
  • BinlogBGWorker:負責執行同步命令;若該節點不是隻讀狀態(還能接受客戶端的同步命令),則在這個階段寫Binlog(在命令執行之前寫)

BinlogReceiverThread接收到一個同步命令後,它會給這個命令賦予一個唯一的序列號(這個序列號是遞增的),並把它分發給一個BinlogBGWorker;而各個BinlogBGWorker則會根據各個命令的所對應的序列號的順序來執行各個命令,這樣也就保證了命令執行的順序和主節點執行的順序一致了 之所以這麼設計主要原因是:

  1. 配備多個BinlogBGWorker是可以提高主從同步的效率,減少主從同步的滯後延遲;
  2. 讓BinlogBGWorker在執行執行之前寫Binlog可以提高命令執行的並行度;
  3. 在當前節點是非只讀狀態,讓BinglogReceiverThread來寫Binlog,是為了讓Binglog裡儲存的命令順序和命令的執行順序保持一致.

上圖是一個主從同步的一個過程(即根據主節點資料庫的操作日誌,將主節點資料庫的改變過程順序的對映到從節點的資料庫上),從圖中可以看出,每一個從節點在主節點下都有一個唯一對應的BinlogSenderThread。 (為了說明方便,我們定一個“同步命令”的概念,即會改變資料庫的命令,如set,hset,lpush等,而get,hget,lindex則不是)

快照式備份

不同於Redis,Pika的資料主要儲存在磁碟中,這就使得其在做資料備份時有天然的優勢,可以直接通過檔案拷貝實現 實現

快照內容:

  • 當前db的所有檔名
  • manifest檔案大小
  • sequence_number
  • 同步點
    • binlog filenum
    • offset

流程

  • 打快照:阻寫,並在這個過程中或的快照內容
  • 非同步執行緒拷貝檔案:通過修改Rocksdb提供的BackupEngine拷貝快照中檔案,這個過程中會阻止檔案的刪除

鎖的應用

應用掛起指令,在掛起指令的執行中,會新增寫鎖,以確保,此時沒有其他指令執行。其他的普通指令在會新增讀鎖,可以並行訪問。其中掛起指令有:

  1. trysync
  2. bgsave
  3. flushall
  4. readonly

在pika系統中,對於資料庫的操作都需要新增行鎖,主要在應用於兩個地方,在系統上層指令過程中和在資料引擎層面。在pika系統中,對於寫指令(會改變資料狀態,如SET,HSET)需要除了更新資料庫狀態,還涉及到pika的增量同步,需要在binlog中新增所執行的寫指令,用於保證master和slave的資料庫狀態一致。故一條寫指令的執行,主要有兩個部分:

  1. 更改資料庫狀態
  2. 將指令新增到binlog中

其加鎖情況,如下圖: 

在圖中可以看到,對同一個key,加了兩次行鎖,在實際應用中,pika上所加的鎖就已經能夠保證資料訪問的正確性。如果只是為了pika所需要的業務,nemo層面使用行鎖是多餘的,但是nemo的設計初衷就是通過對rocksdb的改造和封裝提供一套完整的類redis資料訪問的解決方案,而不僅僅是為pika提供資料庫引擎。這種設計思路也是秉承了Unix中的設計原則:Write programs that do one thing and do it well。

這樣設計大大降低了pika與nemo之間的耦合,也使得nemo可以被單獨拿出來測試和使用,在pika中的資料遷移工具就是完全使用nemo來完成,不必依賴任何pika相關的東西。另外對於nemo感興趣或者有需求的團隊也可以直接將nemo作為資料庫引擎而不需要修改任何程式碼就能使用完整的資料訪問功能。

參考文件:

http://toutiao.com/a6283628736621461761/

https://github.com/Qihoo360/pika/wiki