设为首页 收藏本站
查看: 1120|回复: 0

[经验分享] 一致性Hash算法在Memcached中的应用

[复制链接]

尚未签到

发表于 2015-8-31 11:42:58 | 显示全部楼层 |阅读模式
前言
  大家应该都知道Memcached要想实现分布式只能在客户端来完成,目前比较流行的是通过一致性hash算法来实现.常规的方法是将server的hash值与server的总台数进行求余,即hash%N,这种方法的弊端是当增减服务器时,将会有较多的缓存需要被重新分配且会造成缓存分配不均匀的情况(有可能某一台服务器分配的很多,其它的却很少).
  今天分享一种叫做”ketama”的一致性hash算法,它通过虚拟节点的概念和不同的缓存分配规则有效的抑制了缓存分布不均匀,并最大限度地减少服务器增减时缓存的重新分布。

实现思路
  假设我们现在有N台Memcached的Server,如果我们用统一的规则对memcached进行Set,Get操作. 使具有不同key的object很均衡的分散存储在这些Server上,Get操作时也是按同样规则去对应的Server上取出object,这样各个Server之间不就是一个整体了吗?
  那到底是一个什么样的规则?
  如下图所示,我们现在有5台(A,B,C,D,E)Memcached的Server,我们将其串联起来形成一个环形,每一台Server都代表圆环上的一个点,每一个点都具有唯一的Hash值,这个圆环上一共有2^32个点.
DSC0000.png
  那么该如何确定每台Server具体分布在哪个点上? 这里我们通过”Ketama”的Hash算法来计算出每台Server的Hash值,拿到Hash值后就可以对应到圆环上点了.(可以用Server的IP地址作为Hash算法的Key.)
  这样做的好处是,如下图当我新增Server  F时,那么我只需要将hash值落在C和F之间的object从原本的D上重新分配到F上就可以了,其它的server上的缓存不需要重新分配,并且新增的Server也能及时帮忙缓冲其它Server的压力.
DSC0001.jpg
  到此我们已经解决了增减服务器时大量缓存需要被重新分配的弊端.那该如何解决缓存分配不均匀的问题呢?因为现在我们的server只占据圆环上的6个点,而圆环上总共有2^32个点,这极其容易导致某一台server上热点非常多,某一台上热点很少的情况.
  ”虚拟节点”的概念很好的解决了这种负载不均衡的问题.通过将每台物理存在的Server分割成N个虚拟的Server节点(N通常根据物理Server个数来定,这里有个比较好的阈值为250).这样每个物理Server实际上对应了N个虚拟的节点. 存储点多了,各个Server的负载自然要均衡一些.就像地铁站出口一样,出口越多,每个出口出现拥挤的情况就会越少.
  代码实现:



//保存所有虚拟节点信息, key : 虚拟节点的hash key, value: 虚拟节点对应的真实server
private Dictionary<uint, string> hostDictionary = new Dictionary<uint, string>();
//保存所有虚拟节点的hash key, 已按升序排序
private uint[] ketamaHashKeys = new uint[] { };
//保存真实server主机地址
private string[] realHostArr = new string[] { };
//每台真实server对应虚拟节点个数
private int VirtualNodeNum = 250;
public KetamaVirtualNodeInit(string[] hostArr)
{
this.realHostArr = hostArr;
this.InitVirtualNodes();
}
/// <summary>
/// 初始化虚拟节点
/// </summary>
private void InitVirtualNodes()
{
hostDictionary = new Dictionary<uint, string>();
List<uint> hostKeys = new List<uint>();
if (realHostArr == null || realHostArr.Length == 0)
{
throw new Exception("不能传入空的Server集合");
}
for (int i = 0; i < realHostArr.Length; i++)
{
for (int j = 0; j < VirtualNodeNum; j++)
{
byte[] nameBytes = Encoding.UTF8.GetBytes(string.Format("{0}-node{1}", realHostArr, j));
//调用Ketama hash算法获取hash key
uint hashKey = BitConverter.ToUInt32(new KetamaHash().ComputeHash(nameBytes), 0);
hostKeys.Add(hashKey);
if (hostDictionary.ContainsKey(hashKey))
{
throw new Exception("创建虚拟节点时发现相同hash key,请检查是否传入了相同Server");
}
hostDictionary.Add(hashKey, realHostArr);
}
}
hostKeys.Sort();
ketamaHashKeys = hostKeys.ToArray();
}
  

