一致性哈希算法
1. 解决的问题
在分布式服务中,往往有这样的场景:将某个用户或某台机器的请求负载路由到固定的某台服务器上。简单的做法直接是使用哈希算法,h = hash(key) % N ,该算法的核心思想是:将服务器编号,使用哈希算法取根据某类请求参数key(用户id或IP)计算出一个哈希值,再对该哈希值用服务器数据N进行取余(%)操作,从而得到服务器编号。使用该算法有一个问题,就是服务器数据数目(N)增加中或减少的时候,h的值都会被改变,即请求会负载到新的服务器上,有可能会导致状态数据的失效。有没有一种算法,既可以将同一请求负载到同一台服务器上,又可以在服务器增加或减少的时候将请求的变更控制在一定的范围内,所以提出了一致性哈希算法。
一致性哈希算法(Consistent Hashing)最早在论文《Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web》中被提出,其原理如下:
一致性哈希算法将整个哈希值(整数)空间组织成一个0~2^32-1的虚拟哈希环,首先,服务器按照名称(或编号)取哈希,并将该哈希值放置在哈希环上,然后再对key取哈希,按照随时针方向查找离该值最近的服务器结点哈希值,从而完成key与服务器的匹配映射工作。
通过一致性哈希算法,服务器的增加或减少只会影响该服务器周围的请求,不会扩大到整个哈希环,从而保证算法的可扩展性。
一致性哈希算法也有一个问题,就是服务器较少时,可能出现服务器之间负载的请求可能不均衡,有些服务器负载的请求可能过多,而有些服务负载较少。解决这个问题的关键是增加哈希环上服务节点的数量,在物理服务器不能增加的情况下,可以将一个服务结点映射为多个虚拟结点,均匀分布在哈希环上,从而解决该问题。
2. 哈希算法
在一致性哈希算法中,哈希算法是一个重要的组成部分,它将服务器结点和请求字符串转换为一个整数,如何选择一个好的哈希算法?评判一个哈希算法的标准是什么?
哈希算法大致有两种类型:加密哈希算法和非加密哈希算法,加密哈希算法为了防止攻击者找出碰撞而设计的,它的速度较慢。非加密哈希算法将字符串作为输入,通过计算输出一个整数,理想的哈希算法有一个特性:输出非常均匀分布在可能的输出域,特别是当输入非常相似的时候。可以将哈希算法大致分为三代:
- 第一代:SHA-1(1993),MD5(1992),CRC(1975),Lookup3(2006)
- 第二代:MurmurHash(2008)
- 第三代:CityHash, SpookyHash(2011)
其中SHA-1和MD5属于非加密哈希算法,其它都是非加密哈希算法,在一致性哈希算法中,主要用的是非加密哈希算法。除了以上的算法,还有一些算法没有列出,如JDK中hashCode使用的算法,另外,还有专门针对一致性哈希算法设计的哈希算法,如Ketama,也得到了广泛的运用。
在一致性哈希算法的使用场景中,有几个比较重要的算法,单独说明一下:
- MurmurHash 算法:高运算性能,低碰撞率,由 Austin Appleby 创建于 2008 年,现已应用到 Hadoop、libstdc++、nginx、libmemcached 等开源系统。2011 年 Appleby被Google雇佣,随后Google推出其变种的CityHash算法。官方只提供了C语言的实现版本。Java体系中,Guava,Redis,Memcached,Cassandra,HBase,Lucene都在使用它。
- FNV算法:全名为Fowler-Noll-Vo算法,是以三位发明人Glenn Fowler,Landon Curt Noll,Phong Vo 的名字来命名的,最早在 1991 年提出。特点和用途:FNV 能快速 hash 大量数据并保持较小的冲突率,它的高度分散使它适用于hash一些非常相近的字符串,比如URL,hostname,文件名,text,IP 地址等。
- Ketama 算法:Ketama不仅提供了一个哈希算法,更是提供了一套一致性哈希算法的实现,在Dubbo及Memcached中使用了该算法。
下面针对这些算法,作一一介绍:
1)Java 字符串哈希算法
1 | public int hashCode() { |
Java字符串哈希算法是遍历整个字符串,按照hash * 31 + c的算法计算,最后的哈希值为s[0] *31^(n-1) + s1*31^(n-2) + … + s[n-1]。
2)CRC 哈希算法
1 | public class CRCHashStrategy implements HashStrategy { |
3)FNV1_32 算法
1 | public class FnvHashStrategy implements HashStrategy { |
4)MurmurHash 算法
1 | public class MurmurHashStrategy implements HashStrategy { |
5)Ketama 算法
1 | public class KetamaHashStrategy implements HashStrategy { |
相对来说,在一致性哈希算法中,FNV,MurmurHash及Ketama是相对较好的算法,用的场景也较多。
3. 算法分析
在一致性算法中,需要将哈希值空间组织成一个虚拟的哈希环,服务器结点的哈希值放置在该哈希环上,查找离请求key的哈希值最近的值。在这里提到要构建一个哈希环,它的底层数据结构是什么样的?我们可以转换一个思路,在算法中,最重要的环节是匹配两个最近的哈希值,是否可以将服务器的哈希值存储到一个有序的数据列表中,匹配的操作就是找一个小于等待key的最大值。为了提高查询效率,底层的数据结构一般使用红黑树。下面分析一致性哈希算法的一种可能实现,该算法由作者徐靖峰实现,代码的github地址:https://github.com/lexburner/consistent-hash-algorithm
1 | public class ConsistentHashLoadBalancer implements LoadBalancer{ |
在该算法中,使用TreeMap的ceilingEntry方法返回离key的哈希值最近的服务器。
除了上面的一致性哈希算法的实现,还有一种实现,即Ketama算法,它不仅仅是一个哈希算法,更是一套完整的一致性哈希算法,在memcached中,其介绍如下:
Ketama is an implementation of a consistent hashing algorithm, meaning you can add or remove servers from the memcached pool without causing a complete remap of all keys.
Here’s how it works:
- Take your list of servers (eg: 1.2.3.4:11211, 5.6.7.8:11211, 9.8.7.6:11211)
- Hash each server string to several (100-200) unsigned ints
- Conceptually, these numbers are placed on a circle called the continuum. (imagine a clock face that goes from 0 to 2^32)
- Each number links to the server it was hashed from, so servers appear at several points on the continuum, by each of the numbers they hashed to.
- To map a key->server, hash your key to a single unsigned int, and find the next biggest number on the continuum. The server linked to that number is the correct server for that key.
- If you hash your key to a value near 2^32 and there are no points on the continuum greater than your hash, return the first server in the continuum.
If you then add or remove a server from the list, only a small proportion of keys end up mapping to different servers.
代码实现如下所示:
1 | public class KetamaConsistentHashLoadBalancer implements LoadBalancer { |
在Ketama一致性哈希算法中,以四个虚拟结点为一组,将服务器名称以Md5编码后,得到16个字节的编码,每个虚拟结点对应Md5码16个字节中的4个,组成一个long型数值,做为这个虚拟结点在环中的惟一key。请求key的哈希值则以Md5中的前4个字节计算得到。
4. 总结
在 对一致性Hash算法,Java代码实现的深入研究 这篇文章中,作者通过数据分析,得出使用MurmurHash,FNV及Ketama这三种哈希算法,都能得到较好的性能,具有很好的参考意义。
参考: