
图2 UDP包头
Memcached使用技巧
数据转储
Memcached的实用价值在于其访问速度。但是单靠它最终无法构建一个完整的数据持久层。Memcached中存储的数据无法长期断电保存。因此在Memcached之外需要自己制作数据转储程序。据本人了解市面上有一款MemcacheDB的数据持久框架[2]。它是采用了new BSD license协议,由新浪开发的Memcached改进框架。在Memcached作为前端缓存的前提下集合了Berkeley DB作为持久存储组件。也就是其在框架内实现了数据转储的功能。看相关博客介绍号称是钢铁般的品质,不过本人没有什么研究不过多评论。
数据转存的步骤:
1. 获取Memcached中所有项目的keyset。
2. 遍历keyset,逐个key访问Memcached获取其value。
3. 调用DB接口将项目存储到Database中。
其中存在的问题是,如何获取Memcached中所有项目的keyset?
有人说Memcached有命令stats cachedump,这个命令可以获取Memcached一个指定的内存槽中存储的所有数据的keyset。
这个方法固然还不错,但是,stats cachedump命令的返回包是有大小限制的,一般来说能返回50000个左右的key。如果你存储在Memcached中的key远小于50000时倒是可以试一试,但是对较大型的Web应用来说,这一点很不靠谱。并且 stats cachedump采用的算法效率低下。
一个替代方案是在另一个地方存储你的keyset。

图3 keyset的存储设计
理论上说Memcached中存储的所有数据都是上层逻辑写入的。假设我们有一个DataCenter模块,所有上层的逻辑都调用DataCenter暴露出去的set和get方法对数据进行存储,那么我们也可以在DataCenter中找到一个合适的内存位置存放上层每次访问的key,组成keyset。也就是由上层逻辑自己负责keyset的存储,这样虽然显得逻辑层次不够清楚,但是在内存中操作keyset使得它效率足够高。
如果项目的架构像图4所示的一样(比图3更实际的情况),也即全局存在多个Database、Memcached、上层server的结构。那么上文中所提到的由上层逻辑server来管理keyset的方法将无法使用,因为第一你无法知道玩家每次都从哪一个server登录,第二每一台server都存着keyset也就意味着数据转存服务器需要在转存的时候连接所有的server获取keyset,即耗时又不好统一管理。找个好位置存每个Memcached的keyset,并且要保证你的转存程序能够访问到这些keyset。例如自己编写一个独立的key server。如此一来,存储数据的流程中变增加了一项:向key server中存储key。坏处是每次存储操作都多进行了一次网络I/O处理,好处是key server完全独立于上层逻辑server和底层Memcached server。逻辑独立,并且日后的服务器扩展也可以做的更加轻松。