一致性hash算法的分配规则
  到此我们已经知道了所有虚拟节点的Hash值, 现在让我们来看下当我们拿到一个对象时如何存入Server, 或是拿到一个对象的Key时该如何取出对象.
  Set一个对象时,先将对象的Key作为”Ketama”算法的Key,计算出Hash值后我们需要做下面几个步骤.
  1:首先检查虚拟节点当中是否有与当前对象Hash值相等的,如有则直接将对象存入那个Hash值相等的节点,后面的步骤就不继续了.
  2:如没有,则找出第一个比当前对象Hash值要大的节点,(节点的Hash值按升序进行排序,圆环上对应按照顺时针来排列),即离对象最近的节点,然后将对象存入该节点.
  3:如果没有找到Hash值比对象要大的Server,证明对象的Hash值是介于最后一个节点和第一个节点之间的,也就是圆环上的E和A之间.这种情况就直接将对象存入第一个节点,即A.
  代码实现:  



     /// <summary>
/// 根据hash key 获取对应的真实Server
/// </summary>
/// <param name="hash"></param>
/// <returns></returns>
public string GetHostByHashKey(string key)
{
byte[] bytes = Encoding.UTF8.GetBytes(key);
uint hash = BitConverter.ToUInt32(new KetamaHash().ComputeHash(bytes), 0);
//寻找与当前hash值相等的Server.
int i = Array.BinarySearch(ketamaHashKeys, hash);
//如果i小于零则表示没有hash值相等的虚拟节点
if (i < 0)
{
//将i继续按位求补,得到数组中第一个大于当前hash值的虚拟节点
i = ~i;
//如果按位求补后的i大于等于数组的大小,则表示数组中没有大于当前hash值的虚拟节点
//此时直接取第一个server
if (i >= ketamaHashKeys.Length)
{
i = 0;
}
}
//根据虚拟节点的hash key 返回对应的真实server host地址
return hostDictionary[ketamaHashKeys];
}
  
  Get一个对象,同样也是通过”Ketama”算法计算出Hash值,然后与Set过程一样寻找节点,找到之后直接取出对象即可.
  那么这个”Ketama”到底长什么样呢,让我们来看看代码实现.



    /// <summary>
///  Ketama hash加密算法
///  关于HashAlgorithm参见MSDN链接
///  http://msdn.microsoft.com/zh-cn/library/system.security.cryptography.hashalgorithm%28v=vs.110%29.aspx
/// </summary>
public class KetamaHash : HashAlgorithm
{
private static readonly uint FNV_prime = 16777619;
private static readonly uint offset_basis = 2166136261;
protected uint hash;
public KetamaHash()
{
HashSizeValue = 32;
}
public override void Initialize()
{
hash = offset_basis;
}
protected override void HashCore(byte[] array, int ibStart, int cbSize)
{
int length = ibStart + cbSize;
for (int i = ibStart; i < length; i++)
{
hash = (hash * FNV_prime) ^ array;
}
}
protected override byte[] HashFinal()
{
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
return BitConverter.GetBytes(hash);
}
}
测试性能
  最后我把自己参考BeitMemcached写的算法与老代(Discuz!代震军)参考SPYMemcached写的做了一下对比.
  源码在后面有下载.
  结果:查找5W个key的时间比老代的版本快了100多倍,但在负载均衡方面差了一些.
  测试数据:
  1:真实Server都是5台
  2:随机生成5W个字符串key(生成方法直接拿老代的)
  3:虚拟节点都是250个
  我的版本:
DSC0002.png
  老代的版本:
DSC0003.png

参考资料
  BeitMemcached源码
  老代: 一致性Hash算法(KetamaHash)的c#实现
  总结一致性哈希(Consistent Hashing)
  
  测试代码下载:Memcached-ketama
  
  

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-106769-1-1.html 上篇帖子: 尝试使用Memcached遇到的狗血问题 下篇帖子: memcached client
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表