基于一致性哈希的分布式内存键值存储——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

相关文章
相关标签/搜索