文学起点网
当前位置: 首页 文学百科

分布式一致性hash算法原理详解(分布式数据缓存中的一致性哈希算法)

时间:2023-06-04 作者: 小编 阅读量: 13 栏目名: 文学百科

它的分布式实现依赖于客户端的程序库,这也是Memcached的一大特点。一致性哈希算法能尽可能减少了服务器数量变化所导致的缓存迁移。哈希算法首先,一致性哈希算法依赖于普通的哈希算法。是计算机广泛使用的杂凑算法之一,主流编程语言普遍已有MD5实现。常见的文件完整性校验就是使用MD5。现实情况下,服务器在一致性哈希环上的位置不可能分布的这么均匀,导致了每个节点实际占据环上的区间大小不一。

一致性哈希算法在分布式缓存领域的 Memcached,负载均衡领域的 Nginx 以及各类 RPC 框架中都有广泛的应用,它主要是为了解决传统哈希函数添加哈希表槽位数后要将关键字重新映射的问题。

本文会介绍一致性哈希算法的原理及其实现,并给出其不同哈希函数实现的性能数据对比,探讨Redis 集群的数据分片实现等,文末会给出实现的具体 github 地址。

Memcached 与客户端分布式缓存

Memcached 是一个高性能的分布式缓存系统,然而服务端没有分布式功能,各个服务器不会相互通信。它的分布式实现依赖于客户端的程序库,这也是 Memcached 的一大特点。比如第三方的 spymemcached 客户端就基于一致性哈希算法实现了其分布式缓存的功能。

其具体步骤如下:

  • 向 Memcached 添加数据,首先客户端的算法根据 key 值计算出该 key 对应的服务器。
  • 服务器选定后,保存缓存数据。
  • 获取数据时,对于相同的 key ,客户端的算法可以定位到相同的服务器,从而获取数据。

在这个过程中,客户端的算法首先要保证缓存的数据尽量均匀地分布在各个服务器上,其次是当个别服务器下线或者上线时,会出现数据迁移,应该尽量减少需要迁移的数据量。

客户端算法是客户端分布式缓存性能优劣的关键。

普通的哈希表算法一般都是计算出哈希值后,通过取余操作将 key 值映射到不同的服务器上,但是当服务器数量发生变化时,取余操作的除数发生变化,所有 key 所映射的服务器几乎都会改变,这对分布式缓存系统来说是不可以接收的。

一致性哈希算法能尽可能减少了服务器数量变化所导致的缓存迁移。

哈希算法

首先,一致性哈希算法依赖于普通的哈希算法。大多数同学对哈希算法的理解可能都停留在 JDK 的 hashCode 函数上。其实哈希算法有很多种实现,它们在不同方面都各有优劣,针对不同的场景可以使用不同的哈希算法实现。

下面,我们会介绍一下几款比较常见的哈希算法,并且了解一下它们在分布均匀程度,哈希碰撞概率和性能等方面的优劣。

MD5 算法:全称为 Message-Digest Algorithm 5,用于确保信息传输完整一致。是计算机广泛使用的杂凑算法之一,主流编程语言普遍已有 MD5 实现。MD5 的作用是把大容量信息压缩成一种保密的格式(就是把一个任意长度的字节串变换成定长的16进制数字串)。常见的文件完整性校验就是使用 MD5。

CRC 算法:全称为 CyclicRedundancyCheck,中文名称为循环冗余校验。它是一类重要的,编码和解码方法简单,检错和纠错能力强的哈希算法,在通信领域广泛地用于实现差错控制。

MurmurHash 算法:高运算性能,低碰撞率,由 Austin Appleby 创建于 2008 年,现已应用到 Hadoop、libstdc、nginx、libmemcached 等开源系统。Java 界中 Redis,Memcached,Cassandra,HBase,Lucene和Guava 都在使用它。

