Memcached内存管理的局限性导致尽量不能让KEY永远不过期
在分布式环境下,KEY永远不过期会导致潜在的“脏数据”的风险。本文从Memcached内存管理策略Slab Allocator的角度分析,KEY永远不过期的潜在风险。实验看,Slab Allocator也是存在缺陷的,这种缺陷的存在导致:(1)不同应用的缓存,尽量不要存储在同一个Memcached实例中;(2)KEY不要永远不过期。
内存管理策略:Slab Allocator
Slab Allocator简介:
当Memcached接收到一个KEY-VALUE时,Memcached向操作系统申请一个Page,每个Page是1M。Memcached先看下96B(启动时-n参数指定,默认是96B)是否能够容纳VALUE,如果不能,则放大f=1.25倍,看是否能容纳,不能再放大,直到容纳或者超过1M(1个Page大小)。现在我们假设放大了21次后,找到能容纳它的,于是创建一个编号为22的SLAB(编号1的SLAB不用乘以f),这个SLAB用来做什么呢?先把申请来的1Page按照chunk_size=96B*1.25^21切分,每个块叫chunk,每个chunk用来存储一个合适大小的KEY-VALUE。
如果后继的KEY-VALUE,容量大小有合适的SLAB,则在这个SLAB中存放,如果chunks不够,可以申请新的Page,再切分成chunks;如果没有容量合适的SLAB,则新建一个新的SLAB,道理类似。
存储结构如图所示:
实验:LRU驱逐是针对“合身的”SLAB的
Memcached分配256M内存
启动两个Memcached,各分配256M内存,用Replica做双向复制(其实实验只需要1个Memcached进程,只不过我的实验环境习惯了双向复制)。
在10.10.83.179:11203
/usr/local/bin/memcached -d -p 11203 -m 256-x 10.10.83.177 -X 11234 -u root -l 10.10.83.179 -c 256 -P/tmp/memcloud_11203_10.10.83.177_11234.pid
在10.10.83.177:11204
/usr/local/bin/memcached -d -p 11204 -m 256-x 10.10.83.179 -X 11234 -u root -l 10.10.83.177 -c 256 -P/tmp/memcloud_11204_10.10.83.179_11234.pid
另外,Memcached版本是1.4.5 ; Slab的起始chunk_size是默认的96B;增长率f是默认的1.25。
观察1:执行SET命令前,查看slab状态
Memcached进程刚启动,没有任何slab被分配。
观察2:客户端执行SET
单线程,执行24157次SET指令,Value的大小是10K。数据的过期时间:2天。
客户端使用的是:XMemcached (xmemcached-1.3.6.jar) 注意:这个客户端对Value超过20K的数据序列化时,似乎有问题,wireshark抓包看数据少发了。(勘误1:后来无意间发现容量小的Value与大的Value,SET命令的Flag是不一样的,其实人家XMemcached是压缩了。我孤陋寡闻了。)
代码如下:
执行过程中,不断查看stats slabs的状态:
当客户端停止的时候,我们存入了这些KEY:
K:T1K0; r:true; C:0 ms;
……
K:T1K24156; r:true; C:0 ms;
总共存入了:24157 个KEY。
上图的统计信息也显示:cmd_set命令截止到24157;
STAT22:total_pages 257 表示内存已经用到了257M,因为1个Page是1M;
STAT22:total_chunks 24158 STAT22:used_chunks 24157 只剩余了1个chunk了。
STATtotal_malloced 268250432=(268250432/1024)/1024=255.8M
总的统计信息:stats显示截止目前没有“驱逐”任何的Item
观察3:同样大小的Value,再放入3个
把代码中的:numTime=2;numSetCmd=3
K:T2K0; r:true; C:125 ms;
K:T2K1; r:true; C:16 ms;
K:T2K2; r:true; C:0 ms;
日志显示3条都存入了。(时间代价相差那么大主要是客户端对连续多个SET有合并的功能)
命令cmd_set增加了3个,从24157增加到了24160;最后的一个空闲的chunk被占用了,按道理还缺少2个chunk的。查看stats的驱逐的确有两个被驱逐了“evictions 2”,Memcached已经开始启动LRU进行驱逐了。
此时内存使用率(填充率):STAT bytes 249299446 / STATlimit_maxbytes 268435456=92.8%
观察4:LRU驱逐,查看被驱逐的KEY
输出:符合LRU的驱逐算法
被驱逐的:T1K0
被驱逐的:T1K1
因为LRU的驱逐算法的存在,所以Memcached能够复用chunk的。
观察5:chunk复用的局限性
Memcached从操作系统以Page为单位“批发”内存后,把每个Page按照chunk_size切分成若干chunk,再以chunk为单位进行“零售”。Memcached永远不释放Page,而是不断复用chunk,观察4中的LRU驱逐让我们直观感受到了chunk复用。
问题:如果我们再向Memcached提交Value,而且Value的大小是现有chunk无法容纳的?这个时候,Memcached是拒绝呢?因为复用chunk是无法满足需要的,现有的chunk都太小了,不足以容纳更大容量的Value。
我们把Value的chunkSize调整到12K;numTime=3;numSetCmd=1。提交1次12K的Value。
我们查看:stats slabs和stats的驱逐情况
统计数据显示:memcached新创建了一个slab,chunk_size是13880/1024=13.5K,批发了1个Page,可零售为:75个chunk,目前被用掉了1个。还剩余74个。
记住我们刚才的KEY是:K:T3K0; r:true; C:141 ms;
小结:Memcached不驱逐,也不拒绝。
1、 memcached必须Value的大小分配适合的chunk;
2、 Memcached启动时的-m参数,只是个近似的。在观察3中“STAT bytes 249299446/ STAT limit_maxbytes 268435456=92.8%”利用率到了92.8%的时候(还没到100%呀),就开始拒绝分配内存了。原因很简单:因为还要预留些给其他的chunk_size的slab。否则,当我们填充更大的Value时,就没有Page了,没有Page必然没有chunk。我们可以推断:Memcached的LRU驱逐只是局部于Slab范围的。
观察6:LRU驱逐局部于SLAB
继续以12K的填充75次,因为Memcached只剩余了74个可容纳它的chunks。
结果:SLAB23号的,cmd_set增加了75次,从1到76次了。而used_chunks从1到75,只增加了74个。说明又存在驱逐了:的确stats的驱逐从2变3了。
K:T4K0; r:true; C:0 ms;
K:T4K74; r:true; C:0 ms;
第三次的驱逐是驱逐谁了呢?如果LRU是全局的,那应该驱逐掉SLAB22里面的,KEY应该是“T1K2”;但是,驱逐SLAB22的,腾出的chunk是不够容纳12K的Value的,因此LRU应该是局部于SLAB的,应该驱逐SLAB23里面的,这样KEY应该是:“T3K0”
输出:
提取 K: T1K2; V:10240
驱逐:T3K0
实验数据显示:LRU驱逐不是全局的LRU,而是针对KEY-VALUE“合身的”SLAB的LRU。
实验结论的意义:KEY不应该永远不过期
有了上面实验的结论:“LRU驱逐不是全局的LRU,而是针对KEY-VALUE“合身的”SLAB的LRU”。
如果使用Memcached的时候,KEY设置用不过期,会带来什么危害?
反证一下,如果KEY有个过期时间,那么即使在给定的时间面对大量的set,也会因为失效的KEY而腾出chunk,这样“热点SLAB”不至于把内存吃到92.8%。解释下:“热点SLAB”就是拥有Page最大的SLAB,同时拥有交多的读写操作(cmd_set)。因为一个应用的缓存的VALUE,多半会比较集中在某个chunk_size范围内,因此SLAB的分布往往是不平衡的。内存吃到92.8%,不是100%,就是上面实验显示的Memcached对热点SLAB只允许吃到92.8%,因为要预留内存给其他chunk_size的SLAB分配Page。当然这个92.8%应该跟memcached启动时默认-n=96B,-f=1.25是有关系的。
相反,如果KEY永远不过期,无疑会加剧,甚至非常快(比如应用跑上一周时间)“热点SLAB”(可能是1个,也可能是多个)把内存吃到92.8%。
在内存填充率到达92.8%的时候,如果由于应用数据需求的变化,导致新的热点SLAB的出现,那就糟糕了!!!因为当内存吃到92.8%的时,对于新的热点SLAB只能划出1个Page,也就是1M,这个新热点只能在1M的空间内LRU,无疑会大大降低命中率,缓存就变得没有意义了!!!
简单说,SLAB Allocator缺陷是:当内存填充率到达92.8%的时,应用不能出现新的“热点SLAB”。当然,92.8%只是极端情况,其实内存填充率到了60%,70%,80%的时候,新的“热点SLAB”也只能在剩余的32.8%,22.8%,12.8%的范围内LRU。因此,不同应用的缓存数据,尽可能不要存放在同一个memcached进程内,因为它们的热点SLAB的chunk_size往往不同,导致可共享的内存变小。KEY的永远不过期,会加剧热点SLAB分布太散。
诊断我的Memcached是否生病了?
那么,我们如何诊断我们的Memcached是否生了上面说的病呢?综合三个方面:
1、 内存使用率(填充率):stats.bytes/ stats.limit_maxbytes
2、 命中率:stat.get_hits/ stat.cmd_get
3、 Active SLAB的个数,以及热点SLAB的数量:stats slabs.active_slabs 和 热点SLAB:最好同时具有多个Page和多个cmd_set。
以刚才的实验数据,简单说明下:
SLAB 22号,占据了92.8%的内存,消耗了257个Page,cmd_set24160。SLAB 23号,占据了1个Page,不过好在它的cmd_set才76个,也就是说SLAB 23并不是热点SLAB。读写的人点集中在SLAB 22,而SLAB 22充分享用了被分配的大部分内存,因此这个情况非常理想。但是如果SLAB 23的cmd_set 也等于24160,那就生病了,因为这样必然导致SLAB 23的命中率很低。