为了账号安全,请及时绑定邮箱和手机立即绑定

高性能缓存memcached初窥


关于key的有效期

key的有效期有3种格式:

  • 秒数。从设定的数开始第N秒后失效。
  • 时间戳。特定时间失效,例如在团购网站中,活动到中午12点失效,add key 0 137920999 6s
  • 0.不自动失效。有种误解:设置为0永久有效是错误的。因为编译memcached时可以指定一个常量,默认是30days,所以即使设置为0,30天后也会自动失效,也有可能等不到30天就会被新数据挤出去。

incrdecr命令的操作对象是unit32,范围是[0,2^31-1]。

内存分配机制

考虑这样一种情形:申请20M内存,释放15M内存,然后又需要申请12M内存,这样就产生了3M的内存碎片,随着程序的运行,碎片会越来越多。memcached采用了Slab Allocator分配机制:基本原理是按照预先规定的大小,将分配的内存分割成特定长度的块(chunk),并将尺寸相同的chunk分成组(chunk的集合),尽可能(完全解决内存碎片是不可能的)解决内存碎片问题。

Slab

根据收到的数据大小自动选择最合适的slab(具体实现是memcached中保存着slab空闲chunk的列表,根据这个列表选择空的chunk并将数据缓存其中),例如100bytes的item将会被存放在上图中的Slab2,但是剩下的12字节同样被浪费了,无法重新利用,这种做法只是尽可能减少内存碎片。使用memcached -vvv启动服务的时候可以看到类似以下的输出:

slab class   1: chunk size        96 perslab   10922
slab class   2: chunk size       120 perslab    8738
slab class   3: chunk size       152 perslab    6898
slab class   4: chunk size       192 perslab    5461
slab class   5: chunk size       240 perslab    4369
slab class   6: chunk size       304 perslab    3449
slab class   7: chunk size       384 perslab    2730
slab class   8: chunk size       480 perslab    2184

仔细观察可以发现:120/96 = 152 / 120 = 1.25,这个chunk的增长比例就叫做增长因子(grow factor,默认1.25,启动时可使用-f命令行参数) 。

警告:如果我们要存100bytes数据,正常情况下应该保存在chunk size为120bytes的仓库(即slab2),但是slab2已经满了,并不会选择存到更大的slab,例如slab3,而是将老的数据挤出去,其策略是惰性过期机制和LRU删除机制!

过期和删除机制
  1. 当某个值过期时,并没有从内存中删除,因此stats命令统计的时候curr_items有其信息;
  2. 取值的时候先判断是否过期,如果过期则返回空清空该key占用的chunk,curr_items就减少了;
  3. 如果之前没有get过,将不会自动删除,当某个新值占用其位置时,将会被当做空chunk来用。

过期指的是让用户看不到数据,并不是在过期瞬间正真从内存中移除,被称为惰性失效(lazy expiration)。好处是节省了CPU时间和检测是否过期的成本。

删除机制使用的LRU,对key进行更新,查询将会刷新key为最新(被认为是新数据)。即使某个key被设置为永久有效期也一样会被踢出,这个就是大名鼎鼎的永久数据被踢的现象。以122bytes大小的chunk为例,如果122bytes的chunk满了,又有新的值需要加入(长度为120bytes),应该挤掉最近最久未使用的。

memcached的LRU的原理是:维护每个key的计数器,当某个key被请求时该key对应的计数器清零,其他key对应的计数器加1,最后比较哪个key的计数器最大,这个key就是LRU。

memcached中的一些参数限制
  • key的长度限制为250bytes,二进制协议支持65535字节。
  • value限制为1M(100W字符)。
分布式取模算法的缺陷

所谓的分布式就是将不同的键存储到多态服务器,这样就引来一个问题?如何确定键和服务器的对应关系?最容易的算法是将key转化成数字(例如hash签名算法或者CRC32),然后将这个数字对服务器数量进行MOD运算。这种算法存在一个很大的弊端:原先有3个server分别按照hash取余,存储的数据对应关系为:

server1 - key0,key3 # mod = 0
server2 - key1,key4 # mod = 1
server3 - key2 # mod = 2

如果server3宕机,查询key3的时候,计算出来的是3 mod 2 = 1,即:我们应该到server2去取key3,肯定是无法找到的!然而key3对应的数据仍然好好滴在server1上存储着,一个严重的问题是:缓存命中率大大降低

从数学上讲,当服务器从N变成N-1之后,在[0,NN-1]范围内只有[0,N-1]算出来的余数没有改变(一共N个数),即:每NN-1个key只有N个key的模没有变,命中率为:N/(N*N-1) = 1/N-1。所以10台服务器变成9台服务器的时候11台变成10台服务器的时候命中率只有10%了,下降了90%!,并且服务器越多后果越严重,买的服务器越多结果性能越差,这不就亏大了?

求余算法的另一个缺点是打乱了原有的数据存储,例如:原来有3台服务器,存储数据为

server1 - 0,3,6,9
server2 - 1,4,7
server3 - 2,5,8

当server2宕机时,按照求余算法,数据分布为:

server1 - 0,2,4,6,8
server3 - 1,3,5,7,9

可以发现原来的3和9在server1上好好滴放着被迁移到了server3,原来的2和8在server3上放着被迁移到了server1,我们理想的情况是将宕机的server2中的数据分配到server1和server3就行了。数据迁移是很耗性能的

鉴于分布式取余的以上缺点,我们使用一致性哈希。

一致性哈希

把服务器各个节点映射放在钟表的各个时刻上,吧key也映射到钟表的某个时刻上。该key沿着钟表顺时针走,碰到的第一个节点即为该key的存储节点。如图所示:

memcached一致性哈希

以上有2点疑问:

  1. 时钟上的指针的最大才11点,如果有上百个memcached节点该怎么办?始终只是为了便于理解做的比喻。在实际应用中我们可以在圆环上分布[0,2^32-1]个数字,这样全世界的服务器都可以装下了。
  2. 如何将“节点名”和“键名”转化为整数。可以使用现成的函数,例如:crc32()。也可以自定义转化规则,但是要注意转化的碰撞率要低(碰撞值的是:不同的输入得到了相同的输出)。
一致性hash对其他节点的影响

一致性哈希的影响范围

从上图可以看出:当某个节点宕机之后哦只影响该节点顺时针之后的一个节点,而其他节点不受此影响,一致性哈希最大限度抑制了键的重新分布

一致性哈希+虚拟节点对缓存命中率的影响

由上图我们可以看出,在理想状态下:

  1. 节点在圆环上分配均匀,因此承担的任务也是平均的,但事实上:一般的哈希函数对于节点在圆环上的映射并不均匀。
  2. 当某个节点宕机之后直接冲击下一个节点,对下一个节点的冲击过大,所以能否把down掉的节点上的压力平均分配到其他所有存活着的节点上?

我们可以通过引入虚拟节点完全解决上述问题。

每个真实节点可以映射成M个虚拟节点,则N个真实节点就映射为MN个虚拟节点散列在圆环上。各个真实节点对应的虚拟节点交错分布*。这样,某个真实节点down掉之后,则把其影响分到到其他所有节点上。如图所示:

memcached虚拟节点

即:N台服务器变成N-1台服务器之后,剩下的N-1台服务器分担宕机的1台服务器的影响,从而不命中率为1/N-1,和分布式取模算法刚好相反!java代码实现这个实验模拟了当5台服务器挂掉一台之后不同算法对缓存命中率的影响:

分布式取模
一致性hash

点击查看更多内容
5人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消