consistent hash 算法在 memchached
当前很多大型的web系统为了减轻数据库服务器负载,会采用memchached作为缓存系统以提高响应速度。
目录:
- memchached简介
- hash
- 取模
- 一致性hash
- 虚拟节点
- 源码解析
- 参考资料
1. memchached简介
memcached是一个开源的高性能分布式内存对象缓存系统。
其实思想还是比较简单的,实现包括server端(memcached开源项目一般只单指server端)和client端两部分:
- server端本质是一个in-memory key-value store,通过在内存中维护一个大的hashmap用来存储小块的任意数据,对外通过统一的简单接口(memcached protocol)来提供操作。
- client端是一个library,负责处理memcached protocol的网络通信细节,与memcached server通信,针对各种语言的不同实现分装了易用的API实现了与不同语言平台的集成。
- web系统则通过client库来使用memcached进行对象缓存。
2. hash
memcached的分布式主要体现在client端,对于server端,仅仅是部署多个memcached server组成集群,每个server独自维护自己的数据(互相之间没有任何通信),通过daemon监听端口等待client端的请求。
而在client端,通过一致的hash算法,将要存储的数据分布到某个特定的server上进行存储,后续读取查询使用同样的hash算法即可定位。
client端可以采用各种hash算法来定位server:
取模
最简单的hash算法
targetServer = serverList[hash(key) % serverList.size]
直接用key的hash值(计算key的hash值的方法可以自由选择,比如算法CRC32、MD5,甚至本地hash系统,如java的hashcode)模上server总数来定位目标server。这种算法不仅简单,而且具有不错的随机分布特性。
但是问题也很明显,server总数不能轻易变化。因为如果增加/减少memcached server的数量,对原先存储的所有key的后续查询都将定位到别的server上,导致所有的cache都不能被命中而失效。
一致性hash
为了解决这个问题,需要采用一致性hash算法(consistent hash)
相对于取模的算法,一致性hash算法除了计算key的hash值外,还会计算每个server对应的hash值,然后将这些hash值映射到一个有限的值域上(比如0~2^32)。通过寻找hash值大于hash(key)的最小server作为存储该key数据的目标server。如果找不到,则直接把具有最小hash值的server作为目标server。
为了方便理解,可以把这个有限值域理解成一个环,值顺时针递增。
如上图所示,集群中一共有5个memcached server,已通过server的hash值分布到环中。
如果现在有一个写入cache的请求,首先计算x=hash(key),映射到环中,然后从x顺时针查找,把找到的第一个server作为目标server来存储cache,如果超过了2^32仍然找不到,则命中第一个server。比如x的值介于A~B之间,那么命中的server节点应该是B节点
可以看到,通过这种算法,对于同一个key,存储和后续的查询都会定位到同一个memcached server上。
那么它是怎么解决增/删server导致的cache不能命中的问题呢?
假设,现在增加一个server F,如下图
此时,cache不能命中的问题仍然存在,但是只存在于B~F之间的位置(由C变成了F),其他位置(包括F~C)的cache的命中不受影响(删除server的情况类似)。尽管仍然有cache不能命中的存在,但是相对于取模的方式已经大幅减少了不能命中的cache数量。
虚拟节点
但是,这种算法相对于取模方式也有一个缺陷:当server数量很少时,很可能他们在环中的分布不是特别均匀,进而导致cache不能均匀分布到所有的server上。
如图,一共有3台server – A,B,C。命中B的几率远远高于A和C。
为解决这个问题,需要使用虚拟节点的思想:为每个物理节点(server)在环上分配100~200个点,这样环上的节点较多,就能抑制分布不均匀。
当为cache定位目标server时,如果定位到虚拟节点上,就表示cache真正的存储位置是在该虚拟节点代表的实际物理server上。
另外,如果每个实际server的负载能力不同,可以赋予不同的权重,根据权重分配不同数量的虚拟节点。
源码解析:
下面结合一个java的memcached client(gwhalin / Memcached-Java-Client)的源码来看一下consistent hash的实现。
首先看server的分布:
// 采用有序map来模拟环 this .consistentBuckets = new TreeMap(); MessageDigest md5 = MD5.get(); //用MD5来计算key和server的hash值 // 计算总权重 if ( this .totalWeight for ( int i = 0 ; i < this .weights.length; i++ ) this .totalWeight += ( this .weights[i] == null ) ? 1 : this .weights[i]; } else if ( this .weights == null ) { this .totalWeight = this .servers.length; } // 为每个server分配虚拟节点 for ( int i = 0 ; i < servers.length; i++ ) { // 计算当前server的权重 int thisWeight = 1 ; if ( this .weights != null && this .weights[i] != null ) thisWeight = this .weights[i]; // factor用来控制每个server分配的虚拟节点数量 // 权重都相同时,factor=40 // 权重不同时,factor=40*server总数*该server权重所占的百分比 // 总的来说,权重越大,factor越大,可以分配越多的虚拟节点 double factor = Math.floor( (( double )( 40 * this .servers.length * thisWeight)) / ( double ) this .totalWeight ); for ( long j = 0 ; j < factor; j++ ) { // 每个server有factor个hash值 // 使用server的域名或IP加上编号来计算hash值 // 比如server - "172.45.155.25:11111"就有factor个数据用来生成hash值: // 172.45.155.25:11111-1, 172.45.155.25:11111-2, ..., 172.45.155.25:11111-factor byte [] d = md5.digest( ( servers[i] + "-" + j ).getBytes() ); // 每个hash值生成4个虚拟节点 for ( int h = 0 ; h < 4 ; h++ ) { Long k = (( long )(d[ 3 +h* 4 ]& 0xFF ) << 24 ) | (( long )(d[ 2 +h* 4 ]& 0xFF ) << 16 ) | (( long )(d[ 1 +h* 4 ]& 0xFF ) << 8 ) | (( long )(d[ 0 +h* 4 ]& 0xFF )); // 在环上保存节点 consistentBuckets.put( k, servers[i] ); } } // 每个server一共分配4*factor个虚拟节点 } |
每个server根据权重获得一个虚拟节点数量控制因子factor,然后由services[i]+”-”+i来生成factor个hash值,生成hash值时采用MD5算法。
由于MD5长度是16个字节,正好划分成4段,每段4字节,这样每段就对应一个虚拟节点。linex-liney通过把这一段的4字节拼装成连续的32bit,作为低32位拉升为一个Long。
为key定位cache存储的server:
// 用MD5来计算key的hash值 MessageDigest md5 = MD5.get(); md5.reset(); md5.update( key.getBytes() ); byte [] bKey = md5.digest(); // 取MD5值的低32位作为key的hash值 long hv = (( long )(bKey[ 3 ]& 0xFF ) << 24 ) | (( long )(bKey[ 2 ]& 0xFF ) << 16 ) | (( long )(bKey[ 1 ]& 0xFF ) << 8 ) | ( long )(bKey[ 0 ]& 0xFF ); // hv的tailMap的第一个虚拟节点对应的即是目标server SortedMap tmap = this .consistentBuckets.tailMap( hv ); return ( tmap.isEmpty() ) ? this .consistentBuckets.firstKey() : tmap.firstKey(); |
3. 参考资料
- 首次提出consistent hash的论文 – “Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web”
- Consistent hashing Wiki – http://en.wikipedia.org/wiki/Consistent_hashing
- Ketama: Consistent Hashing
- memcached开源项目主页
- memcached google code主页
- gwhalin / Memcached-Java-Client主页
from:http://www.slimeden.com/2011/09/web/memcached_client_hash
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!