FNV 算法:全称为 Fowler-Noll-Vo 算法,是以三位发明人 Glenn Fowler,Landon Curt Noll,Phong Vo 的名字来命名的,最早在 1991 年提出。 FNV 能快速 hash 大量数据并保持较小的冲突率,它的高度分散使它适用于 hash 一些非常相近的字符串,比如 URL,hostname,文件名,text 和 IP 地址等。

Ketama 算法:一致性哈希算法的实现之一,其他的哈希算法有通用的一致性哈希算法实现,只不过是替换了哈希映射函数而已,但 Ketama 是一整套的流程,我们将在后面介绍。

一致性哈希算法

下面,我们以分布式缓存场景为例,分析一下一致性哈希算法环的原理。

首先将缓存服务器( ip端口号)进行哈希,映射成环上的一个节点,计算出缓存数据 key 值的 hash key,同样映射到环上,并顺时针选取最近的一个服务器节点作为该缓存应该存储的服务器。具体实现见后续的章节。

比如说,当存在 A,B,C,D 四个缓存服务器时,它们及其 key 值为1的缓存数据在一致性哈希环上的位置如下图所示,根据顺时针取最近一个服务器节点的规则,该缓存数据应该存储在服务器 B 上。

当要存储一个 key 值为4的缓存数据时,它在一致性哈希环上的位置如下所示,所以它应该存储在服务器 C 上。

类似的,key 值为5,6的数据应该存在服务 D 上,key 值为7,8的数据应该存储在服务 A 上。

此时,服务器 B 宕机下线,服务器 B 中存储的缓存数据要进行迁移,但由于一致性哈希环的存在,只需要迁移key 值为1的数据,其他的数据的存储服务器不会发生变化。这也是一致性哈希算法比取余映射算法出色的地方。

由于服务器 B 下线,key 值为1的数据顺时针最近的服务器是 C ,所以数据存迁移到服务器 C 上。

现实情况下,服务器在一致性哈希环上的位置不可能分布的这么均匀,导致了每个节点实际占据环上的区间大小不一。

这种情况下,可以增加虚节点来解决。通过增加虚节点,使得每个节点在环上所“管辖”的区域更加均匀。这样就既保证了在节点变化时,尽可能小的影响数据分布的变化,而同时又保证了数据分布的均匀。

具体实现

下面我们实现 Memcached 分布式缓存场景下的一致性哈希算法,并给出具体的测试性能数据。该实现借鉴了 kiritomoe 博文中的实现和 spymemcached 客户端代码。具体实现请看我的github,地址为 https://github.com/ztelur/consistent-hash-algorithm。

NodeLocator 是分布式缓存场景下一致性哈希算法的抽象,它有一个 getPrimary 函数,接收一个缓存数据的 key 值,输出存储该缓存数据的服务器实例。

public interface NodeLocator { MemcachedNode getPrimary(String k);}

下面是通用的一致性哈希算法的实现,它使用 TreeMap 作为一致性哈希环的数据结构,其 ceilingEntry 函数可以获取环上最近的一个节点。buildConsistentHashRing 函数中包含了构建一致性哈希环的过程,默认加入了 12 个虚拟节点。

