用一个例子讲清一致性哈希

一、网站如何分配请求

大多数网站背后肯定不是只有一台服务器提供服务,因为单机的并发量和数据量都是有限的,所以都会用多台服务器构成集群来对外提供服务。
但是问题来了,现在有那么多个节点,要如何分配客户端的请求呢?

解决负载均衡问题的算法很多,不同的负载均衡算法,对应的就是不同的分配策略,适应的业务场景也不同。

最简单的方式,引入一个中间的负载均衡层,让它将外界的请求「轮流」的转发给内部的集群。比如集群有 3 个节点,外界请求有 3 个,那么每个节点都会处理 1 个请求,达到了分配请求的目的。

考虑到每个节点的硬件配置有所区别,我们可以引入权重值,将硬件配置更好的节点的权重值设高,然后根据各个节点的权重值,按照一定比重分配在不同的节点上,让硬件配置更好的节点承担更多的请求,这种算法叫做加权轮询。

但是,加权轮询算法是**无法应对「分布式系统(数据分片的系统)」**的,因为分布式系统中,每个节点存储的数据是不同的。

当我们想提高系统的容量,就会将数据水平切分到不同的节点来存储,也就是将数据分布到了不同的节点。比如一个分布式 KV(key-value) 缓存系统,某个 key 应该到哪个或者哪些节点上获得,应该是确定的,不是说任意访问一个节点都可以得到缓存结果的。

因此,我们要想一个能应对分布式系统的负载均衡算法。

二、使用传统哈希有什么问题

三、使用一致性哈希

一致性哈希算法就很好地解决了分布式系统在扩容或者缩容时,发生过多的数据迁移的问题。

一致哈希算法也用了取模运算,但与哈希算法不同的是,哈希算法是对节点的数量进行取模运算,而一致哈希算法是对 2^32 进行取模运算,是一个固定的值。

一致性哈希要进行两步:

  • 第一步:对存储节点进行哈希计算,也就是对存储节点做哈希映射,比如根据节点的 IP 地址进行哈希;
  • 第二步:当对数据进行存储或访问时,对数据进行哈希映射;

一致性哈希是指将「存储节点」和「数据」都映射到一个首尾相连的哈希环上。

映射的结果值往顺时针的方向的找到第一个节点,就是存储该数据的节点。

假设节点数量从3增加到了4,新的节点D经过哈希计算后映射到了下图中的位置:

可以看到,key-01、key-03 都不受影响,只有 key-02 需要被迁移节点D。

假设节点数量从3减少到了2,比如将节点A移除:

可以看到,key-02 和 key-03 不会受到影响,只有 key-01 需要被迁移节点 B。

在一致哈希算法中,如果增加或者移除一个节点,仅影响该节点在哈希环上顺时针相邻的后继节点,其它数据也不会受到影响

一致性hash算法的目的,就是为了解决2个问题:

  1. hash算法中损毁节点对应的请求失败
  2. 有节点损毁后,请求落点不均匀问题

但是,一致性哈希算法并不保证节点能够在哈希环上分布均匀,这样就会带来一个问题,会有大量的请求集中在一个节点上。

比如,下图中 3 个节点的映射位置都在哈希环的右半边:

这时候有一半以上的数据的寻址都会找节点A,也就是访问请求主要集中的节点A上,这肯定不行的呀,说好的负载均衡呢,这种情况一点都不均衡。

另外,在这种节点分布不均匀的情况下,进行容灾与扩容时,哈希环上的相邻节点容易受到过大影响,容易发生雪崩式的连锁反应。

比如,上图中如果节点A被移除了,当节点A宕机后,根据一致性哈希算法的规则,其上数据应该全部迁移到相邻的节点B上,这样,节点B的数据量、访问量都会迅速增加很多倍,一旦新增的压力超过了节点B的处理能力上限,就会导致节点B崩溃,进而形成雪崩式的连锁反应。

所以,一致性哈希算法虽然减少了数据迁移量,但是存在节点分布不均匀的问题

四、通过虚拟节点提高均衡度

要想解决节点能在哈希环上分配不均匀的问题,就是要有大量的节点,节点数越多,哈希环上的节点分布的就越均匀。

