基於一致性雜湊的分散式記憶體鍵值儲存——CHKV

MageekChiu發表於2019-01-19

Consistent Hashing based Key-Value Memory Storage

基於一致性雜湊的分散式記憶體鍵值儲存——CHKV
目前的定位就是作為 CacheDataBase 的功能先不考慮。

系統設計

  • NameNode : 維護 DataNode節點 列表,用心跳檢測 DataNode(一般被動,被動失效時主動詢問三次),節點增減等系統資訊變化時調整資料並通知 Client
  • DataNode : 儲存具體的資料,向 NameNode 主動發起心跳並採用請求響應的方式來實現上下線,便於 NameNode 發起挪動資料指令,實際挪動操作由 DataNode 自行完成;
  • Client : 負責向 NameNode 請求 DataNode 相關資訊並監聽其變化,操縱資料時直接向對應 DataNode 發起請求就行,

目前支援set,get,delete,keys,expire幾個操作;

NameNode 失效則整個系統不可用。

若當成記憶體資料庫使用,則要注意持久化,而且只要有一個 DataNode 失效(未經請求與資料轉移就下線了)整個系統就不可對外服務;
若當成記憶體快取使用,則 DataNode 失效只是失去了一部分快取,系統仍然可用。

DataNode 失效(未經請求與資料轉移就斷開了和 NameNode 的連線)則 NameNode 需要及時通知 Client

客戶 要使用 CHKV 就必須使用 Client 庫或者自己依據協議(相容redis)實現,可以是多種語言的API。
當然也可以把 Client 當做 Proxy,使得 CHKV 內部結構對 客戶 透明,亦即有如下兩種方式:

方式1:

    
      使用者直接使用Client庫
              ||
        ||          ||
    ||                      ||
NameNode        ||      ||      ||      ||
            DataNode DataNode DataNode DataNode ......  

方式2:

         使用者通過Proxy訪問    
              ||  
         Client庫構建的Proxy
              ||
        ||          ||
    ||                      ||
NameNode        ||      ||      ||      ||
            DataNode DataNode DataNode DataNode ......            

分析

要想實現高可用有兩點: NameNode 要主從雙機熱備,避免單點失效;每個 DataNode 可以做成主從複製甚至叢集。

各個元件之間的連線情況:

  • NameNode 要保持和 NClient 的TCP長連線,但是隻有在叢集發生變化時才有互動,所以使用IO多路複用負載就不大
  • NameNode 要和 MDataNode 保持心跳,TCP請求響應式,負載與 M 和心跳間隔秒數 interval 有關
  • DataNodeClient 是TCP請求響應式操作,Client 請求完畢後保留與該 DataNode TCP連線一段時間,以備後續訪問複用連線,連線採取自動過期策略,類似於LRU
  • DataNodeNameNode 保持心跳
  • ClientNameNode 保持TCP長連線
  • ClientDataNode TCP請求響應式操作

如下圖所示,有4個連線:其中1、2要主動心跳來保持連線;3保持連線以備複用並可以自動超時斷開,再次使用時重連;4完成資料轉移後就斷開連線。

                     NameNode
                   ||       ||     
  1、心跳請求響應||              ||2、監聽長連線 
             ||   3、資料請求響應   ||     
          DataNodes  ==========  Clients
           ||    ||
              ||
      4、資料轉移,可複用3  

開發優先順序:3、1、4、2

程式碼結構

  • NameNode : 實現 NameNode 功能

    • handler : handler
    • res : 資源,如常量,命令工廠
    • service : 服務,含Client管理,DataNode管理
  • DataNode : 實現 DataNode 功能

    • command : 處理客戶端各個命令的具體命令物件
    • job : 一些的任務如心跳、資料遷移
    • handler : 處理連線的handler
    • service : 服務,含定時任務管理,資料請求管理
  • Client : 實現 Client 功能

    • handler : handler
    • Client : 暴露給使用者的命令管理
    • Connection : 發出網路請求
  • Common : 實現一些公共的功能,上面三個模組依賴於此模組

    • command : 命令抽象類
    • model : 一些公用的pojo,如請求響應物件
    • util : 一些工具類
    • helper : 輔助指令碼

使用方法

DataNode 執行起來就可以直接使用 redis-cli 連線,如redis-cli -h 127.0.0.1 -p 10100,並進行set、get、del等操作;