public class ConsistentHashNodeLocator implements NodeLocator { private final static int VIRTUAL_NODE_SIZE = 12; private final static String VIRTUAL_NODE_SUFFIX = "-"; private volatile TreeMap<Long, MemcachedNode> hashRing; private final HashAlgorithm hashAlg; public ConsistentHashNodeLocator(List<MemcachedNode> nodes, HashAlgorithm hashAlg) { this.hashAlg = hashAlg; this.hashRing = buildConsistentHashRing(hashAlg, nodes); } @Override public MemcachedNode getPrimary(String k) { long hash = hashAlg.hash(k); return getNodeForKey(hashRing, hash); } private MemcachedNode getNodeForKey(TreeMap<Long, MemcachedNode> hashRing, long hash) { /* 向右找到第一个key */ Map.Entry<Long, MemcachedNode> locatedNode = hashRing.ceilingEntry(hash); /* 想象成为一个环,超出尾部取出第一个 */ if (locatedNode == null) { locatedNode = hashRing.firstEntry(); } return locatedNode.getValue(); } private TreeMap<Long, MemcachedNode> buildConsistentHashRing(HashAlgorithm hashAlgorithm, List<MemcachedNode> nodes) { TreeMap<Long, MemcachedNode> virtualNodeRing = new TreeMap<>(); for (MemcachedNode node : nodes) { for (int i = 0; i < VIRTUAL_NODE_SIZE; i) { // 新增虚拟节点的方式如果有影响,也可以抽象出一个由物理节点扩展虚拟节点的类 virtualNodeRing.put(hashAlgorithm.hash(node.getSocketAddress().toString()VIRTUAL_NODE_SUFFIXi), node); } } return virtualNodeRing; }}

在 getPrimary 函数中,首先使用 HashAlgorithm 计算出 key 值对应的哈希值,然后调用 getNodeForKey 函数从 TreeMap 中获取对应的最近的服务器节点实例。

HashAlgorithm 是对哈希算法的抽象,一致性哈希算法可以使用各种普通的哈希算法,比如说 CRC ,MurmurHash 和 FNV 等。下面,我们将会对比各种哈希算法给该实现带来的性能差异性。

性能测试

测试数据是评价一个算法好坏的最为真实有效的方法,量化的思维模式一定要有,这也是程序员进阶的法宝之一。我们以下面四个量化的指标对基于不同哈希函数的一致性哈希算法进行评测。

