天天看點

【轉載】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自有它存在的價值,即便已呈頹勢,但是:老兵永遠不死,隻是慢慢凋零。