注意:要首先執行 NameNode,然後可以通過JVM引數的方式調整埠,在同一臺機器上執行多個 DataNode
若要在不同機器上執行 DataNode 也可以直接修改配置檔案。

新的 DataNode 可以直接上線,NameNode 會自動通知下一個節點轉移相應資料給新節點;DataNode 若要下線,
則可以通過 telnet DataNode 節點的下線監聽埠(TCP監聽) 如 telnet 127.0.0.1 6666
併傳送 k 字元即可,待下線的DataNode收到命令 k 後會自動把資料全部轉移給下一個 DataNode
然後提示程式pid,使用者就可以關閉該DataNode程式了,如 Linuxkill -s 9 23456Windows:taskkill /pid 23456

NameNodeDataNode 啟動後就可以使用 Client 了,程式碼示例如下:

Client 程式碼示例在此,關鍵如下:

    try(Client client = new Client("192.168.0.136","10102")){// 支援自動關閉
        logger.debug(client.set("192.168.0.136:10099","123456")+"");
        logger.debug(client.get("192.168.0.136:10099")+"");
        logger.debug(client.set("112","23")+"");
        logger.debug(client.del("1321")+"");
        logger.debug(client.del("112")+"");
    }

壓力測試

在本機開啟1個 NameNode 和1個 DataNode 直接壓測,4次

redis-benchmark -h 127.0.0.1 -p 10100 -c 100 -t set -q

  • SET: 5006.76 requests per second
  • SET: 5056.43 requests per second
  • SET: 5063.55 requests per second
  • SET: 5123.74.55 requests per second

把以上2個節點日誌級別都調整為 info(實際上 DataNode 節點才會影響 qps),重啟

redis-benchmark -h 127.0.0.1 -p 10100 -c 100 -t set -q

  • SET: 62421.97 requests per second
  • SET: 87260.03 requests per second
  • SET: 92592.59 requests per second
  • SET: 94517.96 requests per second

可見日誌對qps影響很大,是 幾k幾十k 的不同數量級的概念,若把級別改成 error平均qps還能提升 幾k,所以生產環境一定要注意日誌級別。

此外觀察,不重啟並且每次壓測間隔都很小的話,qps一般會從 65k 附近開始,經過1、2次的 88k 左右,最終穩定在 98k 附近,數十次測試,最低 62.4k,最高101.2k

重啟的話,qps就會重複上述變化過程,這應該是和記憶體分配等初始化工作有關,第1次壓測有大量的初始化,而後面就沒了,所以第一次qps都比較低;還可能與 JIT 有關,所以 Java 的效能測試嚴格上來說要忽略掉最初的幾個樣本才對。

經觀察,DataNode程式啟動後,記憶體消耗在59M附近,第1次壓測飆升到134M然後穩定到112M,第2次上升到133M然後穩定到116M,後面每次壓測記憶體都是先增加幾M然後減小更多,最終穩定在76M。

在本機執行一個redis-server程式,然後壓測一下

redis-benchmark -h 127.0.0.1 -p 6379 -c 100 -t set -q

  • SET: 129032.27 requests per second
  • SET: 124533.27 requests per second
  • SET: 130208.34 requests per second
  • SET: 132450.33 requests per second

經數十次測試,qps 穩定在 128k 附近,最高 132.3k ,最低 122.7k 可見CHKV的單個 DataNode 目前效能還比不過單個 redis

DataNode 經過重構後,現在的壓測結果如下

redis-benchmark -h 127.0.0.1 -p 10100 -c 100 -t set -q

  • SET: 78554.59 requests per second
  • SET: 114285.71 requests per second
  • SET: 119047.63 requests per second
  • SET: 123628.14 requests per second

經過多次測試,qps 穩定在 125k 附近,最高 131.9k ,最低 78.6k(這是啟動後第一次壓測的特例,後期穩定時最低是 114.3k),可見重構後
單個 DataNode 和單個 redis-serverqps 差距已經很小了,優化效果還是比較明顯的。

主要優化兩個:去掉單獨的 BusinessHandler 的單獨邏輯執行緒,因為沒有耗時操作,直接在IO執行緒操作反而能省掉切換時間;
DataNode 通過 public static volatile Map<String,String> DATA_POOL 共享資料池,其他相關操作類減少了這個域,省一些記憶體;
第一條對比明顯,很容易直接測試,第二條沒直接測,只是分析。

