计算机系统应用教程网站

网站首页 > 技术文章 正文

一致性hash算法

btikc 2024-09-11 01:55:41 技术文章 54 ℃ 0 评论


一、简介

一致性hash算法是1997年由麻省理工学院提出的,用于在分布式环境中,当增加或删除结点时,尽可能小的减少对结点映射和请求映射的改变,进而减少其带来的存储改变、请求处理改变的影响。通常用于分布式环境中缓存分布、负载均衡等场景。

二、传统hash算法的问题

在缓存场景,以结点取模为例,假设有5个缓存结点,即数据存入及读取分布这5个结点上,请求hash值为x,那么请求对应的结点为:x%5=(0,1,2,3,4)。

当新增1个结点 此时结点个数变为6,那么请求hash值对应的结点变为x%6=(0,1,2,3,4,5),以前hash值为5对应结点0,现在是hash值为6才对应结点0,其它hash值对应的结点类似,对应的相关操作如数据存入及读取,都会发生改变,结点上的数据也随着都会发生改变。影响太大。

删除1结点 同上新增结点带来的问题类似。

三、一致性hash算法特点

一致性hash算法拥有均衡性(balance)、单调性(monotonicity)、分散性(spread)、负载(load)这些特点。最终表现,即是当有结点加入或删除时,除少数结点外,大多数结点的映射关系不会改变,且新增结点时,请求要么到新结点,要么到原结点,而不会到其它旧结点,删除时,请求到其它结点,只是删除结点对应的原请求映射关系改变,其它结点对应的请求映射关系不会改变 。

均衡性(balance) 均衡性是指hash结果尽可能均衡分散在所有结点中。

单调性(monotonicity) 单调性是指某hash值一旦映射到某结点,当有新结点加入时,该hash值要么映射到原结点,要么映射到新加的结点,而不会映射到其它旧结点。

分散性(spread) 分散性是指分布式场景中,不同终端可能见不到所有所有输出结点,进而相同hash值映射的结点可能不同。这需要避免。

负载(load) 负载是从另一角度看分散性,指同一输出结点,不能被不同终端存储不同的内容。

四、一致性hash算法设计

一致性hash算法设计,是将请求和输出结点都映射到抽象的hash环上,结点查找时,请求hash值在抽象hash环上,按顺时针查找结点,遇到的第一个结点即为输出结点。

hash环 这是抽象的环,请求和结点按指的hash算法,都映射到hash环上。

虚拟结点 为了更均衡地将请求映射到所有结点,可以将结点虚拟为多个虚拟结点,这样各个结点在hash环上分布的更均衡,相应的处理请求也更均衡。

五、一致性hash算法的java实现

这里使用java来实现一致性hash算法,使用TreeMap作为hash环的数据结构,使用MurmurHash作为hash算法(借助guava27.1-jre的jar包),具体请参考代码注释。

import com.google.common.collect.Lists;

import com.google.common.hash.HashCode;

import com.google.common.hash.HashFunction;

import com.google.common.hash.Hashing;

import java.nio.charset.Charset;

import java.util.List;

import java.util.SortedMap;

import java.util.TreeMap;

import java.util.function.Consumer;

import java.util.function.Function;

import java.util.stream.Collectors;

import java.util.stream.Stream;

public class ConsistentHashAlgMain {

public static void main(String[] args) {

/ 环境模拟 /

//模拟物理机器结点

List<String> servers = Lists.newArrayList("192.168.0.1", "192.168.0.2", "192.168.0.3", "192.168.0.4", "192.168.0.5");

//模拟请求

List<String> reqs = Lists.newArrayList("request one", "request two", "request three", "request four", "request five");

//生成抽象的hash环(使用TreeMap作为hash环的数据结构)

TreeMap<String, String> serverRing = new TreeMap<>();

//指定hash算法为murmur

HashFunction hf = Hashing.murmur3_128();

/ 辅助方法 /

//生成hash值方法

Function<String, HashCode> hash = t -> hf.hashString(t, Charset.forName("utf8"));

//生成虚拟结点后缀列表的方法

List<Integer> virNum = Stream.iterate(1, n -> n + 1).limit(100).collect(Collectors.toList());

//单台物理机器生成虚拟机器结点列表的方法

Function<String, List<String>> buildVirServer = s -> virNum.stream().map(t -> s + "#" + t).collect(Collectors.toList());

/ 增加、删除机器方法 /

//添加一台物理机器方法

Consumer<String> addServer = t -> {

//获取物理机对应的虚拟机器列表

List<String> virServers = buildVirServer.apply(t);

//将虚拟机器列表加入到hash环中

virServers.forEach(v -> {

//获取虚拟机器的hash值

HashCode h = hash.apply(v);

//将虚拟机器加入到hash环中

serverRing.put(h.toString(), t);

});

};

//删除一台物理机器方法

Consumer<String> delServer = t -> {

//获取物理机对应的虚拟机器列表

List<String> virServers = buildVirServer.apply(t);

//将虚拟机器列表从hash环中删除

virServers.forEach(v -> {

//获取虚拟机器的hash值

HashCode h = hash.apply(v);

//将虚拟机器从hash环中删除

serverRing.remove(h.toString());

});

};

//初始化物理机器(每台物理机器会先生成多台虚拟机器列表)到hash环方法

Consumer<List<String>> initServerRing = list -> {

list.forEach(t -> addServer.accept(t));

};

/ 请求处理方法 /

//处理请求方法

Function<String, String> ringHashHost = t -> {

//获取请求的hash值

HashCode h = hash.apply(t);

//查找请求hash值后对应的剩余hash环机器

SortedMap<String, String> tailRing = serverRing.tailMap(h.toString());

//获取请求hash值后找到的第一台机器的key

String idx = tailRing.size() < 1 ? serverRing.firstKey() : tailRing.firstKey();

//获取请求hash值后找到的第一台机器

String s = serverRing.get(idx);

System.out.println(t + " : " + s);

return s;

};

//批量处理请求方法

Consumer<List<String>> handleReqs = list -> {

list.forEach(t -> ringHashHost.apply(t));

};

/ 测试 /

//初始化hash环

initServerRing.accept(servers);

//处理请求

handleReqs.accept(reqs);

//添加新机器并处理请求

System.out.println("###### add server #####");

addServer.accept("192.168.0.6");

handleReqs.accept(reqs);

//删除机器并处理请求

System.out.println("###### del server #####");

delServer.accept("192.168.0.4");

handleReqs.accept(reqs);

}

}

结果输出:

request one : 192.168.0.5

request two : 192.168.0.5

request three : 192.168.0.3

request four : 192.168.0.4

request five : 192.168.0.1

\###### add server #####

request one : 192.168.0.5

request two : 192.168.0.5

request three : 192.168.0.6

request four : 192.168.0.4

request five : 192.168.0.1

\###### del server #####

request one : 192.168.0.5

request two : 192.168.0.5

request three : 192.168.0.6

request four : 192.168.0.1

request five : 192.168.0.1

分析结果可以发现,新增结点时,旧的映射要么不改变,要么映射到新结点,删除结点时,除少数结点外,大多数的映射关系也不会改变。

六、一致性hash算法的简单实现

另外也可以借助google的guava包提供的一致性hash算法的基本实现,直接使用,其关键使用代码(在此不详解)如下:

//所有结点

List<String> servers = Lists.newArrayList("192.168.0.1", "192.168.0.2", "192.168.0.3", "192.168.0.4", "192.168.0.5");

//请求映射到结点

int idx = Hashing.consistentHash(Hashing.sha256().hashString("request one", Charset.forName("utf-8")), servers.size());

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表