基於一致性哈希的分佈式內存鍵值存儲——CHKV

Consistent Hashing based Key-Value Memory Storage

基於一致性哈希的分佈式內存鍵值存儲——CHKV。java

系統設計

  • NameNode : 維護key與節點的映射關係(Hash環),用心跳檢測DataNode(通常被動,被動失效時主動詢問三次),節點增減等系統信息變化時調整數據並通知Client;
  • DataNode : 存儲具體的數據,向NameNode主動發起心跳並採用請求響應的方式來實現上下線,便於NameNode挪動數據
  • Client : 負責向NameNode請求DataNode數據和Hash算法等系統信息並監聽其變化,操縱數據時直接向對應DataNode發起請求就行,暫時只包含set,get,delete三個操做

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

若當成內存數據庫使用,則只要有一個 DataNode 失效(未經請求與數據轉移就下線了)整個系統就不可對外服務; 若當成內存緩存使用,則 DataNode 失效只是失去了一部分緩存,系統仍然可用。 github

客戶要使用CHKV就必須使用Client庫或者本身依據協議(兼容redis)實現,能夠是多種語言的API。redis

分析

要想實現高可用有兩點: NameNode 要主從雙機熱備,避免單點失效;每一個 DataNode 能夠作成主從複製甚至集羣。算法

各個組件之間的鏈接狀況:數據庫

  • NameNode 要保持和 NClient 的TCP長鏈接,可是隻有在集羣發生變化時纔有交互,因此使用IO多路複用負載就不大
  • NameNode 要和 MDataNode 保持心跳,TCP請求響應式,負載與 M 和心跳間隔秒數 interval 有關
  • DataNodeClient 是TCP請求響應式操做,操做結束斷開鏈接,也能夠考慮加入鏈接池
  • DataNodeNameNode 保持心跳
  • ClientNameNode 保持TCP長鏈接
  • ClientDataNode TCP請求響應式操做

以下圖所示,有4個鏈接,其中一、2要保持鏈接,三、4完成請求後就斷開鏈接緩存

NameNode
                   ||       ||     
  一、心跳請求響應||              ||二、監聽長鏈接 
             ||   三、數據請求響應   ||     
          DataNodes  ==========  Clients
           ||    ||
              ||
      四、數據轉移,可複用3  
複製代碼

開發優先級:三、一、四、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

NameNode和DataNode啓動後就可使用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 附近開始,通過一、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

可見這個時候四、3個線程qps都大於2個線程,符合驗證,可是4的qps又比3少,說明線程太多反而很差, 然而把數據大小調到900byte時,4個線程又比3個線程的qps大了, 因此這個參數真的要針對不一樣的應用場景作出不一樣的調整,總結起來就是輕量快速的操做適宜線程適當少,重量慢速操做適宜線程適當多。

將來工做

水平有限,目前項目的問題還不少,能夠改進的地方還不少,先列個清單:

  • 高可用性保證
  • 斷線重連
  • DataNode遷移數據的完整性保證
  • 遷移過程數據的一致性
  • 對於WeakReference的支持
  • 更多數據類型
  • 更多操做
  • 完整的校驗機制
  • 等等......

所有代碼在Github上,歡迎 star,歡迎 issue,歡迎 fork,歡迎 pull request...... 總之就是歡迎你們和我一塊兒完善這個項目,一塊兒進步。

戳此看原文,來自MageekChiu

相關文章
相關標籤/搜索