  • 统计每个服务器节点存储的缓存数量,计算方差和标准差。测量缓存分布均匀情况,我们可以模拟 50000个缓存数据,分配到100 个服务器,测试最后个节点存储缓存数据量的方差和标准差。
  • 随机下线10%的服务器,重新分配缓存,统计缓存迁移比率。测量节点上下线的情况,我们可以模拟 50000 个缓存数据,分配到100 个指定服务器,之后随机下线 10 个服务器并重新分配这50000个数据,统计缓存分配到不同服务器的比例,也就是迁移比率。
  • 使用JMH对不同哈希算法的执行效率进行对比。

具体评测算法如下。

public class NodeLocatorTest { /** * 测试分布的离散情况 */ @Test public void testDistribution() { List<MemcachedNode> servers = new ArrayList<>(); for (String ip : ips) { servers.add(new MemcachedNode(new InetSocketAddress(ip, 8080))); } // 使用不同的DefaultHashAlgorithm进行测试,得出不同的数据 NodeLocator nodeLocator = new ConsistentHashNodeLocator(servers, DefaultHashAlgorithm.NATIVE_HASH); // 构造 50000 随机请求 List<String> keys = new ArrayList<>(); for (int i = 0; i < 50000; i) { keys.add(UUID.randomUUID().toString()); } // 统计分布 AtomicLongMap<MemcachedNode> atomicLongMap = AtomicLongMap.create(); for (MemcachedNode server : servers) { atomicLongMap.put(server, 0); } for (String key : keys) { MemcachedNode node = nodeLocator.getPrimary(key); atomicLongMap.getAndIncrement(node); } System.out.println(StatisticsUtil.variance(atomicLongMap.asMap().values().toArray(new Long[]{}))); System.out.println(StatisticsUtil.standardDeviation(atomicLongMap.asMap().values().toArray(new Long[]{}))); } /** * 测试节点新增删除后的变化程度 */ @Test public void testNodeAddAndRemove() { List<MemcachedNode> servers = new ArrayList<>(); for (String ip : ips) { servers.add(new MemcachedNode(new InetSocketAddress(ip, 8080))); } //随机下线10个服务器, 先shuffle,然后选择0到90,简单模仿随机计算。 Collections.shuffle(servers); List<MemcachedNode> serverChanged = servers.subList(0, 90); NodeLocator loadBalance = new ConsistentHashNodeLocator(servers, DefaultHashAlgorithm.NATIVE_HASH); NodeLocator changedLoadBalance = new ConsistentHashNodeLocator(serverChanged, DefaultHashAlgorithm.NATIVE_HASH); // 构造 50000 随机请求 List<String> keys = new ArrayList<>(); for (int i = 0; i < 50000; i) { keys.add(UUID.randomUUID().toString()); } int count = 0; for (String invocation : keys) { MemcachedNode origin = loadBalance.getPrimary(invocation); MemcachedNode changed = changedLoadBalance.getPrimary(invocation); // 统计发生变化的数值 if (!origin.getSocketAddress().equals(changed.getSocketAddress())) count; } System.out.println(count / 50000D); } static String[] ips = {...};}

JMH的测试脚本如下所示。

@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.MICROSECONDS)@State(Scope.Thread)public class JMHBenchmark { private NodeLocator nodeLocator; private List<String> keys; @Benchmark public void test() { for (String key : keys) { MemcachedNode node = nodeLocator.getPrimary(key); } } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(JMHBenchmark.class.getSimpleName()) .forks(1) .warmupIterations(5) .measurementIterations(5) .build(); new Runner(opt).run(); } @Setup public void prepare() { List<MemcachedNode> servers = new ArrayList<>(); for (String ip : ips) { servers.add(new MemcachedNode(new InetSocketAddress(ip, 8080))); } nodeLocator = new ConsistentHashNodeLocator(servers, DefaultHashAlgorithm.MURMUR_HASH); // 构造 50000 随机请求 keys = new ArrayList<>(); for (int i = 0; i < 50000; i) { keys.add(UUID.randomUUID().toString()); } } @TearDown public void shutdown() { } static String[] ips = {...};}

分别测试了 JDK 哈希算法,FNV132 算法,CRC 算法,MurmurHash 算法和Ketama 算法,分别对应 DefaultHashAlgorithm 的 NATIVE_HASH,FNV1_32_HASH,CRC_HASH,MURMUR_HASH 和 KETAMA_HASH 。具体数据如下所示。

虚拟槽分区

有些文章说,Redis 集群并没有使用一致性哈希算法,而是使用虚拟槽分区算法。但是外网(地址见文末)上都说 Redis 使用的虚拟槽分区只是一致性哈希算法的变种,虚拟槽可以允许 Redis 动态扩容。

或许只有去了解一下Redis的源码才能对这个问题作出准确的回答。请了解的同学积极留言解答,谢谢。

个人remcarpediem,程序员历小冰

个人博客地址: http://remcarpediem.net/

github 地址: https://github.com/ztelur/consistent-hash-algorithm

redis分布式讨论的地址: https://www.reddit.com/r/redis/comments/4yztxi/which_one_is_better_hash_slot_or_consistent/

参考

  • https://jistol.github.io/software engineering/2018/07/07/consistent-hashing-sample/
  • https://mp.weixin.qq.com/s/oe3EPu5DxB0bWheBImMsHg
    推荐阅读
  • 苏州旅游攻略景点必去(苏州旅游必去景点有哪些)

    位于苏州市东北街一百七十八号,始建于明朝正德年间。虎丘是AAAAA级景区及全国文明单位,首批十佳文明风景旅游区示范点。中午,周庄最为欢闹,游人穿梭熙熙攘攘,船儿来回摇摇荡荡,各地的游客与热情的商铺融为一体,热闹非凡,安静的古镇着实多了些欢闹的气息。狮子林为苏州四大名园之一,位于苏州市市城东北园林路。

  • 买的玉米种子是瘪的(去年买的玉米种子剩了很多)

    去年买的陈玉米种子建议不要用针对去年的陈玉米种子,大多情况下不建议再次使用,会影响到玉米后期的生长和产量情况。陈年的因为保管的问题,可能会出现很多因素影响玉米的出苗率或者后期的生长。陈玉米种子隔了一年后再种植,种子自身水分含量降低,水分降低严重的情况下,影响播种的效率和玉米的后期生长,由于活性降低,即使能出芽,也不一定能出苗。

  • 173.2亿!国庆消费火爆 国庆消费市场

    今年国庆、重阳两节叠加,全省消费市场呈现平衡较快增长态势,服装、家电、汽车等商品消费亮点突出,大众餐饮、旅行休闲、文体娱乐等主要服务消费备受青睐。根据商务部业务统一平台生存必需品监测系统显示,国庆黄金周期间,全省生存必需品市场供应充沛,价格总体平衡。除了买买买,国庆还是婚庆、团圆、会友高峰,各地亲友聚餐、婚寿宴等大众化餐饮生意兴隆。

  • 吴承恩是怎么写出的西游记(吴承恩怎么写出的西游记)

    吴承恩怎么写出的西游记诸葛长青:吴承恩写西游记诸葛长青:吴承恩怎么写出的《西游记》西游记,广泛流传西游记,作者吴承恩西游记,包含了儒释道大智慧那么,吴承恩是怎么写出的《西游记》呢?诸葛长青把自己对吴承恩写《西游记》,研究成。

  • 李逵扮演者(大家一起来看看吧)

    我们一起去了解并探讨一下这个问题吧!李逵扮演者赵小锐的李逵应该算是很多人印象当中的经典所在了,他的李逵也是很粗犷,但是这种粗犷当中却带着细腻,也是因为这个角色,他开始受到了不少的观众的关注和喜爱。其实之前的他也有出演过一些电视剧的,但是可惜的是一直都没能够真正的红起来,是李逵这个角色,让他一夜成名爆火了。

  • 汽车空间大小怎么看轴距(什么因素会影响车内空间)

    大众速腾,长度4655mm,轴距2651mm。看外观就明白了,因为宝马320i是后驱车,发动机采用纵置布局;而大众速腾是前驱车,发动机采用横置布局。而且由于发动机纵置,后驱设计,对于车内空间侵占较为严重,所以宝马320的长轴距实际上对于空间的帮助是“虚高”的。前面我们就提到了,宝马3系采用了后驱,大众速腾采用了前驱。回到我们的主题,通常来说,麦弗逊与扭力梁对于车辆空间的侵占是最小的,而多连杆和双叉臂对于车辆空间侵占是要更大的。

  • 湖南省医保局2015年工作思路与安排 湖南省医疗保障局领导班子组成人员

    督促指导各统筹地区核实提高缴费基数,强化保险费足额征收。继续加强工伤认定参与,把好工伤入口关。认真核实、积极处理群众举报问题,始终保持高压态势。加强生育医疗服务管理,规范生育津贴发放。二是启动实施工伤保险信息系统改造升级,改进工伤职工异地就医联网结算,方便工伤职工救治。三是加强财务、业务数据清理,提高数据质量;通报全省“三险”基金运行分析,指导市州加强基金运行风险管控。

  • 民国最渣四大渣男(民国著名4大渣男)

    当时很多文人在接受自由恋爱的思想时,家中已经有了父母为之安排的妻子。郁达夫一生有过三位妻子,一位同居情人。郁达夫后来还是和王映霞离婚,1940年在新加坡认识了比他小20岁的播音员李莜英,两人很快就同居了。第二任妻子佐藤富子,是个日本女人,为了和郭沫若在一起,不仅改名为“郭安娜”,还和父母断绝了关系。1937年,郭沫若抛弃妻子回国,和女明星于立群同居,两人于2年后再重庆结婚。

  • 电脑怎么连打印机教程(教会你快速学会电脑如何连接打印机的安装使用方法)

    最近很多网友都在私信给小编,小编也无法一一回复,有些问题也无法简约介绍,所以只能在头条文章内与大家共享。

  • 爱吃鸡蛋的注意了这3种鸡蛋不能买(这些鸡蛋没你想的那么好)

    营养均衡的孩子没必要补这种元素;真正缺乏硒,靠富硒蛋补,根本起不了多大作用。这类蛋再好,也别给孩子吃那就是全生或半熟的蛋,比如溏心蛋。一般溏心蛋的加热时间短,不能完全杀死细菌,生蛋液根本没有处理细菌,对于抵抗力低、易感染的宝宝来说,非常容易被细菌感染。