谁动了我的内存? ---memcached内存分配回收策略出坑指南
使用过memcached缓存服务的朋友,肯定遇到过或者将要遇到一些内存分配与回收的坑。本文从实战问题出发,分析memcache使用的Slab Allocator内存内存管理机制的优势与缺陷。给大家一些出坑的建议。
先来看两个场景:
一、存储大文本并有频繁更新的场景。设置了key过期,当memcached服务跑了一段时间后,发现新key写不进去了,但配置的内存其实还剩余很多。(无法重分)
二、用户登录逻辑,常会将用户的session放入cache中加速返回,当memcached服务跑了一段时间后,发现一些session信息还未到设置的过期时间就被强制淘汰。(分区间淘汰、强制LRU、惰性回收)
带着这两个问题, 我们先从概念上了解一下memcached的内存分配机制。
memcached的设计哲学是“短平快”。
“短”是限制key和value的大小(value不能超时1m)
“平”是只支持KV这一种数据结构(对比Redis有各种数据结构)
“快”指的就是slab allocator内存分配机制。
slab allocator做为一种预分配内存管理机制。即要申请内存的时候,一次申请一个page,page大小固定为1m,根据要存的item,把page分成chunk,在一个page里面,chunk大小一样。而多个page中这些大小一样的chunk组成一类slab class。
凡事有利必要弊,架构就是取舍。这种内存分配策略对比传统malloc/free策略来说,优势在于减少了内存碎片,缺点在于浪费空间。
比如某个chunk是96bit,要存的item为1bit,那也要用掉这个96bit的chunk。当然这个例子比较极端。
memcahced的解决方案是这样的:把chunk按大小分为多个区间,当一个item过来时,看他落在哪个区间,然后存到对应区间的chunk里去。我们启动memcahced看看启动输出:
我们可以看到,默认分配了39类chunk。我们看第一行 slab class 1,chunk大小从96开始 一共有10922个 乘起来刚好是1m。看最后一行slab class 39,chunk大小为524288 一个page只能分2个这样大的chunk。
这里面我们可以看到 chunk大小是按一个系数递增的,这个系数默认为1.25,可以使用-f来修改,第一个chunk大小为96bit,可以使用-n参数来修改。大家通过控制这两个参数就可以控制chunk的区间分布,大家想想什么场景可以用上?后面我们来给几个应用场景。
用我蹩脚的shell写个测试代码,从小到大写入一些value,看看如何分配
看看slabs分布情况
如果上面的概念看得费劲,没关系,我来举个例子:
假设你开了家超市,用100个桶(page)用来装东西,先进了一批苹果,于是你拿出一个桶,在上面贴个标签(chunk)“苹果”,这个桶就只能装苹果。如果一个桶装满了,就再给一个桶贴上标签用来装苹果,直到这批苹果装完。然后又来了一批“香蕉”,于是你又把一些桶贴上梨的标签。直到有一天,你把100个桶的标签都贴完了。现在你进了一些西瓜,这西瓜就没有桶可以装了。
这看起来似乎并不是一个问题,问题在于有一天,“苹果”和“香蕉”都买完了,这时候西瓜还是装不进去
因为所有的桶都打上了“苹果”和“香蕉”的标签,标签没法改,干瞪眼!
这个例子就对应我们的场景一。当之前写入的数据集中在某一chunk时,所有的内存都被分配到了这类slab class。此时需要写入另一个chunk时,已经没有内存再给这类chunk分了,而此时新的数据又只能写入这个区间。导致写入失败
出坑指南一:
这里有一个tips可以缓解这种情况。就是在memcached服务启动时,先自己执行一个init阶段,把各个区间的chunk都至少分配一个page。这样开启lru后,可以保证所有区间chunk可用。缺点是如果问题爆发在大区间chunk,则一个page只能切分成少量的slab,这样会导致频繁过期,影响效率。
在1.5版本中,memcahced已经修改机制,支持slab reassign,解决了此蛋疼问题。详见[1][2]
出坑指南二:
前面我们提到了-f和-n参数可以控制chunk区间,这里我提两个应用场景。
1、小chunk场景:如要记录qq号、微信号这种已知最大长度的数据,现在全球不超过100亿人口,假设每人一个号,则最大不超过12位。这样我们可以把初始值往小的设,递增系数设置很大,大到只分配一个slab,这样就可以缩小区间跨度,节省内存。
如上图,只分配了2种slab
2、大chunk场景:如用户的session。一般随着业务的增长,session里的字段会越来越多,这时候我们可以把初始值设置为现在session的大小,在满足前期需求的情况下,省内存,同时保证后续扩张空间。
注意,chunk是一个数据结构,自身需要消耗48bit,所以如果你的session占用100bit,则初始chunk会分配多少? 我们测试一下
Oops,并不是48+100=148。因为chunk以8bit为最小单位,所以是往上取8的倍数,为152。这也是为什么chunk值并不是严格1.25倍递增的原因。
第二个场景的问题现象展开说一下:
session在应用场景一般存的字段差不多,所以value的大小容易集中在某几个区间,假设为x区间。跑了一段时间后,后台代码增加了session中的一个字段,这种改动很正常,value大小分布到了往上一个区间y,因为之前写入一直是x区间,memcached给x区间分配了99%的内存,只要极少page被划分成了y区间的chunk。
这时忽然一批写入是y区间的,则y区间的写入只会从y区间里面选择key来淘汰,如果设置了过期的y区间key被淘汰完了,则会强制淘汰y区间的key(强制LRU),虽然此时x区间有大量设置了过期时间的key未被淘汰。(分区间淘汰)
这里有几个坑:
1、memcached默认最长过期时间是30天,超过30天的过期配置,会重置为1s。。。
2、如果一个key已经到达过期时间,并不会立即释放内存,而是等到下一个get才会去判断是否过期。这叫惰性过期(lazy expiration)。
3、如果启动时没有设置-M参数,则就算是还没有过期,为了保证新key写进来,也会强制淘汰已经存在的key。。。
出坑指南三:
既然是惰性过期,我们就可以手动构造一下条件触发嘛
在Redis中,我们可以使用keys *命令来获取key。memcached中可以分三步来实现
stats items
stats cachedump $item $num
get $key
如果某个items里面key比较多,可以通过$num参数来少量获取,避免卡顿。设置为0表示全部获取
如上图,get完之后,如果key已经达到过期时间的话,就会淘汰掉
在1.4版本中,memcahed已经支持后台爬虫回收功能,大家去google一个lru_crawler这个关键字,参见[3]
对了,我在测试的时候还发现在一bug,就是通过-m参数配置memcahced使用内存时,为了快速达到实验效果,我设置为2m,但是总是限制不住。
查了下,在小于48m的时候,不太能生效。。
出坑指南四:
这个坑应该很难掉进去吧,咱都21世界了,谁还设64m内存,虽然现在内存爆涨,起码还是给memcached分1G吧,不然就别内存库了,写汇编吧。
one more thing
使用前,可以参考一下源码包doc目录里面的memcached.1、protocal.txt等文件,详细了解对应版本支持的参数和特性,这样可以避免入坑。
[1] https://github.com/memcached/memcached/wiki/ReleaseNotes150
[2] https://blog.elijaa.org/2012/01/17/memcached-1-4-11-released/
[3] http://chenzhenianqing.com/articles/1329.html
- 点赞
- 收藏
- 关注作者
评论(0)