但问题是,实际中我们没有那么多节点。所以这个时候我们就加入

具体做法是,不再将真实节点映射到哈希环上,而是将虚拟节点映射到哈希环上,并将虚拟节点映射到实际节点,所以这里有「两层」映射关系

如下图,引入虚拟节点后,原本哈希环上只有 3 个节点的情况,就会变成有 9 个虚拟节点映射到哈希环上,哈希环上的节点数量多了 3 倍。

可以看到,节点数量多了后,节点在哈希环上的分布就相对均匀了。这时候,如果有访问请求寻址到「A-01」这个虚拟节点,接着再通过「A-01」虚拟节点找到真实节点 A,这样请求就能访问到真实节点 A 了。

上面为了方便理解,每个真实节点仅包含3个虚拟节点,这样能起到的均衡效果其实很有限。而在实际的工程中,虚拟节点的数量会大很多,比如Nginx 的一致性哈希算法,每个权重为1的真实节点就含有160个虚拟节点。

另外,虚拟节点除了会提高节点的均衡度,还会提高系统的稳定性。当节点变化时,会有不同的节点共同分担系统的变化,因此稳定性更高

比如,当某个节点被移除时,对应该节点的多个虚拟节点均会移除,而这些虚拟节点按顺时针方向的下一个虚拟节点,可能会对应不同的真实节点,即这些不同的真实节点共同分担了节点变化导致的压力。

而且,有了虚拟节点后,还可以为硬件配置更好的节点增加权重,比如对权重更高的节点增加更多的虚拟机节点即可。因此,带虚拟节点的一致性哈希方法不仅适合硬件配置不同的节点的场景,而且适合节点规模会发生变化的场景

五、代码模拟示例

示例说明:一个hash环,模拟节点数为256个,实际可用的节点有8个(比如线上实际的ip服务节点),256个模拟节点跟8个ip节点有均匀的映射关系;
我们模拟的环上有256个虚拟节点,真实可用的节点(可以想象为线上实际集群有8个可用ip供存储)。

一致性hash使用方法:

  1. 环上的虚拟节点跟所有真实节点之间有个映射关系,256个节点跟8个节点的映射, 用的是除8取模的对应关系,例如: 0、8、16、24 都对应到node_0; 1、7、15、23都映射到node_1;
  2. 发一个请求字符串, 我们计算出hash值, 除1024取模;
  3. 将取模结果,先映射到虚拟节点环上的点, 比如"hello"的hashcode/256 = 210;
  4. 查询虚拟环和真实节点映射关系,210对应的真实节点为node_2; 于是"hello"就落到节点node_2上;
  5. 调用ConsistentHashMock#dropBadNode(“node_2”)来删除node_2节点;
  6. 删除node_2后,"hello"应该落在211上,对应到环的真实映射是node_3,于是"hello"的请求就落到node_3;
public class ConsistentHashMock {

    /**
     * 假设我们一共初始化有8个节点(可以是ip, 就理解为ip吧);
     * 把256个虚拟节点跟 8个资源节点相对应
     */
    private static        Map<Integer, String>     realNodeMap     = new HashMap<>();
    /**
     * 假设我们的环上有256个虚拟节点
      */
    private static        int                      V_NODES         = 256;
    private static        TreeMap<Integer, String> virtualNodeMap  = new TreeMap<>();
    private static final Integer                  REAL_NODE_COUNT = 8;

    static {
        realNodeMap.put(0, "node_0");
        realNodeMap.put(1, "node_1");
        realNodeMap.put(2, "node_2");
        realNodeMap.put(3, "node_3");
        realNodeMap.put(4, "node_4");
        realNodeMap.put(5, "node_5");
        realNodeMap.put(6, "node_6");
        realNodeMap.put(7, "node_7");

        for (Integer i = 0; i < V_NODES; i++) {
            // 每个虚拟节点跟其取模的余数的 nodeMap 中的key相对应;
            // 下面删除虚拟节点的时候, 就可以根据取模规则来删除 TreeMap中的节点了;
            virtualNodeMap.put(i, realNodeMap.get(i % REAL_NODE_COUNT));
        }
    }