图4实际项目更可能出现的情况
并行访问时数据的同步
Memcached只有一个,但是其对应的上层访问线程可能不止一个。
例如:
item1 = 0
线程A:get item1 // item1 = 0
线程B:get item1 // item1 = 0
线程A:item1++; set item1 // item1 = 1
线程B:item1–; set item1 // item1 = -1
最终Memcached中item1的值是什么取决于线程A和B到底哪个后写入Memcached。线程A和线程B就像是两个上层Server,它们都以为自己获取的是最新的Memcached数据,但是实际上它们并不知道这个值在别的地方已经被修改过了。
这种问题可以通过加锁解决,很多人听到加锁就皱眉头,加锁并不意味着程序效率难以接受,处理的重点在于对什么东西加锁和对什么样的操作加锁。
针对数据加悲观锁,那么在占用线程释放此数据之前别的线程是无法访问该数据的。从底层否定了并行的可能性,但同时安全性也最高。
针对修改添加标记,例如Memcached中提供的命令cas和gets。不同于普通的set和get,cas在set之后还会修改一个数据对应的version值。每一次修改都会将这个version值加1,。同时,修改之前也会判断数据修改者所持有的version是否有权令其修改该数据。(较小的version持有者将无法修改该数据,只能重新获取。)
至于最终使用什么方法,我的建议是根据不同的业务逻辑情况分类处理。想要一刀切的话固然简单方便,但是效率和适用性会受影响。甚至可以说根据业务的特性,如果可以带来大量的性能提升,牺牲一些安全性也未尝不可(这一点很重要,不是所有的数据都需要你的层层保护,有时候性能更加重要)。
故障处理
Memcached的数据是存储在内存中的,直到这些数据被转存到磁盘之前,它们都很不完全。一台物理服务器可能会出现各种各样的故障情况,网络断开,硬盘损坏,服务器电源松动(如果检测人员每天都需要漫步在服务器机房里,这个不是天方夜谭)。同时如果项目应用所占有的服务器数量众多,那么整体的故障几率会更高。
同时,不同的故障对系统带来的影响也不近相同。
网络断开:Memcached中存储的数据不会丢失。如果是上层服务和Memcached之间网络断开,那么上层服务需要将数据缓存下来,恢复连接之后立刻写入Memcached(此处省略了无数需要详细考虑的问题,缓存的同时也会带来数据同步和上层服务器内存容量的问题… …)。如果是Memcached和数据转存程序之间的网络断开,这个影响并不算大,保证网络连接尽快恢复即可(不得不说的是Memcached容量也是有限的,持续写入数据而不将它们存入数据库中,旧有内容很可能被替换)。
Memcached机器硬损坏:这种尴尬的情况就不需要多说什么了,老天用雷劈你你能如何。用户数据回档时间取决于你数据转存程序的运行周期… …好在发生几率不大。
服务器电源松动:同机器硬损坏(同时你可以有机会暴揍一顿某人)。这也从另一个角度对各位设计的数据转存效率提出了要求,数据转存的周期越小,用户数据的损失也越小。
Memcached服务器组的维护
假设我们面对的是一个真正大型的网络应用,全局将会存在多个Memcached服务器。那么,第一,每一个Memcached负责的数据范围是我们需要设计的。第二,当某个Memcached内存吃紧时,如何扩容也是一个问题。
考虑一下下面的这个情形:
假设我们将所有男性用户的数据存在Memcached1中,女性存在Memcached2中。应用开始没多久,我们就发现由于男性的数据远大于女性数据,Memcached1的内存占用超过80%并开始报警。
我们需要进行的操作是:开启新的Memcached服务器Memcached3,将男性数据部分划分到Memcached3中。
于是我们开启了服务器Memcached3,同时也做出了划分,30岁以下的男性用户数据将继续存储在Memcached1中,30岁以上的男性数据将由Memcached3处理。
通过动态修改Memcached地址解析配置,现在所有的上层应用在面对新的男性大叔数据的时候都会定位到Memcached3中(旧有的Memcached1中的男性大叔数据怎么办?什么情况下访问Memcached3,什么情况下访问Memcached1?这些都是各位需要考虑的问题)。
如此便是一次简单的Memcached扩容过程,同理的还有针对某个Memcached服务器的停止操作。
个人感悟
所谓的额外劳动换取大量效能提升,其中的“额外劳动”也会是大量的… …这也正是等价交换之原理。
一个完整的程序架构可能是图5这样的:

图5一个可能完整的程序架构
加入Memcached对数据持久层的代码结构所可能带来的影响有:
1. 增加一个数据转存程序。其定时连接Memcached,获取所有的数据,拉入到Database中。
2. 增加MCClient。这个是提供给上层逻辑的数据存取接口,这个Client需要将上层的访问转换成Memcached协议,并与Memcached通信。现在MCClient针对各种语言已经有很多开源的实现了,但是这些开源包都只是实现了Memcached的基本协议。就我上面所提到的转存,同步等问题,还需要各位针对项目自己考虑完善了。
3. 一系列的Memcached维护工具,以及数据持久层错误应急处理方案。这个才是最麻烦的东西,Memcached网断了怎么办,MemcachedClient线程挂了怎么办,是否支持某些数据跳过Memcached直接即时存储Database,数据转存程序连接不上Database怎么办,数据转存程序进程挂了怎么办?… …当然了你也可以不考虑这些或者其中的部分问题,但假如你要制作一个面向百万在线用户的应用… …一切都需要详细规划。
参考
[1]What is memcached:http://code.google.com/p/memcached/wiki/NewOverview
[2] MemcacheDB官网:http://memcachedb.org/