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