    /**
     * 输入一个id
     *
     * @param value
     * @return
     */
    public static String getRealServerNode(String value) {
        // 1. 传递来一个字符串, 得到它的hash值
        Integer vnode = value.hashCode() % V_NODES;
        // 2.找到对应节点最近的key的节点值
        // ceiling表示向上取整
        String realNode = virtualNodeMap.ceilingEntry(vnode).getValue();

        return realNode;
    }

    /**
     * 模拟删掉一个物理可用资源节点, 其他资源可以返回其他节点
     *
     * @param nodeName
     */
    public static void dropBadNode(String nodeName) {
        int nodek = -1;
        // 1. 遍历 nodeMap 找到故障节点 nodeName对应的key;
        for (Map.Entry<Integer, String> entry : realNodeMap.entrySet()) {
            if (nodeName.equalsIgnoreCase(entry.getValue())) {
                nodek = entry.getKey();
                break;
            }
        }
        if (nodek == -1) {
            System.err.println(nodeName + "在真实资源节点中无法找到, 放弃删除虚拟节点!");
            return;
        }

        // 2. 根据故障节点的 key, 对应删除所有 chMap中的虚拟节点
        Iterator<Entry<Integer, String>> iter = virtualNodeMap.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry<Integer, String> entry = iter.next();
            int key = entry.getKey();
            String value = entry.getValue();
            if (key % REAL_NODE_COUNT == nodek) {
                System.out.println("删除节点虚拟节点: [" + key + " = " + value + "]");
                iter.remove();
            }
        }
    }

    public static void main(String[] args) {
        // 1. 一个字符串请求(比如请求字符串存储到8个节点中的某个实际节点);
        String requestValue = "hello";
        System.out.println(requestValue.hashCode());
        // 2. 打印虚拟节点和真实节点的对应关系;
        System.out.println(virtualNodeMap);
        // 3. 核心: 传入请求信息, 返回实际调用的节点信息
        System.out.println(getRealServerNode(requestValue));
        // 4. 删除某个虚拟节点后
        System.out.println("==========删除 node_2 之后: ================");
        dropBadNode("node_2");
        System.out.println("===============删除之后的虚拟节点map: ===========");
        System.out.println(virtualNodeMap);
        System.out.println("==============删除之后, 获取节点的真正node节点对应者: ");
        System.out.println(getRealServerNode(requestValue));

    }
}

代码运行结果

