在之前的文章中,我們分享了 Mailbox如何在六星期實(shí)現(xiàn)從零到百萬(wàn)用戶及日處理億條消息。其中我們提過(guò)Mailbox以14個(gè)人的小團(tuán)隊(duì),在6個(gè)星期內(nèi)實(shí)現(xiàn)0到百萬(wàn)用戶的壯舉,而服務(wù)日承載信息破億條。隨后在App發(fā)布不到3周,他們將自己以1億美元的價(jià)格賣(mài)給了Dropbox。
在之前的文章中,我們分享了 Mailbox如何在六星期實(shí)現(xiàn)從零到百萬(wàn)用戶及日處理億條消息。其中我們提過(guò)Mailbox以14個(gè)人的小團(tuán)隊(duì),在6個(gè)星期內(nèi)實(shí)現(xiàn)0到百萬(wàn)用戶的壯舉,而服務(wù)日承載信息破億條。隨后在App發(fā)布不到3周,他們將自己以1億美元的價(jià)格賣(mài)給了Dropbox。這次我們帶來(lái)的是,Mailbox在快速擴(kuò)展過(guò)程中,MongoDB所遭遇的性能瓶頸及解決途徑。
以下為譯文:
在Mailbox快速擴(kuò)展過(guò)程中,其中一個(gè)性能問(wèn)題就是MongoDB的數(shù)據(jù)庫(kù)級(jí)別寫(xiě)鎖,在鎖等待過(guò)程中耗費(fèi)的時(shí)間,直接反應(yīng)到用戶使用服務(wù)過(guò)程中的延時(shí)。為了解決這個(gè)長(zhǎng)期存在的問(wèn)題,我們決定將一個(gè)常用的MongoDB集合(儲(chǔ)存了郵件相關(guān)數(shù)據(jù))遷移到獨(dú)立的集群上。根據(jù)我們推斷,這將減少50%的鎖等待時(shí)間;同時(shí),我們還可以添加更多的分片,我們還期望可以獨(dú)立的優(yōu)化及管理不同類(lèi)型數(shù)據(jù)。
我們首先從MongoDB文檔開(kāi)始,很快的就發(fā)現(xiàn)了 cloneCollection命令。然而隨后悲劇的發(fā)現(xiàn),它不可以在分片集合中使用;同樣, renameCollection也不能在分片集合中使用。在否定了其它可能性之后(基于性能問(wèn)題),我們編寫(xiě)了一個(gè)Python腳本用以復(fù)制數(shù)據(jù),和另一個(gè)用于比較原始和目標(biāo)數(shù)據(jù)的腳本。在這個(gè)過(guò)程中,我們還發(fā)現(xiàn)了許多有意思的事情,比如 gevent及 pymongo復(fù)制大數(shù)據(jù)集的時(shí)間是 mongodump(C++編寫(xiě))的一半,即使MongoDB客戶端和服務(wù)器在同臺(tái)主機(jī)上。通過(guò)最終努力,我們開(kāi)發(fā)了 Hydra,用于MongoDB遷移的工具集,現(xiàn)已開(kāi)源。首先,我們建立了MongoDB集合的原始快照。
問(wèn)題1:悲劇的性能
早期我做了一個(gè)實(shí)驗(yàn)以測(cè)試MongoDB API運(yùn)作所能達(dá)到的極限速度——啟用一個(gè)簡(jiǎn)單的使用MongoDB C++ 軟件開(kāi)發(fā)工具包的速度。一方面對(duì)C++ 感覺(jué)厭煩,一方面希望我大多數(shù)熟練使用Python的同事可以在其他用途上使用或適應(yīng)這種代碼,我沒(méi)有更進(jìn)一步的探索C++的使用,而是發(fā)現(xiàn),如果是針對(duì)少量數(shù)據(jù),在處理相同任務(wù)上,簡(jiǎn)單的C++應(yīng)用速度是簡(jiǎn)單Python應(yīng)用的5-10倍。
所以,我的研究方向回到了Python,這個(gè)Dropbox默認(rèn)語(yǔ)言。此外,進(jìn)行了諸如對(duì)mongod查詢等的一系列遠(yuǎn)程網(wǎng)絡(luò)請(qǐng)求時(shí),客戶端往往需要耗費(fèi)大量時(shí)間等待服務(wù)器響應(yīng);似乎也沒(méi)有很多copy_collection.py (我的MongoDB集合復(fù)制工具)需要的CPU密集型操作(部分)。initialcopy_collection.py占很少的CPU使用率也證實(shí)了這一點(diǎn)。
然后,MongoDB請(qǐng)求到copy_collection.py.。最初的工作線程實(shí)驗(yàn)結(jié)果并不理想。但接下來(lái),我們通過(guò)Python Queue對(duì)象來(lái)實(shí)現(xiàn)工作線程通信。這樣的性能依舊不是很好,因?yàn)镮PC上的開(kāi)銷(xiāo)讓并發(fā)帶來(lái)的提升黯然失色。使用Pipes和其他IPC機(jī)制也并沒(méi)有多大幫助。
接下來(lái),我們嘗試了使用單線程Python進(jìn)行MongoDB異步查詢,看看可以有多少性能結(jié)余。其中Gevent是實(shí)現(xiàn)這個(gè)途徑常用庫(kù)之一,我們對(duì)它進(jìn)行了嘗試。Gevent 修改了標(biāo)準(zhǔn)Python模塊以實(shí)現(xiàn)異步操作,比如socket。比較好的一點(diǎn)是,你可以簡(jiǎn)單的編寫(xiě)異步讀取代碼,就像同步代碼一樣。
通常情況下,兩個(gè)集合之間復(fù)制文檔的異步代碼會(huì)是:
import asynclib def copy_documents(source_collection, destination_collection, _ids, callback): """ Given a list of _id's (MongoDB's unique identifier field for each document), copies the corresponding documents from the source collection to the destination collection """ def _copy_documents_callback(...): if error_detected(): callback(error) # copy documents, passing a callback function that will handle errors and # other notifications for _id in _ids: copy_document(source_collection, destination_collection, _id, _copy_documents_callback) # more error handling omitted for brevity callback(None) def copy_document(source_collection, destination_collection, _id, callback): """ Copies document corresponding to the given _id from the source to the destination. """ def _insert_doc(doc): """ callback that takes the document read from the source collection and inserts it into destination collection """ if error_detected(): callback(error) destination_collection.insert(doc, callback) # another MongoDB operation # find the specified document asynchronously, passing a callback to receive # the retrieved data source_collection.find_one({'$id': _id}, callback=_insert_doc)
有了gevent,這些代碼不再需要使用callback:
import gevent gevent.monkey.patch_all() def copy_documents(source_collection, destination_collection, _ids): """ Given a list of _id's (MongoDB's unique identifier field for each document), copies the corresponding documents from the source collection to the destination collection """ # copies each document using a separate greenlet; optimizations are certainly # possible but omitted in this example for _id in _ids: gevent.spawn(copy_document, source_collection, destination_collection, _id) def copy_document(source_collection, destination_collection, _id): """ Copies document corresponding to the given _id from the source to the destination. """ # both of the following function calls block without gevent; with gevent they # simply cede control to another greenlet while waiting for Mongo to respond source_doc = source_collection.find_one({'$id': _id}) destination_collection.insert(source_doc) # another MongoDB operation
這種簡(jiǎn)單的代碼可以根據(jù)它們的_idfields,從MongoDB源集合拷取代碼到目標(biāo)位置,它們的_idfields是每個(gè)MongoDB文檔的唯一標(biāo)識(shí)符。opy_documents 會(huì)產(chǎn)委派greenlets運(yùn)行runcopy_document()做文檔復(fù)制。當(dāng)greenlets執(zhí)行一項(xiàng)阻塞操作,比如對(duì)MongoDB的任何需求,它會(huì)將控制放給其它準(zhǔn)備執(zhí)行的greenlet。因?yàn)樗術(shù)reenlets都在相同的線程和進(jìn)程中執(zhí)行,你一般不需要任何形式的內(nèi)部鎖定。
有了gevent,就能夠找到比工作者線程池或工作者進(jìn)程池更快的方法。下面總結(jié)了每種方法的性能:
Approach | Performance (higher is better) |
---|---|
single process, no gevent | 520 documents/sec |
thread worker pool | 652 documents/sec |
process worker pool | 670 documents/sec |
single process, with gevent | 2,381 documents/sec |
綜合gevent和工作者進(jìn)程(每個(gè)分片一個(gè))可以在性能上得到一個(gè)線性提升。有效使用工作進(jìn)程的關(guān)鍵是盡可能使用更少的IPC。
問(wèn)題2:快照后的復(fù)制修改
因?yàn)镸ongoDB不支持事務(wù),如果你對(duì)正在執(zhí)行修改的大數(shù)據(jù)集進(jìn)行讀取,你得到的結(jié)果可能會(huì)因時(shí)而異。舉個(gè)例子,你使用MongoDB find()進(jìn)行整個(gè)數(shù)據(jù)集上的讀取,你的結(jié)果集可能是:
此外,為了在Mailbox后端指向新副本集時(shí)能最小化故障時(shí)間,盡可能減少?gòu)脑醇簯?yīng)用到新集群過(guò)程中所耗費(fèi)的時(shí)間則至關(guān)重要。
類(lèi)似多數(shù)的異步復(fù)制存儲(chǔ),MongoDB使用了操作日志oplog記錄下了mongod實(shí)例上發(fā)生的增、改、刪操作,用以分配給這個(gè)mongod實(shí)例的所有副本。鑒于快照,oplog記錄下快照發(fā)生后的所有改變。
所以這里的工作就變成了在目標(biāo)集群上應(yīng)用源集群的oplog記錄,從 Kristina Chodorow的教學(xué)博客上,我們清楚了oplog的格式。鑒于序列化的格式,增和刪都非常容易執(zhí)行,而改則成為了其中的難點(diǎn)。
改操作的oplog日志記錄結(jié)構(gòu)并不是非常友好:在MongoDB 2.2中使用了duplicate key,然而這些duplicate key并 不能通過(guò)Mongo shell呈現(xiàn),更不必說(shuō)大部分的MongoDB驅(qū)動(dòng)。深思熟慮之后,選擇了一個(gè)簡(jiǎn)單的變通方案:將_id嵌入修改源文檔,以觸發(fā)其它的文檔副本。因?yàn)橹皇轻槍?duì)修改,雖然不能做到副本集和源實(shí)例的完全同步,但是卻可以盡可能的減少副本集實(shí)時(shí)狀態(tài)與快照之間的差距。下面這個(gè)圖表顯示為何中間版本(v2)并不一定完全相同,但是源副本與目的副本仍能保持最終一致:
在這里同樣出現(xiàn)了目標(biāo)集群的性能問(wèn)題:雖然為每個(gè)分片的ops使用了獨(dú)立的進(jìn)程,但是連續(xù)的ops性能仍然匹配不了Mailbox的需求。
這樣ops的并行就成了必選之路,然而其中的正確性保證卻并不容易。特別的是,同_id操作必須被順序執(zhí)行。這里采用了一個(gè)Python集去維持正在執(zhí)行修改ops的_id集:當(dāng)copy_collection.py上發(fā)生一個(gè)請(qǐng)求正在執(zhí)行修改操作的文檔時(shí),系統(tǒng)會(huì)阻塞后申請(qǐng)的所有ops(不管是修改或者是其它),直到舊的操作結(jié)束。如圖所示:
>
驗(yàn)證復(fù)制數(shù)據(jù)
比較副本集與源實(shí)例數(shù)據(jù)通常是個(gè)簡(jiǎn)單的操作,但是在多進(jìn)程與多命名空間中進(jìn)行卻是個(gè)非常大的挑戰(zhàn)。同時(shí)基于數(shù)據(jù)正在不斷的被修改,需要考慮的事情就更多了:
首先使用compare_collections.py(為對(duì)比數(shù)據(jù)開(kāi)發(fā)的工具)對(duì)最近修改的文檔進(jìn)行數(shù)據(jù)校驗(yàn),如果出現(xiàn)不一致則進(jìn)行提醒,隨后再進(jìn)行復(fù)查。然而這對(duì)文檔的刪除并不有效,因?yàn)闆](méi)有最后修改的時(shí)間戳。
其次想到的是“ 最終一致性”,因?yàn)檫@在異步場(chǎng)景中非常流行,比如MongoDB的副本集和MySQL的主/從復(fù)制。經(jīng)過(guò)非常多的嘗試之后(除下大故障情景下),源數(shù)據(jù)和副本都會(huì)保持最終一致。因此又進(jìn)行了一些反復(fù)對(duì)比,在連續(xù)的重試中不斷的增加backoff。發(fā)現(xiàn)仍然有一些問(wèn)題存在,比如數(shù)據(jù)在兩個(gè)值之間搖擺不定;然而在修改模式下,遷移的數(shù)據(jù)并不會(huì)出現(xiàn)任何問(wèn)題。
在執(zhí)行新舊MongoDB集群的最終轉(zhuǎn)換之前,必須確保最近ops已經(jīng)被應(yīng)用,因此我們?cè)赾ompare_collections.py增加了命令行選項(xiàng),用以對(duì)比文檔被修改的最近N個(gè)操作,這樣可以有效的避免不一致性。這個(gè)操作并不用耗費(fèi)太多的時(shí)間,單分片執(zhí)行數(shù)十萬(wàn)的ops對(duì)比只需短短的幾分鐘,還能緩和對(duì)比和重試途徑的壓力。
意外情況處理
盡管使用了多種途徑去處理錯(cuò)誤(重試、發(fā)現(xiàn)可能的異常、日志),在產(chǎn)品遷移之前的最終測(cè)試中仍然出現(xiàn)了許多未預(yù)計(jì)的錯(cuò)誤。出現(xiàn)了一些不定期的網(wǎng)絡(luò)問(wèn)題,一個(gè)特定的文檔集會(huì)一直導(dǎo)致mongos斷開(kāi)與copy_collection.py連接,以及與mongod的偶然連接重置。
而在嘗試之后,我們發(fā)現(xiàn)針對(duì)這些問(wèn)題制定出專(zhuān)門(mén)的解決方案,所以快速的轉(zhuǎn)到了故障恢復(fù)方面。我們記錄了這些compare_collections.py 檢測(cè)出的文檔_id,然后專(zhuān)門(mén)建立了針對(duì)這些_id的文檔重復(fù)制工具。
最終遷移時(shí)刻
在產(chǎn)品遷移過(guò)程中,copy_collection.py建立了一個(gè)上千萬(wàn)電子郵件的原始快照,并且重現(xiàn)了過(guò)億的MongoDB ops。執(zhí)行原始快照、建立索引,整個(gè)復(fù)制過(guò)程持續(xù)了大約9個(gè)小時(shí),而我們?cè)O(shè)定的時(shí)限是24個(gè)小時(shí)。期間我們又使用copy_collection.py重復(fù)3次,對(duì)需要復(fù)制的數(shù)據(jù)核查了3次。
全部轉(zhuǎn)換直到今日才完成,與MongoDB相關(guān)的工作其實(shí)很少(只有幾分鐘)。在一個(gè)簡(jiǎn)潔的維護(hù)窗口中,我們使用compare_collections.py對(duì)比每個(gè)分片的最近的50萬(wàn)個(gè)ops。在確保最后操作中沒(méi)有不一致后,我們又做了一些相關(guān)測(cè)試,然后將Mailbox后端指向了新集群,并將服務(wù)重新為用戶開(kāi)放。而在轉(zhuǎn)換之后,我們未收到任何用戶反饋的問(wèn)題。讓用戶感覺(jué)不到遷移,就是最大的成功。遷移后的提升如下圖所示:
寫(xiě)鎖上的時(shí)間減少遠(yuǎn)高于50%(原預(yù)計(jì))
開(kāi)源Hydra
Hydra是上文操作所用到的所有工具合集,現(xiàn)已在 GitHub上開(kāi)源。
Scaling MongoDB at Mailbox(編譯/仲浩 審校/周小璐)
更多內(nèi)容請(qǐng)關(guān)注CSDN云計(jì)算頻道 及@CSDN云計(jì)算微博
聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問(wèn)題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com