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