前段时间在了解分布式,发现firefoxbug在博客中写的这篇《一致性hash在分布式系统中的应用》对这个问题说明得比较清晰易懂,本文主要是自己的理解和实践。

在后端一般会遇到这样的场景:随着应用系统的访问量或者DB/文件存储系统的数据量增大,系统由于负载增大而出现响应延迟甚至down掉的情况。为了解决这个问题,往往会对系统采用垂直扩展和水平扩展的架构设计,而分布式系统正是水平扩展架构的一种应用实践。

1 分布式系统要求

分布式设计的初衷就是为了解决单一服务端负载过大的问题,所以在对系统做水平扩展后,数据要尽量均匀地分布在每台服务器节点的上(即不会出现热点数据节点)。其次,如果后期需要扩容或者某一节点发生故障需要从集群中剔除,那么处理后的分布式系统应该做到对已存储的数据影响最小,降低数据迁移的成本风险

2 解决方法

由于机器的数量不可能是无限的,所以水平扩展的时候,要考虑把无限的数据通过一定的算法平衡、有序、易扩展地分布在这些机器上。

常见的做法是利用把要处理的数据进行编号,然后对机器的数据进行取模运算。例如,假设有10个数据(编号为0~9),机器数量为3(编号为0~2),那么每个数据编号对机器数3取模后,0号机器存放了编号为0,3,6,9的数据;1号机器存了编号为1,4,7的数据;2号机器存放了编号为2,5,8的数据。

取模算法比较简单,但是当某个服务器节点出现故障或者新增节点后,需要对已有数据作大量的迁移。在memcached分布式原理中介绍了Consistent Hashing算法,它能较好地解决这个问题。

3 一致性哈希算法原理

如上图所示,memcached分布式提供的哈希算法的主要处理流程如下:

1、使用算法求出每个memcached服务器节点(ip地址)的哈希值x,并将其分配到0~2^32的圆上(值域);
2、用同样的方法求出存储数据键的哈希值y,并映射到圆上。
3、按顺时针方向查找第1个比y大的x,那么y就分布在x前面那个节点上。

4 示例程序

在firefoxbug的原文中提供了python2的示例程序,这里改成了python3。注意,程序中对这4台机器都使用了虚拟节点(replicas),它可以增加数据分布的均匀性。

