Memcached絕對稱得上是NoSQL老兵!可惜隨著時間的推移,Redis等后起之秀羽翼漸豐,Memcached相比之下已呈頹勢。那我們還用不用學習它?答案是肯定的!畢竟仍然有很多項目依賴著它,如果忽視它,一旦出了問題就只有干瞪眼的份兒了。 網絡上關于Memcached的資
Memcached絕對稱得上是NoSQL老兵!可惜隨著時間的推移,Redis等后起之秀羽翼漸豐,Memcached相比之下已呈頹勢。那我們還用不用學習它?答案是肯定的!畢竟仍然有很多項目依賴著它,如果忽視它,一旦出了問題就只有干瞪眼的份兒了。
網絡上關于Memcached的資料可以說是浩如煙海,其中不乏一些精彩之作,比如說由愛好者翻譯的「Memcached全面剖析」系列文章,在中文社區廣為流傳,雖然已經是幾年前的文章了,但是即便現在讀起來,依然感覺收獲良多,推薦大家多看幾遍:
當然,官方Wiki永遠是最權威的資料,即便是里面的ReleaseNotes也不要放過。
實際應用Memcached時,我們遇到的很多問題都是因為不了解其內存分配機制所致,下面就讓我們以此為開端來開始Memcached之旅吧!
為了規避內存碎片問題,Memcached采用了名為SlabAllocator的內存分配機制。內存以Page為單位來分配,每個Page分給一個特定長度的Slab來使用,每個Slab包含若干個特定長度的Chunk。實際保存數據時,會根據數據的大小選擇一個最貼切的Slab,并把數據保存在對應的Chunk中。如果某個Slab沒有剩余的Chunk了,系統便會給這個Slab分配一個新的Page以供使用,如果沒有Page可用,系統就會觸發LRU機制,通過刪除冷數據來為新數據騰出空間,這里有一點需要注意的是:LRU不是全局的,而是針對Slab而言的。
一個Slab可以有多個Page,這就好比在古代一個男人可以娶多個女人;一旦一個Page被分給某個Slab后,它便對Slab至死不渝,猶如古代那些貞潔的女人。但是女人的數量畢竟是有限的,所以一旦一些男人娶得多了,必然另一些男人就只剩下咽口水的份兒,這在很大程度上增加了社會的不穩定因素,于是乎我們要解放女性。
好在Memcached已經意識到解放女性的重要性,新版本中Page可以調配給其它的Slab:
shell> memcached -o slab_reassign,slab_automove
換句話說:女人可以改嫁了!這方面,其實Memcached的兒子Twemcache革命得更徹底,他甚至寫了一篇大字報,以事實為依據,痛斥老子的無能,有興趣的可以繼續閱讀:Random Eviciton vs Slab Automove。
了解Memcached內存使用情況的最佳工具是:Memcached-tool。如果我們發現某個Slab的Evicted不為零,則說明這個Slab已經出現了LRU的情況,這通常是個危險的信號,但也不能一概而論,需要結合Evict_Time來做進一步判斷。
…
在Memcached的使用過程中,除了會遇到內存分配機制相關的問題,還有很多稀奇古怪的問題等著你呢,下面我選出幾個有代表性的問題來逐一說明:
通常我們會為兩種數據做Cache,一種是熱數據,也就是說短時間內有很多人訪問的數據;另一種是高成本的數據,也就說查詢很很耗時的數據。當這些數據過期的瞬間,如果大量請求同時到達,那么它們會一起請求后端重建Cache,造成擁堵問題,就好象在北京上班做地鐵似的,英文稱之為:stampeding herd,老外這里的用詞還是很形象的。
一般有如下幾種解決思路可供選擇:
首先,我們可以主動更新Cache。前端程序里不涉及重建Cache的職責,所有相關邏輯都由后端獨立的程序(比如CRON腳本)來完成,但此方法并不適應所有的需求。
其次,我們可以通過加鎖來解決問題。以PHP為例,偽代碼大致如下:
get($key); if ($cache->getResultCode() == Memcached::RES_NOTFOUND) { if ($cache->add($lockKey, $lockData, $lockExpiration)) { $data = $db->query(); $cache->set($key, $data, $expiration); $cache->delete($lockKey); } else { sleep($interval); $data = query(); } } return $data; } ?>
不過這里有一個問題,代碼里用到了sleep,也就是說客戶端會卡住一段時間,就拿PHP來說吧,即便這段時間非常短暫,也有可能堵塞所有的FPM進程,從而使服務中斷。于是又有人想出了柔性過期的解決方案,所謂柔性過期,指的是設置一個相對較長的過期時間,或者干脆不再直接設置數據的過期時間,取而代之的是把真正的過期時間嵌入到數據中去,查詢時再判斷,如果數據過期就加鎖重建,如果加鎖失敗,不再sleep,而是直接返回舊數據,以PHP為例,偽代碼大致如下:
get($key); if (isset($data['expiration']) && $data['expiration'] < $now) { if ($cache->add($lockKey, $lockData, $lockExpiration)) { $data = $db->query(); $data['expiration'] = $expiration; $cache->set($key, $data); $cache->delete($lockKey); } } return $data; } ?>
問題到這里似乎已經圓滿解決了,且慢!還有一些特殊情況沒有考慮到:設想一下服務重啟;或者某個Cache里原本沒有的冷數據因為某些情況突然轉換成熱數據;又或者由于LRU機制導致某些鍵被意外刪除,等等,這些情況都可能會讓上面的方法失效,因為在這些情況里就不存在所謂的舊數據,等待用戶的將是一個空頁面。
好在我們還有Gearman這根救命稻草。當需要更新Cache的時候,我們不再直接查詢數據庫,而是把任務拋給Gearman來處理,當并發量比較大的時候,Gearman內部的優化可以保證相同的請求只查詢一次后端數據庫,以PHP為例,偽代碼大致如下:
get($key); if ($cache->getResultCode() == Memcached::RES_NOTFOUND) { $data = $gearman->do($function, $workload, $unique); $cache->set($key, $data, $expiration); } return $data; } ?>
說明:如果多個并發請求的$unique參數一樣,那么實際上Gearman只會請求一次。
Facebook在Memcached的實際應用中,發現了Multiget無底洞問題,具體表現為:出于效率的考慮,很多Memcached應用都已Multiget操作為主,隨著訪問量的增加,系統負載捉襟見肘,遇到此類問題,直覺通常都是通過增加服務器來提升系統性能,但是在實際操作中卻發現問題并不簡單,新加的服務器好像被扔到了無底洞里一樣毫無效果。
為什么會這樣?讓我們來模擬一下案發經過,看看到底發生了什么:
我們使用Multiget一次性獲取100個鍵對應的數據,系統最初只有一臺Memcached服務器,隨著訪問量的增加,系統負載捉襟見肘,于是我們又增加了一臺Memcached服務器,數據散列到兩臺服務器上,開始那100個鍵在兩臺服務器上各有50個,問題就在這里:原本只要訪問一臺服務器就能獲取的數據,現在要訪問兩臺服務器才能獲取,服務器加的越多,需要訪問的服務器就越多,所以問題不會改善,甚至還會惡化。
不過,作為被告方,Memcached官方開發人員對此進行了辯護:
請求多臺服務器并不是問題的癥結,真正的原因在于客戶端在請求多臺服務器時是并行的還是串行的!問題是很多客戶端,包括Libmemcached在內,在處理Multiget多服務器請求時,使用的是串行的方式!也就是說,先請求一臺服務器,然后等待響應結果,接著請求另一臺,結果導致客戶端操作時間累加,請求堆積,性能下降。
如何解決這個棘手的問題呢?只要保證Multiget中的鍵只出現在一臺服務器上即可!比如說用戶名字(user:foo:name),用戶年齡(user:foo:age)等數據在散列到多臺服務器上時,不應按照完整的鍵名(user:foo:name和user:foo:age)來散列的,而應按照特殊的鍵(foo)來散列的,這樣就保證了相關的鍵只出現在一臺服務器上。以PHP的 Memcached客戶端為例,有getMultiByKey和setMultiByKey可供使用。
老實說,這個問題和Memcached沒有半毛錢關系,任何網絡應用都有可能會碰到這個問題,但是鑒于很多人在寫Memcached程序的時候會遇到這個問題,所以還是拿出來聊一聊,在這之前我們先來看看Nagle和DelayedAcknowledgment的含義:
先看看Nagle:
假如需要頻繁的發送一些小包數據,比如說1個字節,以IPv4為例的話,則每個包都要附帶40字節的頭,也就是說,總計41個字節的數據里,其中只有1個字節是我們需要的數據。為了解決這個問題,出現了Nagle算法。它規定:如果包的大小滿足MSS,那么可以立即發送,否則數據會被放到緩沖區,等到已經發送的包被確認了之后才能繼續發送。通過這樣的規定,可以降低網絡里小包的數量,從而提升網絡性能。
再看看DelayedAcknowledgment:
假如需要單獨確認每一個包的話,那么網絡中將會充斥著無數的ACK,從而降低了網絡性能。為了解決這個問題,DelayedAcknowledgment規定:不再針對單個包發送ACK,而是一次確認兩個包,或者在發送響應數據的同時捎帶著發送ACK,又或者觸發超時時間后再發送ACK。通過這樣的規定,可以降低網絡里ACK的數量,從而提升網絡性能。
Nagle和DelayedAcknowledgment雖然都是好心,但是它們在一起的時候卻會辦壞事。下面我們舉例說說Nagle和DelayedAcknowledgment是如何產生延遲問題的:
Nagle和DelayedAcknowledgment的延遲問題
客戶端需要向服務端傳輸數據,傳輸前數據被分為ABCD四個包,其中ABC三個包的大小都是MSS,而D的大小則小于MSS,交互過程如下:
首先,因為客戶端的ABC三個包的大小都是MSS,所以它們可以耗無障礙的發送,服務端由于DelayedAcknowledgment的存在,會把AB兩個包放在一起來發送ACK,但是卻不會單獨為C包發送ACK。
接著,因為客戶端的D包小于MSS,并且C包尚未被確認,所以D包不會立即發送,而被放到緩沖區里延遲發送。
最后,服務端觸發了超時閾值,終于為C包發送了ACK,因為不存在未被確認的包了,所以即便D包小于MSS,也總算熬出頭了,可以發送了,服務端在收到了所有的包之后就可以發送響應數據了。
說到這里,假如你認為自己已經理解了這個問題的來龍去脈,那么我們嘗試改變一下前提條件:傳輸前數據被分為ABCDE五個包,其中ABCD四個包的大小都是MSS,而E的大小則小于MSS。換句話說,滿足MSS的完整包的個數是偶數個,而不是前面所說的奇數個,此時又會出現什么情況呢?答案我就不說了,留給大家自己思考。
知道了問題的原委,解決起來就簡單了:我們只要設置socket選項為TCP_NODELAY即可,這樣就可以禁用Nagle,以PHP為例:
setOption(Memcached::OPT_TCP_NODELAY, true); ?>
如果大家意猶未盡,可以繼續瀏覽:TCP Performance problems caused by interaction between Nagle’s Algorithm and Delayed ACK。
…
希望本文能讓大家在使用Memcached的過程中少走一些彎路。相對于Memcached,其實我更喜歡Redis,從功能上看,Redis可以說是Memcached的超集,不過Memcached自有它存在的價值,即便已呈頹勢,但是:老兵永遠不死,只是慢慢凋零。
原文地址:Memcached二三事兒, 感謝原作者分享。
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com