场景

如果要设计一套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 = nodes
self.number = len(nodes) def get_node(self, index):
"""Return node by index % servers number
"""
if index < 0:
return None
return self.nodes[index%self.number] def normal_hash():
"""Normal hash usage example"""
nh = NormalHash(servers)
words = content.split() # 模拟初始化每天机器的db
database = {}
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 % 5

1%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 md5

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 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 = replicas self.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] = node
self._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 returned
along with it's position in the ring. If the hash ring is empty, (`None`, `None`) is returned.
"""
if not self.ring:
return None, None key = self.gen_key(string_key) nodes = self._sorted_keys
for i in xrange(0, len(nodes)):
node = nodes[i]
if key <= node:
return self.ring[node], i return self.ring[nodes[0]], 0 def 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 ring
starting at the correct position.
"""
if not self.ring:
yield None, None node, 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(): # 模拟初始化每天机器的db
database = {}
for s in servers:
database[s] = [] hr = HashRing(servers) for w in words.split():
database[hr.get_node(w)].append(w) print database consistent_hash()

 

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

一致性hash在分布式系统中的应用的更多相关文章

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

    大家好,我是小富~ 个人公众号:程序员内点事,欢迎学习交流 这两天看到技术群里,有小伙伴在讨论一致性hash算法的问题,正愁没啥写的题目就来了,那就简单介绍下它的原理.下边我们以分布式缓存中经典场景举 ...

  2. 给面试官讲明白:一致性Hash的原理和实践

    "一致性hash的设计初衷是解决分布式缓存问题,它不仅能起到hash作用,还可以在服务器宕机时,尽量少地迁移数据.因此被广泛用于状态服务的路由功能" 01分布式系统的路由算法 假设 ...

  3. 浅谈一致性hash

    相信做过互联网应用的都知道,如何很好的做到横向扩展,其实是个蛮难的话题,缓存可横向扩展,如果采用简单的取模,余数方式的部署,基本是无法做到后期的扩展的,数据迁移及分布都是问题,举个例子: 假设采用取模 ...

  4. 一致性hash算法以及其在分布式系统中的应用(转)

    初始架构

  5. 转载自lanceyan: 一致性hash和solr千万级数据分布式搜索引擎中的应用

    一致性hash和solr千万级数据分布式搜索引擎中的应用 互联网创业中大部分人都是草根创业,这个时候没有强劲的服务器,也没有钱去买很昂贵的海量数据库.在这样严峻的条件下,一批又一批的创业者从创业中获得 ...

  6. 一致性Hash算法在Redis分布式中的使用

    由于redis是单点,但是项目中不可避免的会使用多台Redis缓存服务器,那么怎么把缓存的Key均匀的映射到多台Redis服务器上,且随着缓存服务器的增加或减少时做到最小化的减少缓存Key的命中率呢? ...

  7. 一致性hash和solr千万级数据分布式搜索引擎中的应用

    互联网创业中大部分人都是草根创业,这个时候没有强劲的服务器,也没有钱去买很昂贵的海量数据库.在这样严峻的条件下,一批又一批的创业者从创业中 获得成功,这个和当前的开源技术.海量数据架构有着必不可分的关 ...

  8. 一致性Hash算法在Memcached中的应用

    前言 大家应该都知道Memcached要想实现分布式只能在客户端来完成,目前比较流行的是通过一致性hash算法来实现.常规的方法是将server的hash值与server的总台数进行求余,即hash% ...

  9. Jedis中的一致性hash

    Jedis中的一致性hash 本文仅供大家参考,不保证正确性,有问题请及时指出 一致性hash就不多说了,网上有很多说的很好的文章,这里说说Jedis中的Shard是如何使用一致性hash的,也为大家 ...

随机推荐

  1. OpenCV3学习笔记

    http://blog.csdn.net/u010429424/article/details/73691001 http://blog.csdn.net/zhaoxfxy/article/detai ...

  2. Mysql锁的类型与简析

    数据库锁设计的初衷是处理并发问题.作为多用户共享的资源,当出现并发访问的时候,数据库需要合理地控制资源的访问规则.而锁就是用来实现这些访问规则的重要数据结构. 根据加锁的范围,MySQL 里面的锁大致 ...

  3. Eclipse如何定位到某一个类所在硬盘上的位置

    解决方法:安装OpenExplorer_1.5.0.v201108051513.jar插件 将OpenExplorer_1.5.0.v201108051513.jar文件添加到Eclipse所在目录下 ...

  4. yii2 DateTimePicker显示到天

    扩展是 kartik\datetime\DateTimePicker; 关键是加入此配置  'minView'=> "month",示例如下: <?php echo D ...

  5. awk练习总结

    >>> >>>awk是个优秀文本处理工具,可以说是一门程序设计语言.下面是awk内置变量. 一.内置变量表 属性 说明 $0 当前记录(作为单个变量) $1~$n ...

  6. HTML canvas fillText()与measureText()方法

    HTML5 canvas fillText() 方法 实例 使用 fillText(),在画布上写文本 "你好!word!" 和 "我是w3c": JavaSc ...

  7. 二叉排序树实现(C++封装)

    设计思路 设计一个类,根结点只可读取,具备构造二叉树.插入结点.删除结点.查找. 查找最大值.查找最小值.查找指定结点的前驱和后继等功能接口. 二叉排序树概念 它或者是一棵空树:或者是具有下列性质的二 ...

  8. 图片热点的使用,html <area> 的用法

    <area>标记主要用于图像地图,通过该标记可以在图像地图中设定作用区域(又称为热点),这样当用户的鼠标移到指定的作用区域点击时,会自动链接到预先设定好的页面.其基本语法结构如下: < ...

  9. (17) go 协程管道

    一.协程 二.管道

  10. pycharm中在andconda环境中配置pyqt环境

    一般在andconda环境中,自带pyqt5 在pip install pyqt5之后,需要安装pyqt5_tools. 对于pycharm需要配置pyqt Designer和pyqt UIC. De ...