「一致性哈希」是什么
什么是哈希算法呢,百度百科的定义如下:
哈希算法将任意长度的二进制值映射为较短的固定长度的二进制值,这个小的二进制值称为哈希值。哈希值是一段数据唯一且极其紧凑的数值表示形式。
Hash算法存在一定的局限性。例如,我们有7台服务器,通过我们上一节的Hash算法将用户id(自然数)作为输入,得到一个服务器编号。然后将用户的信息近似均匀地分配到这7台服务器上,从而实现负载均衡。随着用户增多,我们增加一台服务器时,一般需要进行重新Hash操作,对各个服务器上的数据进行重新分配,此操作几乎涉及所有服务器上的所有数据,工作量极大。
那么一致性哈希是什么呢?
一致性Hash算法(Consistent Hashing
)是一种hash算法,它能够在Hash输出空间发生变化时,引起最小的变动。
一致性哈希算法在1997年由麻省理工学院提出,是一种特殊的哈希算法,目的是解决分布式缓存的问题,解决了简单哈希算法在分布式哈希表中存在的动态伸缩等问题。
用虚拟环形的结构来表示资源请求者(为了叙述方便,后文将称之为“请求”)和服务器节点,这个环通常被称作一个 hashring
。
良好的分布式cahce系统中的一致性hash算法应该满足以下几个方面:
- 平衡性(Balance)
平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。
- 单调性(Monotonicity)
单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲区加入到系统中,那么哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲区中去,而不会被映射到旧的缓冲集合中的其他缓冲区。简单的哈希算法往往不能满足单调性的要求。哈希结果的变化意味着当缓冲空间发生变化时,所有的映射关系需要在系统内全部更新。
- 分散性(Spread)
在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。
- 负载(Load)
负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。
- 平滑性(Smoothness)
平滑性是指缓存服务器的数目平滑改变和缓存对象的平滑改变是一致的。
一致性hash算法思路
一致性hash时采用如下步骤:
- 首先求出服务器(节点)的哈希值,并将其配置到0~2^32的圆(continuum)上。
- 然后采用同样的方法求出存储数据的键的哈希值,并映射到相同的圆上。
- 然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器上。
- 如果超过2^32仍然找不到服务器,就会保存到第一台服务器上。
一致性hash存在的问题及虚拟节点的引入:
问题:如果服务器数特别少,不是均匀分布在哈希环上,那么当客户端访问时,容易造成大量的访问积压在一台服务器上,造成数据倾斜问题,引起负载过高。
解决方案:引入虚拟节点的概念,对每一个服务器节点计算依据特定规则计算多个Hash,分布在哈希环上,当客户端请求来时,如果顺时针指定的为某个虚拟节点,则交由真实的节点服务器处理请求。
- 一致性哈希算法代码
import java.util.SortedMap;
import java.util.TreeMap;
/**
* 一致性hash算法(不含虚拟节点)
*/
public class ConsistentHashNoVirtual {
public static void main(String[] args) {
//step1:定义server服务器的ip值映射到哈希环上
String[] tomcatServers = new String[]{"123.111.0.0","123.101.3.1","111.20.35.2","123.98.26.3"};
//使用sortedMap模拟哈希环
SortedMap<Integer,String> serverMap = new TreeMap<Integer, String>();
for(String tomcatServer:tomcatServers){
//计算每个服务器id的哈希值
int serverHash = Math.abs(tomcatServer.hashCode());
//将服务端哈希值以及其ip地址的对应关系存储到sortedMap中
serverMap.put(serverHash,tomcatServer);
}
//step2:针对客户端的ip计算出对应的哈希值
String[] clientServers = new String[]{"10.78.12.3","113.25.63.1","126.12.3.8"};
for(String clientServer:clientServers){
int clientHash = Math.abs(clientServer.hashCode());
//step3:看客户端的ip能够被哪个服务器所处理
//顺时针获取比这个客户端ip的hash值大的服务
SortedMap<Integer, String> integerStringSortedMap = serverMap.tailMap(clientHash);
if(integerStringSortedMap.isEmpty()){
//如果查询出来为空的,那么取哈希环上的第一个值
Integer integer = serverMap.firstKey();
System.out.println("客户端:"+clientServer+"路由到的tomcat服务器ip为"+serverMap.get(integer));
}else{
Integer integer = integerStringSortedMap.firstKey();
System.out.println("客户端:"+clientServer+"路由到的tomcat服务器ip为"+serverMap.get(integer));
}
}
}
}
复制代码
- 含虚拟节点的哈希算法
import java.util.SortedMap;
import java.util.TreeMap;
/**
* 一致性hash算法(包含虚拟节点)
*/
public class ConsistentHashWithVirtual {
public static void main(String[] args) {
//step1:定义server服务器的ip值映射到哈希环上
String[] tomcatServers = new String[]{"123.111.0.0","123.101.3.1","111.20.35.2","123.98.26.3"};
//使用sortedMap模拟哈希环
SortedMap<Integer,String> serverMap = new TreeMap<Integer, String>();
//定义虚拟节点的数量
int virtualCount = 3;
for(String tomcatServer:tomcatServers){
//计算每个服务器id的哈希值
int serverHash = Math.abs(tomcatServer.hashCode());
//将服务端哈希值以及其ip地址的对应关系存储到sortedMap中
serverMap.put(serverHash,tomcatServer);
//配置虚拟节点
for(int i = 0;i<virtualCount;i++){
//计算虚拟节点的哈希值
int virtualHash = Math.abs((tomcatServer+"#"+i).hashCode());
//将虚拟节点哈希值与服务器关系存储到
serverMap.put(virtualHash,"由虚拟节点"+i+"映射过来的请求:"+tomcatServer);
}
}
//step2:针对客户端的ip计算出对应的哈希值
String[] clientServers = new String[]{"10.78.12.3","113.25.63.1","126.12.3.8"};
for(String clientServer:clientServers){
int clientHash = Math.abs(clientServer.hashCode());
//step3:看客户端的ip能够被哪个服务器所处理
//顺时针获取比这个客户端ip的hash值大的服务
SortedMap<Integer, String> integerStringSortedMap = serverMap.tailMap(clientHash);
if(integerStringSortedMap.isEmpty()){
//如果查询出来为空的,那么取哈希环上的第一个值
Integer integer = serverMap.firstKey();
System.out.println("客户端:"+clientServer+"路由到的tomcat服务器ip为"+serverMap.get(integer));
}else{
Integer integer = integerStringSortedMap.firstKey();
System.out.println("客户端:"+clientServer+"路由到的tomcat服务器ip为"+serverMap.get(integer));
}
}
}
}
复制代码
一致性哈希算法一般用来干什么
一般我们在项目的负载均衡上要求资源被均匀的分配到所有的服务器节点上,同时,还需要对资源的请求能迅速的路由到具体的节点,例如:
-
我们在做RPC服务的时候,会经常部署多台服务器,然而有时有这样的需求就是,我们希望将同一类型的请求路由到同一台机器上,这个时候就可以用一致性hash算法来实现。
-
MemCache集群,要求存储数据均匀的放到集群中的各个节点上,访问这些数据时能快速的路由到集群中对应存放该数据的节点上;并且要求增删节点对整个集群的影响很小,不至于有大的动荡造成整体负载的不稳定,这个时候也是可以用一致性hash算法。
常见的「一致性哈希」算法
KETAMA_HASH算法
该算法最初是由Last.fm的程序员实现的并得到了广泛的应用,一些开源框架譬如spymemcached,twemproxy等都内置了该算法的实现。
我们从spymemcached的源码出发,分析Ketama算法的具体实现。
在类KetamaNodeLocator.java里有个setKetamaNodes()方法负责一致性哈希环的初始化工作, 代码如下:
protected void setKetamaNodes(List<MemcachedNode> nodes) {
TreeMap<Long, MemcachedNode> newNodeMap = new TreeMap<Long, MemcachedNode>();
int numReps = config.getNodeRepetitions();
for (MemcachedNode node : nodes) {
// Ketama does some special work with md5 where it reuses chunks.
if (hashAlg == DefaultHashAlgorithm.KETAMA_HASH) {
for (int i = 0; i < numReps / 4; i++) {
byte[] digest = DefaultHashAlgorithm.computeMd5(config.getKeyForNode(node, i));
for (int h = 0; h < 4; h++) {
Long k = ((long) (digest[3 + h * 4] & 0xFF) << 24)
| ((long) (digest[2 + h * 4] & 0xFF) << 16)
| ((long) (digest[1 + h * 4] & 0xFF) << 8)
| (digest[h * 4] & 0xFF);
newNodeMap.put(k, node);
getLogger().debug("Adding node %s in position %d", node, k);
}
}
} else {
for (int i = 0; i < numReps; i++) {
newNodeMap.put(hashAlg.hash(config.getKeyForNode(node, i)), node);
}
}
}
assert newNodeMap.size() == numReps * nodes.size();
ketamaNodes = newNodeMap;
}
复制代码
MurmurHash算法
MurmurHash 是一种非加密型哈希函数,适用于一般的哈希检索操作。 由Austin Appleby在2008年发明, 并出现了多个变种,都已经发布到了公有领域(public domain)。与其它流行的哈希函数相比,对于规律性较强的key,MurmurHash的随机分布特征表现更良好。
Redis在实现字典时用到了两种不同的哈希算法,MurmurHash便是其中一种(另一种是djb).
代码来自GitHub: github.com/aappleby/sm…
Murmurhash3.h
//-----------------------------------------------------------------------------
// MurmurHash3 was written by Austin Appleby, and is placed in the public
// domain. The author hereby disclaims copyright to this source code.
#ifndef _MURMURHASH3_H_
#define _MURMURHASH3_H_
//-----------------------------------------------------------------------------
// Platform-specific functions and macros
// Microsoft Visual Studio
#if defined(_MSC_VER) && (_MSC_VER < 1600)
typedef unsigned char uint8_t;
typedef unsigned int uint32_t;
typedef unsigned __int64 uint64_t;
// Other compilers
#else // defined(_MSC_VER)
#include <stdint.h>
#endif // !defined(_MSC_VER)
//-----------------------------------------------------------------------------
void MurmurHash3_x86_32 ( const void * key, int len, uint32_t seed, void * out );
void MurmurHash3_x86_128 ( const void * key, int len, uint32_t seed, void * out );
void MurmurHash3_x64_128 ( const void * key, int len, uint32_t seed, void * out );
//-----------------------------------------------------------------------------
#endif // _MURMURHASH3_H_
复制代码
Murmurhash3.c
//-----------------------------------------------------------------------------
// MurmurHash3 was written by Austin Appleby, and is placed in the public
// domain. The author hereby disclaims copyright to this source code.
// Note - The x86 and x64 versions do _not_ produce the same results, as the
// algorithms are optimized for their respective platforms. You can still
// compile and run any of them on any platform, but your performance with the
// non-native version will be less than optimal.
//
// github : https://github.com/aappleby/smhasher
#include "MurmurHash3.h"
//-----------------------------------------------------------------------------
// Platform-specific functions and macros
// Microsoft Visual Studio
#if defined(_MSC_VER)
#define FORCE_INLINE __forceinline
#include <stdlib.h>
#define ROTL32(x,y) _rotl(x,y)
#define ROTL64(x,y) _rotl64(x,y)
#define BIG_CONSTANT(x) (x)
// Other compilers
#else // defined(_MSC_VER)
#define FORCE_INLINE inline __attribute__((always_inline))
inline static uint32_t rotl32 ( uint32_t x, int8_t r )
{
return (x << r) | (x >> (32 - r));
}
inline static uint64_t rotl64 ( uint64_t x, int8_t r )
{
return (x << r) | (x >> (64 - r));
}
#define ROTL32(x,y) rotl32(x,y)
#define ROTL64(x,y) rotl64(x,y)
#define BIG_CONSTANT(x) (x##LLU)
#endif // !defined(_MSC_VER)
//-----------------------------------------------------------------------------
// Block read - if your platform needs to do endian-swapping or can only
// handle aligned reads, do the conversion here
FORCE_INLINE uint32_t getblock32 ( const uint32_t * p, int i )
{
return p[i];
}
FORCE_INLINE uint64_t getblock64 ( const uint64_t * p, int i )
{
return p[i];
}
//-----------------------------------------------------------------------------
// Finalization mix - force all bits of a hash block to avalanche
FORCE_INLINE uint32_t fmix32 ( uint32_t h )
{
h ^= h >> 16;
h *= 0x85ebca6b;
h ^= h >> 13;
h *= 0xc2b2ae35;
h ^= h >> 16;
return h;
}
//----------
FORCE_INLINE uint64_t fmix64 ( uint64_t k )
{
k ^= k >> 33;
k *= BIG_CONSTANT(0xff51afd7ed558ccd);
k ^= k >> 33;
k *= BIG_CONSTANT(0xc4ceb9fe1a85ec53);
k ^= k >> 33;
return k;
}
//-----------------------------------------------------------------------------
void MurmurHash3_x86_32 ( const void * key, int len,
¦ uint32_t seed, void * out )
{
const uint8_t * data = (const uint8_t*)key;
const int nblocks = len / 4;
uint32_t h1 = seed;
const uint32_t c1 = 0xcc9e2d51;
const uint32_t c2 = 0x1b873593;
//----------
// body
const uint32_t * blocks = (const uint32_t *)(data + nblocks*4);
for(int i = -nblocks; i; i++)
{
uint32_t k1 = getblock32(blocks,i);
k1 *= c1;
k1 = ROTL32(k1,15);
k1 *= c2;
h1 ^= k1;
h1 = ROTL32(h1,13);
h1 = h1*5+0xe6546b64;
}
//----------
// tail
const uint8_t * tail = (const uint8_t*)(data + nblocks*4);
uint32_t k1 = 0;
switch(len & 3)
{
case 3: k1 ^= tail[2] << 16;
case 2: k1 ^= tail[1] << 8;
case 1: k1 ^= tail[0];
k1 *= c1; k1 = ROTL32(k1,15); k1 *= c2; h1 ^= k1;
};
//----------
// finalization
h1 ^= len;
h1 = fmix32(h1);
*(uint32_t*)out = h1;
}
//-----------------------------------------------------------------------------
void MurmurHash3_x86_128 ( const void * key, const int len,
uint32_t seed, void * out )
{
const uint8_t * data = (const uint8_t*)key;
const int nblocks = len / 16;
uint32_t h1 = seed;
uint32_t h2 = seed;
uint32_t h3 = seed;
uint32_t h4 = seed;
const uint32_t c1 = 0x239b961b;
const uint32_t c2 = 0xab0e9789;
const uint32_t c3 = 0x38b34ae5;
const uint32_t c4 = 0xa1e38b93;
//----------
// body
const uint32_t * blocks = (const uint32_t *)(data + nblocks*16);
for(int i = -nblocks; i; i++)
{
uint32_t k1 = getblock32(blocks,i*4+0);
uint32_t k2 = getblock32(blocks,i*4+1);
uint32_t k3 = getblock32(blocks,i*4+2);
uint32_t k4 = getblock32(blocks,i*4+3);
k1 *= c1; k1 = ROTL32(k1,15); k1 *= c2; h1 ^= k1;
h1 = ROTL32(h1,19); h1 += h2; h1 = h1*5+0x561ccd1b;
k2 *= c2; k2 = ROTL32(k2,16); k2 *= c3; h2 ^= k2;
h2 = ROTL32(h2,17); h2 += h3; h2 = h2*5+0x0bcaa747;
k3 *= c3; k3 = ROTL32(k3,17); k3 *= c4; h3 ^= k3;
h3 = ROTL32(h3,15); h3 += h4; h3 = h3*5+0x96cd1c35;
k4 *= c4; k4 = ROTL32(k4,18); k4 *= c1; h4 ^= k4;
h4 = ROTL32(h4,13); h4 += h1; h4 = h4*5+0x32ac3b17;
}
//----------
// tail
const uint8_t * tail = (const uint8_t*)(data + nblocks*16);
uint32_t k1 = 0;
uint32_t k2 = 0;
uint32_t k3 = 0;
uint32_t k4 = 0;
switch(len & 15)
{
case 15: k4 ^= tail[14] << 16;
case 14: k4 ^= tail[13] << 8;
case 13: k4 ^= tail[12] << 0;
k4 *= c4; k4 = ROTL32(k4,18); k4 *= c1; h4 ^= k4;
case 12: k3 ^= tail[11] << 24;
case 11: k3 ^= tail[10] << 16;
case 10: k3 ^= tail[ 9] << 8;
case 9: k3 ^= tail[ 8] << 0;
k3 *= c3; k3 = ROTL32(k3,17); k3 *= c4; h3 ^= k3;
case 8: k2 ^= tail[ 7] << 24;
case 7: k2 ^= tail[ 6] << 16;
case 6: k2 ^= tail[ 5] << 8;
case 5: k2 ^= tail[ 4] << 0;
k2 *= c2; k2 = ROTL32(k2,16); k2 *= c3; h2 ^= k2;
case 4: k1 ^= tail[ 3] << 24;
case 3: k1 ^= tail[ 2] << 16;
case 2: k1 ^= tail[ 1] << 8;
case 1: k1 ^= tail[ 0] << 0;
k1 *= c1; k1 = ROTL32(k1,15); k1 *= c2; h1 ^= k1;
};
//----------
// finalization
h1 ^= len; h2 ^= len; h3 ^= len; h4 ^= len;
h1 += h2; h1 += h3; h1 += h4;
h2 += h1; h3 += h1; h4 += h1;
h1 = fmix32(h1);
h2 = fmix32(h2);
h3 = fmix32(h3);
h4 = fmix32(h4);
h1 += h2; h1 += h3; h1 += h4;
h2 += h1; h3 += h1; h4 += h1;
((uint32_t*)out)[0] = h1;
((uint32_t*)out)[1] = h2;
((uint32_t*)out)[2] = h3;
((uint32_t*)out)[3] = h4;
}
//-----------------------------------------------------------------------------
void MurmurHash3_x64_128 ( const void * key, const int len,
const uint32_t seed, void * out )
{
const uint8_t * data = (const uint8_t*)key;
const int nblocks = len / 16;
uint64_t h1 = seed;
uint64_t h2 = seed;
const uint64_t c1 = BIG_CONSTANT(0x87c37b91114253d5);
const uint64_t c2 = BIG_CONSTANT(0x4cf5ad432745937f);
//----------
// body
const uint64_t * blocks = (const uint64_t *)(data);
for(int i = 0; i < nblocks; i++)
{
uint64_t k1 = getblock64(blocks,i*2+0);
uint64_t k2 = getblock64(blocks,i*2+1);
k1 *= c1; k1 = ROTL64(k1,31); k1 *= c2; h1 ^= k1;
h1 = ROTL64(h1,27); h1 += h2; h1 = h1*5+0x52dce729;
k2 *= c2; k2 = ROTL64(k2,33); k2 *= c1; h2 ^= k2;
h2 = ROTL64(h2,31); h2 += h1; h2 = h2*5+0x38495ab5;
}
//----------
// tail
const uint8_t * tail = (const uint8_t*)(data + nblocks*16);
uint64_t k1 = 0;
uint64_t k2 = 0;
switch(len & 15)
{
case 15: k2 ^= ((uint64_t)tail[14]) << 48;
case 14: k2 ^= ((uint64_t)tail[13]) << 40;
case 13: k2 ^= ((uint64_t)tail[12]) << 32;
case 12: k2 ^= ((uint64_t)tail[11]) << 24;
case 11: k2 ^= ((uint64_t)tail[10]) << 16;
case 10: k2 ^= ((uint64_t)tail[ 9]) << 8;
case 9: k2 ^= ((uint64_t)tail[ 8]) << 0;
k2 *= c2; k2 = ROTL64(k2,33); k2 *= c1; h2 ^= k2;
case 8: k1 ^= ((uint64_t)tail[ 7]) << 56;
case 7: k1 ^= ((uint64_t)tail[ 6]) << 48;
case 6: k1 ^= ((uint64_t)tail[ 5]) << 40;
case 5: k1 ^= ((uint64_t)tail[ 4]) << 32;
case 4: k1 ^= ((uint64_t)tail[ 3]) << 24;
case 3: k1 ^= ((uint64_t)tail[ 2]) << 16;
case 2: k1 ^= ((uint64_t)tail[ 1]) << 8;
case 1: k1 ^= ((uint64_t)tail[ 0]) << 0;
k1 *= c1; k1 = ROTL64(k1,31); k1 *= c2; h1 ^= k1;
};
//----------
// finalization
h1 ^= len; h2 ^= len;
h1 += h2;
h2 += h1;
h1 = fmix64(h1);
h2 = fmix64(h2);
h1 += h2;
h2 += h1;
((uint64_t*)out)[0] = h1;
((uint64_t*)out)[1] = h2;
}
//-----------------------------------------------------------------------------
#if 1
#include <string.h>
#include <stdio.h>
#define SEED 0x97c29b3a
int main()
{
const char *str="abcdefghijklmn";
uint32_t out1;
MurmurHash3_x86_32(str, strlen(str), SEED, &out1);
printf("%u\n", out1);
uint32_t out2[4];
MurmurHash3_x86_128(str, strlen(str), SEED, out2);
printf("%u, %u, %u, %u\n", out2[0], out2[1], out2[2], out2[3]);
uint64_t out3[2];
MurmurHash3_x64_128(str, strlen(str), SEED, out3);
printf("%lu, %lu\n", out3[0], out3[1]);
return 0;
}
#endif
复制代码
- MurmurHash3_x86_32 将key 哈希32位的正整数
- MurmurHash3_x86_128 将key 哈希128位的4个无符号位32整数,x86是32位的
- MurmurHash3_x64_128 将key 哈希128位的2个无符号64位整数,x64是64位的
参考资料
zhuanlan.zhihu.com/p/183074177
zhuanlan.zhihu.com/p/203269133