天天看点

【转载】Memcached二三事儿

网络上关于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&gt; memcached -o slab_reassign,slab_automove

在memcached的使用过程中,除了会遇到内存分配机制相关的问题,还有很多稀奇古怪的问题等着你呢,下面我选出几个有代表性的问题来逐一说明:

一般有如下几种解决思路可供选择:

首先,我们可以主动更新cache。前端程序里不涉及重建cache的职责,所有相关逻辑都由后端独立的程序(比如cron脚本)来完成,但此方法并不适应所有的需求。

其次,我们可以通过加锁来解决问题。以php为例,伪代码大致如下:

&lt;!--?php function query() {     $data = $cache---&gt;get($key);

2

3

    if ($cache-&gt;getresultcode() == memcached::res_notfound) {

4

        if ($cache-&gt;add($lockkey, $lockdata, $lockexpiration)) {

5

            $data = $db-&gt;query();

6

            $cache-&gt;add($key, $data, $expiration);

7

            $cache-&gt;delete($lockkey);

8

        } else {

9

            sleep($interval);

10

            $data = query();

11

        }

12

    }

13

14

    return $data;

15

}

16

17

?&gt;

不过这里有一个问题,代码里用到了sleep,也就是说客户端会卡住一段时间,就拿php来说吧,即便这段时间非常短暂,

也有可能堵塞所有的fpm进程,从而使服务中断。于是又有人想出了柔性过期的解决方案,所谓柔性过期,指的是设置一个相对较长的过期时间,

或者干脆不再直接设置数据的过期时间,取而代之的是把真正的过期时间嵌入到数据中去,查询时再判断,如果数据过期就加锁重建,

如果加锁失败,不再sleep,而是直接返回旧数据,以php为例,伪代码大致如下:

&lt;!--?php function query() {     $data = $cache---&gt;get($key);

    if (isset($data['expiration']) &amp;&amp; $data['expiration'] &lt; $now) {        if ($cache-&gt;add($lockkey, $lockdata, $lockexpiration)) {

            $data = $db-&gt;query();

            $data['expiration'] = $expiration;

            $cache-&gt;add($key, $data);

    return $data;

问题到这里似乎已经圆满解决了,且慢!还有一些特殊情况没有考虑到:设想一下服务重启;

或者某个cache里原本没有的冷数据因为某些情况突然转换成热数据;又或者由于lru机制导致某些键被意外删除,等等,

这些情况都可能会让上面的方法失效,因为在这些情况里就不存在所谓的旧数据,等待用户的将是一个空页面。

好在我们还有gearman这根救命稻草。当需要更新cache的时候,我们不再直接查询数据库,而是把任务抛给gearman来处理,

当并发量比较大的时候,gearman内部的优化可以保证相同的请求只查询一次后端数据库,以php为例,伪代码大致如下:

    if ($cache-&gt;getresultcode() == memcached::res_notfound) {

        $data = $gearman-&gt;do($function, $workload, $unique);

        $cache-&gt;add($key, $data, $expiration);

说明:如果多个并发请求的$unique参数一样,那么实际上gearman只会请求一次。

为什么会这样?让我们来模拟一下案发经过,看看到底发生了什么:

我们使用multiget一次性获取100个键对应的数据,系统最初只有一台memcached服务器,随着访问量的增加,系统负载捉襟见肘,于是我们又增加了一台memcached服务器,数据散列到两台服务器上,开始那100个键在两台服务器上各有50个,问题就在这里:原本只要访问一台服务器就能获取的数据,现在要访问两台服务器才能获取,服务器加的越多,需要访问的服务器就越多,所以问题不会改善,甚至还会恶化。

先看看nagle:

再看看delayedacknowledgment:

假如需要确认每一个包的话,那么网络中将会充斥着数不胜数的ack,从而降低了网络性能。为了解决这个问题,delayedacknowledgment规定:不再针对单个包发送ack,而是一次确认两个包,或者在发送响应数据的同时捎带着发送ack,又或者触发超时时间后再发送ack。通过这样的规定,可以降低网络里ack的数量,从而提升网络性能。

nagle和delayedacknowledgment虽然都是好心,但是它们在一起的时候却会办坏事。下面我们看看nagle和delayedacknowledgment是如何产生延迟问题的,如下图所示:

【转载】Memcached二三事儿

客户端需要向服务端传输数据,传输前数据被分为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自有它存在的价值,即便已呈颓势,但是:老兵永远不死,只是慢慢凋零。