99162322
{0=node_0, 1=node_1, 2=node_2, 3=node_3, 4=node_4, 5=node_5, 6=node_6, 7=node_7, 8=node_0, 9=node_1, 10=node_2, 11=node_3, 12=node_4, 13=node_5, 14=node_6, 15=node_7, 16=node_0, 17=node_1, 18=node_2, 19=node_3, 20=node_4, 21=node_5, 22=node_6, 23=node_7, 24=node_0, 25=node_1, 26=node_2, 27=node_3, 28=node_4, 29=node_5, 30=node_6, 31=node_7, 32=node_0, 33=node_1, 34=node_2, 35=node_3, 36=node_4, 37=node_5, 38=node_6, 39=node_7, 40=node_0, 41=node_1, 42=node_2, 43=node_3, 44=node_4, 45=node_5, 46=node_6, 47=node_7, 48=node_0, 49=node_1, 50=node_2, 51=node_3, 52=node_4, 53=node_5, 54=node_6, 55=node_7, 56=node_0, 57=node_1, 58=node_2, 59=node_3, 60=node_4, 61=node_5, 62=node_6, 63=node_7, 64=node_0, 65=node_1, 66=node_2, 67=node_3, 68=node_4, 69=node_5, 70=node_6, 71=node_7, 72=node_0, 73=node_1, 74=node_2, 75=node_3, 76=node_4, 77=node_5, 78=node_6, 79=node_7, 80=node_0, 81=node_1, 82=node_2, 83=node_3, 84=node_4, 85=node_5, 86=node_6, 87=node_7, 88=node_0, 89=node_1, 90=node_2, 91=node_3, 92=node_4, 93=node_5, 94=node_6, 95=node_7, 96=node_0, 97=node_1, 98=node_2, 99=node_3, 100=node_4, 101=node_5, 102=node_6, 103=node_7, 104=node_0, 105=node_1, 106=node_2, 107=node_3, 108=node_4, 109=node_5, 110=node_6, 111=node_7, 112=node_0, 113=node_1, 114=node_2, 115=node_3, 116=node_4, 117=node_5, 118=node_6, 119=node_7, 120=node_0, 121=node_1, 122=node_2, 123=node_3, 124=node_4, 125=node_5, 126=node_6, 127=node_7, 128=node_0, 129=node_1, 130=node_2, 131=node_3, 132=node_4, 133=node_5, 134=node_6, 135=node_7, 136=node_0, 137=node_1, 138=node_2, 139=node_3, 140=node_4, 141=node_5, 142=node_6, 143=node_7, 144=node_0, 145=node_1, 146=node_2, 147=node_3, 148=node_4, 149=node_5, 150=node_6, 151=node_7, 152=node_0, 153=node_1, 154=node_2, 155=node_3, 156=node_4, 157=node_5, 158=node_6, 159=node_7, 160=node_0, 161=node_1, 162=node_2, 163=node_3, 164=node_4, 165=node_5, 166=node_6, 167=node_7, 168=node_0, 169=node_1, 170=node_2, 171=node_3, 172=node_4, 173=node_5, 174=node_6, 175=node_7, 176=node_0, 177=node_1, 178=node_2, 179=node_3, 180=node_4, 181=node_5, 182=node_6, 183=node_7, 184=node_0, 185=node_1, 186=node_2, 187=node_3, 188=node_4, 189=node_5, 190=node_6, 191=node_7, 192=node_0, 193=node_1, 194=node_2, 195=node_3, 196=node_4, 197=node_5, 198=node_6, 199=node_7, 200=node_0, 201=node_1, 202=node_2, 203=node_3, 204=node_4, 205=node_5, 206=node_6, 207=node_7, 208=node_0, 209=node_1, 210=node_2, 211=node_3, 212=node_4, 213=node_5, 214=node_6, 215=node_7, 216=node_0, 217=node_1, 218=node_2, 219=node_3, 220=node_4, 221=node_5, 222=node_6, 223=node_7, 224=node_0, 225=node_1, 226=node_2, 227=node_3, 228=node_4, 229=node_5, 230=node_6, 231=node_7, 232=node_0, 233=node_1, 234=node_2, 235=node_3, 236=node_4, 237=node_5, 238=node_6, 239=node_7, 240=node_0, 241=node_1, 242=node_2, 243=node_3, 244=node_4, 245=node_5, 246=node_6, 247=node_7, 248=node_0, 249=node_1, 250=node_2, 251=node_3, 252=node_4, 253=node_5, 254=node_6, 255=node_7}
node_2
==========删除 node_2 之后: ================
删除节点虚拟节点: [2 = node_2]
删除节点虚拟节点: [10 = node_2]
删除节点虚拟节点: [18 = node_2]
删除节点虚拟节点: [26 = node_2]
删除节点虚拟节点: [34 = node_2]
删除节点虚拟节点: [42 = node_2]
删除节点虚拟节点: [50 = node_2]
删除节点虚拟节点: [58 = node_2]
删除节点虚拟节点: [66 = node_2]
删除节点虚拟节点: [74 = node_2]
删除节点虚拟节点: [82 = node_2]
删除节点虚拟节点: [90 = node_2]
删除节点虚拟节点: [98 = node_2]
删除节点虚拟节点: [106 = node_2]
删除节点虚拟节点: [114 = node_2]
删除节点虚拟节点: [122 = node_2]
删除节点虚拟节点: [130 = node_2]
删除节点虚拟节点: [138 = node_2]
删除节点虚拟节点: [146 = node_2]
删除节点虚拟节点: [154 = node_2]
删除节点虚拟节点: [162 = node_2]
删除节点虚拟节点: [170 = node_2]
删除节点虚拟节点: [178 = node_2]
删除节点虚拟节点: [186 = node_2]
删除节点虚拟节点: [194 = node_2]
删除节点虚拟节点: [202 = node_2]
删除节点虚拟节点: [210 = node_2]
删除节点虚拟节点: [218 = node_2]
删除节点虚拟节点: [226 = node_2]
删除节点虚拟节点: [234 = node_2]
删除节点虚拟节点: [242 = node_2]
删除节点虚拟节点: [250 = node_2]
===============删除之后的虚拟节点map: ===========
{0=node_0, 1=node_1, 3=node_3, 4=node_4, 5=node_5, 6=node_6, 7=node_7, 8=node_0, 9=node_1, 11=node_3, 12=node_4, 13=node_5, 14=node_6, 15=node_7, 16=node_0, 17=node_1, 19=node_3, 20=node_4, 21=node_5, 22=node_6, 23=node_7, 24=node_0, 25=node_1, 27=node_3, 28=node_4, 29=node_5, 30=node_6, 31=node_7, 32=node_0, 33=node_1, 35=node_3, 36=node_4, 37=node_5, 38=node_6, 39=node_7, 40=node_0, 41=node_1, 43=node_3, 44=node_4, 45=node_5, 46=node_6, 47=node_7, 48=node_0, 49=node_1, 51=node_3, 52=node_4, 53=node_5, 54=node_6, 55=node_7, 56=node_0, 57=node_1, 59=node_3, 60=node_4, 61=node_5, 62=node_6, 63=node_7, 64=node_0, 65=node_1, 67=node_3, 68=node_4, 69=node_5, 70=node_6, 71=node_7, 72=node_0, 73=node_1, 75=node_3, 76=node_4, 77=node_5, 78=node_6, 79=node_7, 80=node_0, 81=node_1, 83=node_3, 84=node_4, 85=node_5, 86=node_6, 87=node_7, 88=node_0, 89=node_1, 91=node_3, 92=node_4, 93=node_5, 94=node_6, 95=node_7, 96=node_0, 97=node_1, 99=node_3, 100=node_4, 101=node_5, 102=node_6, 103=node_7, 104=node_0, 105=node_1, 107=node_3, 108=node_4, 109=node_5, 110=node_6, 111=node_7, 112=node_0, 113=node_1, 115=node_3, 116=node_4, 117=node_5, 118=node_6, 119=node_7, 120=node_0, 121=node_1, 123=node_3, 124=node_4, 125=node_5, 126=node_6, 127=node_7, 128=node_0, 129=node_1, 131=node_3, 132=node_4, 133=node_5, 134=node_6, 135=node_7, 136=node_0, 137=node_1, 139=node_3, 140=node_4, 141=node_5, 142=node_6, 143=node_7, 144=node_0, 145=node_1, 147=node_3, 148=node_4, 149=node_5, 150=node_6, 151=node_7, 152=node_0, 153=node_1, 155=node_3, 156=node_4, 157=node_5, 158=node_6, 159=node_7, 160=node_0, 161=node_1, 163=node_3, 164=node_4, 165=node_5, 166=node_6, 167=node_7, 168=node_0, 169=node_1, 171=node_3, 172=node_4, 173=node_5, 174=node_6, 175=node_7, 176=node_0, 177=node_1, 179=node_3, 180=node_4, 181=node_5, 182=node_6, 183=node_7, 184=node_0, 185=node_1, 187=node_3, 188=node_4, 189=node_5, 190=node_6, 191=node_7, 192=node_0, 193=node_1, 195=node_3, 196=node_4, 197=node_5, 198=node_6, 199=node_7, 200=node_0, 201=node_1, 203=node_3, 204=node_4, 205=node_5, 206=node_6, 207=node_7, 208=node_0, 209=node_1, 211=node_3, 212=node_4, 213=node_5, 214=node_6, 215=node_7, 216=node_0, 217=node_1, 219=node_3, 220=node_4, 221=node_5, 222=node_6, 223=node_7, 224=node_0, 225=node_1, 227=node_3, 228=node_4, 229=node_5, 230=node_6, 231=node_7, 232=node_0, 233=node_1, 235=node_3, 236=node_4, 237=node_5, 238=node_6, 239=node_7, 240=node_0, 241=node_1, 243=node_3, 244=node_4, 245=node_5, 246=node_6, 247=node_7, 248=node_0, 249=node_1, 251=node_3, 252=node_4, 253=node_5, 254=node_6, 255=node_7}
==============删除之后, 获取节点的真正node节点对应者: 
node_3

六、一致性哈希应用案例

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值