然後通過 -Xint 或者 -Djava.compiler=NONE 關閉 JIT 使用 解釋模式,再壓測試試。

redis-benchmark -h 127.0.0.1 -p 10100 -c 100 -t set -q

  • SET: 16105.65 requests per second
  • SET: 16244.31 requests per second
  • SET: 16183.85 requests per second
  • SET: 16170.76 requests per second

可見關閉 JITqps 降低了 7倍多,而且每次差別不大(即使是第一次),這也能說明上面(預設是混合模式)第一次壓測的 qps 比後面低了那麼多的原因確實和 JIT 有關。

通過 -Xcomp 使用 編譯模式 ,啟動會很慢。

redis-benchmark -h 127.0.0.1 -p 10100 -c 100 -t set -q

  • SET: 83612.04 requests per second
  • SET: 117647.05 requests per second
  • SET: 121802.68 requests per second
  • SET: 120048.02 requests per second

可見 編譯模式 並沒有比 混合模式 效果好,因為即使是不熱點的程式碼也要編譯,反而浪費時間,所以一般還是選擇預設的 混合模式 較好。

然後來驗證執行緒數、客戶端操作qps 的關係,實驗機器是 4 core、8 processor,我把 DataNodeDataManagerworkerGroup的執行緒數依次減少從 8 調到為 1 (之前的測試都是 4 ),
發現 qps 先升後降,在值為 2 的時候達到最大值,超過了redis,下面是資料

redis-benchmark -h 127.0.0.1 -p 10100 -c 100 -t set -q

  • SET: 93283.04 requests per second
  • SET: 141043.05 requests per second
  • SET: 145560.68 requests per second
  • SET: 145384.02 requests per second

經數十次測試,qps 穩定在 142k 附近,最高 150.6k ,穩定後最低 137.2k
Netty 本身使用了IO多路複用,在客戶端操作都比較輕量(壓測這個 set 也確實比較輕量)時選擇執行緒數較少是合理的,
因為這時候執行緒切換的代價超過了多執行緒帶來的好處,這樣我們也能理解 redis 單執行緒設計的初衷了,
單執行緒雖然有些極端,但是如果考慮 面向快速輕量操作的客戶端單執行緒的安全與簡潔特性,也是最佳的選擇。

但是如果客戶端操作不是輕量級的,比如我們把 set 資料大小調為500bytes,再對 CKHV 不同的 workerGroup執行緒數進行壓測

2 redis-benchmark -h 127.0.0.1 -p 10100 -c 100 -t set -d 500 -q

  • SET: 80450.52 requests per second
  • SET: 102459.02 requests per second
  • SET: 108813.92 requests per second
  • SET: 99206.34 requests per second

3 redis-benchmark -h 127.0.0.1 -p 10100 -c 100 -t set -d 500 -q

  • SET: 92592.59 requests per second
  • SET: 133868.81 requests per second
  • SET: 133868.81 requests per second
  • SET: 135685.22 requests per second

4 redis-benchmark -h 127.0.0.1 -p 10100 -c 100 -t set -d 500 -q

  • SET: 72046.11 requests per second
  • SET: 106723.59 requests per second
  • SET: 114810.56 requests per second
  • SET: 119047.63 requests per second

可見這個時候4、3個執行緒qps都大於2個執行緒,符合驗證,但是4的qps又比3少,說明執行緒太多反而不好,
然而把資料大小調到900byte時,4個執行緒又比3個執行緒的qps大了,
所以這個引數真的要針對不同的應用場景做出不同的調整,總結起來就是輕量快速的操作適宜執行緒 適當少,重量慢速操作適宜執行緒 適當多

未來工作

水平有限,目前專案的問題還很多,可以改進的地方還很多,先列個清單:

  • 高可用性保證
  • 斷線重連
  • DataNode遷移資料的正確性保障
  • 對於WeakReference的支援
  • 更多資料型別
  • 更多操作
  • 完整的校驗機制
  • 等等……

全部程式碼在Github上,歡迎 star,歡迎 issue,歡迎 fork,歡迎 pull request……
總之就是歡迎大家和我一起完善這個專案,一起進步。

戳此看原文,來自MageekChiu

相關文章