网络上关于memcached的资料可以说是浩如烟海,其中不乏一些精彩之作,比如说由爱好者翻译的「memcached全面剖析」系列文章,在中文社区广为流传,虽然已经是几年前的文章了,但是即便现在读起来,依然感觉收获良多,推荐大家多看几遍:
<a href="http://tech.idv2.com/2008/07/10/memcached-001/" target="_blank">memcached的基础</a>
<a href="http://tech.idv2.com/2008/07/11/memcached-002/" target="_blank">理解memcached的内存存储</a>
<a href="http://tech.idv2.com/2008/07/16/memcached-003/" target="_blank">memcached的删除机制和发展方向</a>
<a href="http://tech.idv2.com/2008/07/24/memcached-004/" target="_blank">memcached的分布式算法</a>
<a href="http://tech.idv2.com/2008/07/31/memcached-005/" target="_blank">memcached的应用和兼容程序</a>
一个slab可以有多个page,这就好比在古代一个男人可以娶多个女人;一旦一个page被分给某个slab后,它便对slab至死不渝,犹如古代那些贞洁的女人。但是女人的数量毕竟是有限的,所以一旦一些男人娶得多了,必然另一些男人就只剩下咽口水的份儿,这在很大程度上增加了社会的不稳定因素,于是乎我们要解放女性。
1
shell> memcached -o slab_reassign,slab_automove
…
在memcached的使用过程中,除了会遇到内存分配机制相关的问题,还有很多稀奇古怪的问题等着你呢,下面我选出几个有代表性的问题来逐一说明:
一般有如下几种解决思路可供选择:
首先,我们可以主动更新cache。前端程序里不涉及重建cache的职责,所有相关逻辑都由后端独立的程序(比如cron脚本)来完成,但此方法并不适应所有的需求。
其次,我们可以通过加锁来解决问题。以php为例,伪代码大致如下:
<!--?php function query() { $data = $cache--->get($key);
2
3
if ($cache->getresultcode() == memcached::res_notfound) {
4
if ($cache->add($lockkey, $lockdata, $lockexpiration)) {
5
$data = $db->query();
6
$cache->add($key, $data, $expiration);
7
$cache->delete($lockkey);
8
} else {
9
sleep($interval);
10
$data = query();
11
}
12
}
13
14
return $data;
15
}
16
17
?>
不过这里有一个问题,代码里用到了sleep,也就是说客户端会卡住一段时间,就拿php来说吧,即便这段时间非常短暂,
也有可能堵塞所有的fpm进程,从而使服务中断。于是又有人想出了柔性过期的解决方案,所谓柔性过期,指的是设置一个相对较长的过期时间,
或者干脆不再直接设置数据的过期时间,取而代之的是把真正的过期时间嵌入到数据中去,查询时再判断,如果数据过期就加锁重建,
如果加锁失败,不再sleep,而是直接返回旧数据,以php为例,伪代码大致如下:
<!--?php function query() { $data = $cache--->get($key);
if (isset($data['expiration']) && $data['expiration'] < $now) { if ($cache->add($lockkey, $lockdata, $lockexpiration)) {
$data = $db->query();
$data['expiration'] = $expiration;
$cache->add($key, $data);
return $data;
问题到这里似乎已经圆满解决了,且慢!还有一些特殊情况没有考虑到:设想一下服务重启;
或者某个cache里原本没有的冷数据因为某些情况突然转换成热数据;又或者由于lru机制导致某些键被意外删除,等等,
这些情况都可能会让上面的方法失效,因为在这些情况里就不存在所谓的旧数据,等待用户的将是一个空页面。
好在我们还有gearman这根救命稻草。当需要更新cache的时候,我们不再直接查询数据库,而是把任务抛给gearman来处理,
当并发量比较大的时候,gearman内部的优化可以保证相同的请求只查询一次后端数据库,以php为例,伪代码大致如下:
if ($cache->getresultcode() == memcached::res_notfound) {
$data = $gearman->do($function, $workload, $unique);
$cache->add($key, $data, $expiration);
说明:如果多个并发请求的$unique参数一样,那么实际上gearman只会请求一次。
为什么会这样?让我们来模拟一下案发经过,看看到底发生了什么:
我们使用multiget一次性获取100个键对应的数据,系统最初只有一台memcached服务器,随着访问量的增加,系统负载捉襟见肘,于是我们又增加了一台memcached服务器,数据散列到两台服务器上,开始那100个键在两台服务器上各有50个,问题就在这里:原本只要访问一台服务器就能获取的数据,现在要访问两台服务器才能获取,服务器加的越多,需要访问的服务器就越多,所以问题不会改善,甚至还会恶化。
先看看nagle:
再看看delayedacknowledgment:
假如需要确认每一个包的话,那么网络中将会充斥着数不胜数的ack,从而降低了网络性能。为了解决这个问题,delayedacknowledgment规定:不再针对单个包发送ack,而是一次确认两个包,或者在发送响应数据的同时捎带着发送ack,又或者触发超时时间后再发送ack。通过这样的规定,可以降低网络里ack的数量,从而提升网络性能。
nagle和delayedacknowledgment虽然都是好心,但是它们在一起的时候却会办坏事。下面我们看看nagle和delayedacknowledgment是如何产生延迟问题的,如下图所示:
客户端需要向服务端传输数据,传输前数据被分为abcd四个包,其中abc三个包的大小都是mss,而d的大小则小于mss。客户端和服务端的交互过程如下所示:
首先,因为客户端的ab两个包的大小都是mss,所以它们可以耗无障碍的发送,服务端因为delayedacknowledgment的缘故,会把这两个包放在一起来发送ack。
接着,客户端发送c包,而d包由于小于mss,所以不会立即发送,而被放到缓冲区里延迟发送,服务端因为delayedacknowledgment的缘故,不会单独确认c包,于是就傻傻的等啊等,等到花儿都谢了,最终触发了超时时间,发送了姗姗来迟的ack。
最后,客户端收到ack后,因为没有未被确认的包存在了,所以即便d包小于mss,也总算熬出头了,可以发送了,服务端在收到了所有的包之后就可以发送响应数据了。
说到这里,假如你认为自己已经理解了这个问题的来龙去脉,那么我们尝试改变一下前提条件:传输前数据被分为abcde五个包,其中abcd四个包的大小都是mss,而e的大小则小于mss。换句话说,满足mss的完整包的个数是偶数个,而不是前面所说的奇数个,此时又会出现什么情况呢?答案我就不说了,留给大家自己思考。
知道了问题的原委,解决起来就简单了:我们只要设置socket选项为tcp_nodelay即可,这样就可以禁用nagle,如此一来,少了一个巴掌,问题自然就拍不响了。
希望本文能让大家在使用memcached的过程中少走一些弯路。相对于memcached,其实我更喜欢redis,从功能上看,redis可以说是memcached的超集,不过memcached自有它存在的价值,即便已呈颓势,但是:老兵永远不死,只是慢慢凋零。