# -*- coding: UTF-8 -*-'''
FileName:      consistenthashdistributed1.sh
Description:   分布式系统:一致性hash算法的应用
Simple Usage:  python consistenthashdistributed1.py [numbers of replicate]
Reference:     http://www.firefoxbug.com/index.php/archives/2791/
(c) 2018.02.17 vfhky https://typecodes.com/python/consistenthashdistributed1.html
'''import sys
import hashlibCONTENT = """Consistent hashing is a special kind of hashing such that when a hash table is resized and consistent hashing is used, only K/n keys need to be remapped on average, where K is the number of keys, and n is the number of slots. In contrast, in most traditional hash tables, a change in the number of array slots causes nearly all keys to be remapped."""# 所有机器列表
SERVERS = ["192.168.1.1","192.168.2.2","192.168.3.3","192.168.4.4"
]class HashRing(object):"""Constructs."""def __init__(self, nodes=None, replicas=3):"""Manages a hash ring.`nodes` is a list of objects that have a proper __str__ representation.`replicas` indicates how many virtual points should be used pr. node,replicas are required to improve the distribution."""self.replicas = replicasself.ring = dict()self._sorted_keys = []if nodes:for node in nodes:self.add_node(node)def add_node(self, node):"""Adds a `node` to the hash ring (including a number of replicas)."""for i in range(0, self.replicas):key = self.gen_key('%s:%s' % (node, i))self.ring[key] = node# print("key=[%s]=[%s]." %(key, node))self._sorted_keys.append(key)self._sorted_keys.sort()#print("%s" %(self._sorted_keys))def remove_node(self, node):"""Removes `node` from the hash ring and its replicas."""for i in range(0, self.replicas):key = self.gen_key('%s:%s' % (node, i))del self.ring[key]self._sorted_keys.remove(key)def get_node(self, string_key):"""Given a string key a corresponding node in the hash ring is returned.If the hash ring is empty, `None` is returned."""return self.get_node_pos(string_key)[0]def get_node_pos(self, string_key):"""Given a string key a corresponding node in the hash ring is returnedalong with it's position in the ring.If the hash ring is empty, (`None`, `None`) is returned."""if not self.ring:return None, Nonekey = self.gen_key(string_key)nodes = self._sorted_keysnodes_num = len(nodes)for i in range(0, nodes_num):node = nodes[i]if key <= node:return self.ring[node], i# 对于key>node节点key的,全部落在第1个key对应的节点(192.168.1.4)上,这样就形成了1个闭环。print("[%s:%s] string_key=[%s] key=[%s] node=[%s] self.ring[nodes[0]]=[%s].\n" %(__file__, sys._getframe().f_lineno, string_key, key, node, self.ring[nodes[0]]))return self.ring[nodes[0]], 0def gen_key(self, key):"""Given a string key it returns a long value,this long value represents a place on the hash ring.md5 is currently used because it mixes well."""m = hashlib.md5()m.update(key.encode('utf-8'))return m.hexdigest()def consistent_hash(replicas):'''docstring'''# 模拟初始化每天机器的dbdatabase = {}for s in SERVERS:database[s] = []hr = HashRing(SERVERS,replicas)for w in CONTENT.split():database[hr.get_node(w)].append(w)# 打印所有的节点下面的数据for node in database:print("[%s]=[%s].\n" %(node, database[node]))if __name__ == '__main__':'''docstring'''replicas = 3if len(sys.argv) > 1:replicas = long(sys.argv[1])if( replicas < 3 or replicas > 100000 ):print( "Rreplicas should lower than 100000." )sys.exit()consistent_hash(replicas)

上面程序在查找落地节点时,采用的是遍历整个hash圈上的值,所以虚拟节点不宜过大,否则会出现查找时间过长的问题。如下图所示,BZ在自己的单核1G内存的虚拟机中测试,发现4个节点如果都有10000个虚拟节点时在速度和均衡性方面都是不错的。5 测试

6 参考文章

《Memcached 分布式缓存实现原理》。

原文:https://typecodes.com/python/consistenthashdistributed1.html?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io

一致性hash在分布式系统中的应用

场景

如果要设计一套KV存储的系统,用户PUT一个key和value,存储到系统中,并且提供用户根据key来GET对应的value。要求随着用户规模变大,系统是可以水平扩展的,主要要解决以下几个问题。

  1. 系统是一个集群,包含很多节点,如何解决用户数据的存储问题?保证用户的数据尽可能平均分散到各个节点上。
  2. 如果用户量增长,需要对集群进行扩容,扩容完成后如何解决数据重新分布?保证不会出现热点数据节点。

方案一:取模hash

要设计上面的系统,最简单的方案就是取模hash。基本的原理就是:假设集群一共有N台机器提供服务,对于用户的请求编号,比如编号M,那么就把这个请求通过取模发送到指定机器。

机器序号 = M % N

举个例子,比如有下面这些机器

0. 192.168.1.1
1. 192.168.2.2
2. 192.168.3.3
3. 192.168.4.4

用户PUT 100个请求,此时客户端(可以设计)带上一个编号,分别是1-100,那么

1%4 = 1 <<-->> 192.168.2.2
2%4 = 2 <<-->> 192.168.3.3
3%4 = 3 <<-->> 192.168.4.4
...
100%4 = 0 <<-->> 192.168.1.1

这样就可以很简单把用户的请求负载均衡到4台机器上了,解决了第一个问题。可以看看下面代码实现

content = """Consistent hashing is a special kind of hashing such that when a hash table is resized and consistent hashing is used, only K/n keys need to be remapped on average, where K is the number of keys, and n is the number of slots. In contrast, in most traditional hash tables, a change in the number of array slots causes nearly all keys to be remapped."""### 所有机器列表
servers = ["192.168.1.1","192.168.2.2","192.168.3.3","192.168.4.4"
]class NormalHash(object):"""Normal Hash """def __init__(self, nodes=None):if nodes:self.nodes = nodesself.number = len(nodes)def get_node(self, index):"""Return node by index % servers number"""if index < 0:return Nonereturn self.nodes[index%self.number]def normal_hash():"""Normal hash usage example"""nh = NormalHash(servers)words = content.split()# 模拟初始化每天机器的dbdatabase = {}for s in servers:database[s] = []for i in xrange(len(words)):database[nh.get_node(i)].append(words[i])print database

上面这部分是客户端的代码,NormalHash其实可以是在服务端实现,客户端每次要PUT或者GET一个key,就调用服务端的sdk,获取对应机器,然后操作。

取模hash情况下扩容机器

取模hash有一个明显的缺点,就是上面提出的第二个问题,如何解决扩容机器后数据分布的问题?继续上面的例子,比如这时候要新增一台机器,机器规模变成

0. 192.168.1.1
1. 192.168.2.2
2. 192.168.3.3
3. 192.168.4.4
4. 192.168.5.5

那么问题就来了,如果现在用户要通过GET请求数据,同样还是1-100的请求编号,这时候取模就变成

i % 51%5 = 1 <<-->> 192.168.2.2
2%5 = 2 <<-->> 192.168.3.3
3%5 = 3 <<-->> 192.168.4.4
4%5 = 4 <<-->> 192.168.5.5  ->> 这里开始就变化了
...

很显然,对于新的PUT操作不会有影响,但是对于用户老的数据GET请求, 数据就不一致了,这时候必须要进行移数据,可以推断出,这里的数据变更是很大的,在80%左右。

但是,如果扩容的集群是原来的倍数,之前是N台,现在扩容到 M * N台,那么数据迁移量是50%。

取模hash总结

取模hash能解决负载均衡问题,而且实现很简单,维护meta信息成本也很小,但是扩容集群的时候,最好是按照整数倍扩容,否则数据迁移成本太高。

我个人觉得,取模hash已经能满足业务比较小的场景了,在机器只有几台或者几十台的时候,完全能够应付了。而且这种方案很简洁,实现起来很容易,很容易理解。

方案二:一致性hash

一致性hash基本实现如下图,这张图最早出现在是memcached分布式实现里。如何理解一致性hash呢?

  • 首先我们设计一个环,假设这个环是由2^32 - 1个点组成,也就是说[0, 2^32)上的任意一个点都能在环上找到。
  • 现在采用一个算法(md5就可以),把我们集群中的服务器以ip地址作为key,然后根据算法得到一个值,这个值映射到环上的一个点,然后还有对应的数据存储区间
IP地址          hash     value(例子)           数据范围
192.168.1.1     -->>        1000        -->>  (60000, 1000](可以看环来理解,和时钟一样)
192.168.2.2     -->>        8000        -->>   (1000, 8000]
192.168.3.3     -->>        25000       -->>   (8000, 25000]
192.168.4.4     -->>        60000       -->>   (25000, 60000]
  • 用户的请求过来后,对key进行hash,也映射到环上的一个点,根据ip地址的数据范围存储到对应的节点上,图上粉红色的点就代表数据映射后的环上位置,然后箭头就是代表存储的节点位置

一致性hash情况下扩容机器

一致性hash在某种程度上是可以解决数据的负载均衡问题的,再来看看扩容的情况,这时候新增加一个节点,图

机器情况变成

IP地址          hash     value(例子)           数据范围
192.168.1.1     -->>        1000        -->>  (60000, 1000](注意:取模后的逻辑大小)
192.168.2.2     -->>        8000        -->>   (1000, 8000]
192.168.5.5     -->>       15000        -->>  (8000, 15000] (新增的)
192.168.3.3     -->>        25000       -->>   (15000, 25000]
192.168.4.4     -->>        60000       -->>   (25000, 60000]

这时候被影响的数据范围仅仅是(8000, 15000]的数据,这部分需要做迁移。同样的如果有一台机器宕机,那么受影响的也只是比这台机器对应环上的点大,比下一个节点值小的点。

一致性hash总结

一致性hash能解决热点分布的问题,对于缩容和扩容也能低成本进行。但是一致性hash在小规模集群中,就会有问题,很容易出现数据热点分布不均匀的现象,因为当机器数量比较少的时候,hash出来很有可能各自几点管理的“范围”有大有小。而且一旦规模比较小的情况下,如果数据原本是均匀分布的,这时候新加入一个节点,就会影响数据分布不均匀。

虚拟节点

虚拟节点可以解决一致性hash在节点比较少的情况下的问题,简单而言就是在一个节点实际虚拟出多个节点,对应到环上的值,然后按照顺时针或者逆时针划分区间

下面贴上一致性hash的代码,replicas实现了虚拟节点,当replicas=1的时候,就退化到上面的图,一个节点真实对应到一个环上的点。

# -*- coding: UTF-8 -*-import md5content = """Consistent hashing is a special kind of hashing such that when a hash table is resized and consistent hashing is used, only K/n keys need to be remapped on average, where K is the number of keys, and n is the number of slots. In contrast, in most traditional hash tables, a change in the number of array slots causes nearly all keys to be remapped."""# 所有机器列表
servers = ["192.168.1.1","192.168.2.2","192.168.3.3","192.168.4.4"
]class HashRing(object):def __init__(self, nodes=None, replicas=3):"""Manages a hash ring.`nodes` is a list of objects that have a proper __str__ representation.`replicas` indicates how many virtual points should be used pr. node,replicas are required to improve the distribution."""self.replicas = replicasself.ring = dict()self._sorted_keys = []if nodes:for node in nodes:self.add_node(node)def add_node(self, node):"""Adds a `node` to the hash ring (including a number of replicas)."""for i in xrange(0, self.replicas):key = self.gen_key('%s:%s' % (node, i))self.ring[key] = nodeself._sorted_keys.append(key)self._sorted_keys.sort()def remove_node(self, node):"""Removes `node` from the hash ring and its replicas."""for i in xrange(0, self.replicas):key = self.gen_key('%s:%s' % (node, i))del self.ring[key]self._sorted_keys.remove(key)def get_node(self, string_key):"""Given a string key a corresponding node in the hash ring is returned.If the hash ring is empty, `None` is returned."""return self.get_node_pos(string_key)[0]def get_node_pos(self, string_key):"""Given a string key a corresponding node in the hash ring is returnedalong with it's position in the ring.If the hash ring is empty, (`None`, `None`) is returned."""if not self.ring:return None, Nonekey = self.gen_key(string_key)nodes = self._sorted_keysfor i in xrange(0, len(nodes)):node = nodes[i]if key <= node:return self.ring[node], ireturn self.ring[nodes[0]], 0def get_nodes(self, string_key):"""Given a string key it returns the nodes as a generator that can hold the key.The generator is never ending and iterates through the ringstarting at the correct position."""if not self.ring:yield None, Nonenode, pos = self.get_node_pos(string_key)for key in self._sorted_keys[pos:]:yield self.ring[key]while True:for key in self._sorted_keys:yield self.ring[key]def gen_key(self, key):"""Given a string key it returns a long value,this long value represents a place on the hash ring.md5 is currently used because it mixes well."""m = md5.new()m.update(key)return long(m.hexdigest(), 16)def consistent_hash():# 模拟初始化每天机器的dbdatabase = {}for s in servers:database[s] = []hr = HashRing(servers)for w in words.split():database[hr.get_node(w)].append(w)print databaseconsistent_hash()

标签:hash, distributed

http://www.firefoxbug.com/index.php/archives/2791/

Kotlin 开发者社区

国内第一Kotlin 开发者社区公众号,主要分享、交流 Kotlin 编程语言、Spring Boot、Android、React.js/Node.js、函数式编程、编程思想等相关主题。

越是喧嚣的世界,越需要宁静的思考。

合抱之木,生于毫末;
九层之台,起于垒土;
千里之行,始于足下。
积土成山,风雨兴焉;
积水成渊,蛟龙生焉;
积善成德,而神明自得,圣心备焉。
故不积跬步,无以至千里;
不积小流,无以成江海。
骐骥一跃,不能十步;
驽马十驾,功在不舍。
锲而舍之,朽木不折;
锲而不舍,金石可镂。
蚓无爪牙之利,筋骨之强,上食埃土,下饮黄泉,用心一也。
蟹六跪而二螯,非蛇鳝之穴无可寄托者,用心躁也。

分布式系统:一致性hash算法 在分布式系统中的应用相关推荐

  1. hash oracle 分表_一致性Hash算法在数据库分表中的实践

    最近有一个项目,其中某个功能单表数据在可预估的未来达到了亿级,初步估算在90亿左右.与同事详细讨论后,决定采用一致性Hash算法来完成数据库的自动扩容和数据迁移.整个程序细节由我同事完成,我只是将其理 ...

  2. 5分钟带你理解一致性Hash算法

    转载自 5分钟带你理解一致性Hash算法 一致性Hash算法背景 一致性哈希算法在1997年由麻省理工学院的Karger等人在解决分布式Cache中提出的,设计目标是为了解决因特网中的热点(Hot s ...

  3. OpenStack_Swift源代码分析——Ring基本原理及一致性Hash算法

    1.Ring的基本概念 Ring是swfit中最重要的组件.用于记录存储对象与物理位置之间的映射关系,当用户须要对Account.Container.Object操作时,就须要查询相应的Ring文件( ...

  4. 「分布式专题」分布式系统中一致性hash算法

    近年来B2C.O2O等商业概念的提出和移动端的发展,使得分布式系统流行了起来.分布式系统相对于单系统,解决了流量大.系统高可用和高容错等问题.功能强大也意味着实现起来需要更多技术的支持.例如系统访问层 ...

  5. 良好的分布式cahce系统中,一致性hash算法需要满足什么?

    良好的分布式cahce系统中,一致性hash算法需要满足什么?你知道吗?让我们来一起学习下吧. 良好的分布式cahce系统中,一致性hash算法应该满足哪些方面 平衡性(Balance).单调性(Mo ...

  6. 从jredis中学习一致性hash算法

    jredis是redis的java客户端,通过sharde实现负载路由,一直很好奇jredis的sharde如何实现,翻开jredis源码研究了一番,所谓sharde其实就是一致性hash算法.其实, ...

  7. java中的算法(一致性hash算法和数据结构的问题)

    文章目录 一.一致性hash算法 二.问题的引入? 2.1 解决方案1 HashSet 2.2 解决方案2 TreeSet 里面 2.3 使用集合存储字符串数据的优缺点 三.引入位集合 3.1 图示 ...

  8. 不会一致性hash算法,劝你简历别写搞过负载均衡

    这两天看到技术群里,有小伙伴在讨论一致性hash算法的问题,正愁没啥写的题目就来了,那就简单介绍下它的原理.下边我们以分布式缓存中经典场景举例,面试中也是经常提及的一些话题,看看什么是一致性hash算 ...

  9. 什么是一致性 Hash 算法

    数据分片 先让我们看一个例子吧 我们经常会用 Redis 做缓存,把一些数据放在上面,以减少数据的压力. 当数据量少,访问压力不大的时候,通常一台Redis就能搞定,为了高可用,弄个主从也就足够了: ...

最新文章

  1. linux命令lsof
  2. 今天仔细学习了html加载执行的顺序
  3. 深入理解C++类的构造函数与析构函数
  4. 实验一 绘制金刚石图案
  5. 目标检测数据集MSCOCO简介
  6. WPF之无法触发KeyDown或者KeyUp键盘事件
  7. LPC2000 UART串口使用心得
  8. I begin to keep a daily
  9. 绝对干货!纯用HTML+CSS+JS 编写的计算器应用
  10. docker 删除默认连接_database – 如何从已删除的Docker容器中恢复数据?如何将其重新连接到数据?...
  11. linux根文件分析,Linux根文件系统详解
  12. NSTimer 的正确用法你真的知道吗?
  13. Internet Download Manager永久版功能强大的网络下载器
  14. 计算机视觉的一些测试数据集和源码站点
  15. python实现粒子滤波目标跟踪_QT+Opencv粒子滤波算法实现视频目标跟踪——如何选择跟踪算法...
  16. 基于Netty和Java的GUI界面实现在线聊天室软件
  17. java出现圅_java获取汉字拼音首字母A
  18. Docker管理工具Web UI:DockerUI Shipyard /portainer
  19. Perl_Tkx_Canvas绘图功能函数介绍
  20. 如何清除windowsoffice KMS激活

热门文章

  1. 任正非谈“咖啡杯”文化
  2. matlab ild,10GBASE-KR
  3. 用python输入 菱形
  4. java无侵入埋点 point_无侵入埋点
  5. WINDOWS系统“资源管理器”进程选项关闭之后的解决方法
  6. 风电齿轮箱在线监测方案
  7. 【2022新版】Java 终极学习路线(文末高清大图)-共计9大模块/6大框架/13个中间件
  8. 基于JAVA竞赛信息发布及组队系统计算机毕业设计源码+系统+mysql数据库+lw文档+部署
  9. 小白的测试人生(三)小白如何进入IT行业及如何选择培训机构
  10. win服务器物理内存占用高,win10系统长时间使用物理内存过高的解决方法