memcached批量删除方案探讨
2008-08-22, FigoMemcached 是开源的分布式cache系统,现在很多的大型web应用程序包括facebook,youtube,wikipedia,yahoo 等等都在使用memcached来支持他们每天数亿级的页面访问。通过把cache层与他们的web架构集成,他们的应用程序在提高了性能的同时,还大大 降低了数据库的负载。
如果您还对memcached还不是很了解,请先阅读:
本文中我们将集中探讨如何对memcached中存储的cache进行高效管理。在我们实际项目中,由于网站经常需要更新,而cache难免会发生与数据库不同步却仍然还有效的情况,那网站管理员就有需求对某个或某些cache进行手工清理。
比如,大众点评的每个商户页面都利用了cache,并存成键值为Shop.[ShopID]的cache对象。如果一个商户ID为1234的页面cache有问题,我们就可以通过Remove(”Shop.1234”)方法通知memcached清掉对应的cache对象。
但问题又随之而来了,有时候我们发现有一批cache都存在问题而没有及时过期。此时我们会面临一个两难的选择:
- 通过flush_all清掉所有的cache
这种做法很黄很暴力…把有问题和没问题都一起kill掉了…
- 找出这一批cache中所有的key值并逐一进行清理
这种做法看似不错,但没有好的办法实现。因为memcached是hash表结构,无法提供像sql里select from where的操作,所以你不知道这批数据到底有多少已经存进了cache
暴力法我们肯定是不能主动考虑的,否则也不用这篇文章了,呵呵。所以我们看看有没有可行的方案来实现方法2。
LogDB方案
这个方案说起来也很简单。既然memcached没有select操作,我们就用db去记录所有的Cache SET操作。只要memcached存了一个cache对象进instance,就同时往logdb上写一条记录。
等到要删除的时候,我们就通过select操作,找出所有Shop.xxxx的商户,并逐一进行remove操作。
这种办法虽然可以达到精确删除的目的,但是所消耗的代价也未免有些过高,为了清除key还要另外拉个数据库进来做辅助。而且cache的存取量是很大的,说不定数据库频繁的insert操作还会成为另一个潜在的性能瓶颈。此方案在硬件条件不是很充裕的条件下谨慎使用。
自定义keylocator方案
了解memcached的读者都知道,cache对象分配到哪个instance是由memcached客户端决定的。而memcached客户端 默认的分配算法是SHA-1散列算法。所以默认情况下,商户类的cache对象(Shop.xxxx)是被散列到不同的memcached instance上的。
(X:User100,Shop202 Y:User101,Shop200 Z:User102,Shop201)
假如我们可以自定义这个分配算法,将Shop.xxx类的cache对象都同一存到某一个instance或某几个instance下,这样我们就通过清掉这些Shop.xxx专用的instance来达到批量清楚的目的。
(X:User100 Y:Shop200,Shop201,Shop202 Z:User101,User102)
这个方案看似不错,其实还是有不少衍生问题的:
- 使用率不均问题:
从上面两个例子就可以看到,前面通过散列算法存放,每个instance都放了两个对象,但是采用自定义的办法,instance Y就存了3个,X才存了1个。如果自定义的locator在实现之前不能够通盘考虑各类key的使用率的话,很容易造成instance使用不均的问题。
- 负载率不均问题:
有些热门类的key读取率很高,因为原本是散列在各个服务器上,读取负载也就自然地分解到各个服务器上。现在采用专用服务器模式的话,所有的读取操作就限制在某一台服务器上,无疑是加大了这台服务器的负担。
Key flag 方案
这个方案是眼下笔者认为最简单有效的一个办法,通过在key上做标记来实现懒清理。其实这种做法也是很合memcached的胃口的,之前的文章就介绍过,memcached server的清理策略就是懒清理。
让我们看一下实际的例子:
我们在所有的key后面都带一个版本标记,“Shop.200_1”代表商户ID为200,版本号为1的缓存。一旦有需要对所有的商户类缓存进行清理的话,我们只需要升级一下Shop类缓存的版本就可以做到。
上例中,商户类缓存版本都升级到2了,读取的key值也发生了变化。所以原来的Shop.xxx_1的缓存就永远不会再被读取到,也就等同于失效了。
这样做还有个好处就是,不需要进行批量删除的操作,而耗费大量的维护时间。那些过时版本的cache会根据LRU原则,只要cache一满,他们就会自动被剔除,不需要我们还劳神劳力地去伺候他们。
所以,在memcached这个圈子里也有个原则就是,不用的话就别去管它…它自己会消失的。
说道这个方案不足的话,我想也就是key的长度不得不进行加长来包含版本号,不过这方面会带来的性能影响是微乎其微的。
memcached的批量清理一直是个比较麻烦的问题,希望以上几种方案的探讨能给您带来一些启发。
<script type="text/javascript">SHARETHIS.addEntry({ title: "memcached批量删除方案探讨", url: "http://it.dianping.com/memcached_item_batch_del.htm" });</script>
有10条点评
懒清理虽然好用,但是这必然导致每次清空缓存的时候,版本号也随之变大、变长,时间一长,管理起来会不会有问题?
这方面我有考虑过,我们可以建立一个循环,即版本只有0-9,9之后再升级就是0了,因为升级后,老版本cache会在3-24小时之内消失,等到全部消失后,老的这个版本号就可以再一次被启用了。
找回密码的邮件应该立即发送,否则就失去了意义。象昨天晚上我找回密码,今天才收到,有点太慢了吧。
发送队列难道不能把这个的优先级设高点?
Key flag这个方案还是不错的,不过有一个缺陷,内存浪费。在memcached内存已占满的情况下,放入大量新的缓存项,肯定会导致一些缓存项被释放,按LRU规则,不一定清掉什么内容。
这个可以对比下批量清除前后一段时间(比如一小时)的cache hit ratio,看一下影响有多大。
可以看一下BeIT的memcached Client API中有没有用到flag。
.net本身没有缓存解决方案么?为什么要用开源的memcached?
现在微软有个CTP的方案,叫做Velocity
http://www.microsoft.com/downloads/details.aspx?FamilyId=B24C3708-EEFF-4055-A867-19B5851E7CD2&displaylang=en
我们用的时候还没有这个解决方案,况且mc是在很多大站经过实际考验的,况且客户端很丰富,跨平台很方便。
.net暂时没有成熟的分布式缓存解决方案,酒精也说了,velocity只是ctp,姑且关注着吧
请问 版本号 如何处理?
假 设每种key都有一个名字,比如商户种类的缓存叫shop,然后shop1234_0,代表id为1234,版本号为0的商户的缓存的key,一但你要清 除所有商户的缓存,你可以升级版本,那读取的key都变成了shop1234_1,因为原来没有shop1234_1的缓存,所以等于是对缓存进行了刷 新,然后shop1234_0的缓存超过时间会自动消失的,也就懒清除的办法
如果仅仅是升级了版本,却不删除掉老版本的话,无疑会增加MS对内存的占用。
可以在升级版本的时候,把老析版Remove掉。
比如你要“升级”到“shop1234_1”时候,就顺手把“shop1234_0”给踢除掉。