在之前的文章中,我們分享了 Mailbox如何在六星期實現從零到百萬用戶及日處理億條消息。其中我們提過Mailbox以14個人的小團隊,在6個星期內實現0到百萬用戶的壯舉,而服務日承載信息破億條。隨后在App發布不到3周,他們將自己以1億美元的價格賣給了Dropbox。
在之前的文章中,我們分享了 Mailbox如何在六星期實現從零到百萬用戶及日處理億條消息。其中我們提過Mailbox以14個人的小團隊,在6個星期內實現0到百萬用戶的壯舉,而服務日承載信息破億條。隨后在App發布不到3周,他們將自己以1億美元的價格賣給了Dropbox。這次我們帶來的是,Mailbox在快速擴展過程中,MongoDB所遭遇的性能瓶頸及解決途徑。
以下為譯文:
在Mailbox快速擴展過程中,其中一個性能問題就是MongoDB的數據庫級別寫鎖,在鎖等待過程中耗費的時間,直接反應到用戶使用服務過程中的延時。為了解決這個長期存在的問題,我們決定將一個常用的MongoDB集合(儲存了郵件相關數據)遷移到獨立的集群上。根據我們推斷,這將減少50%的鎖等待時間;同時,我們還可以添加更多的分片,我們還期望可以獨立的優化及管理不同類型數據。
我們首先從MongoDB文檔開始,很快的就發現了 cloneCollection命令。然而隨后悲劇的發現,它不可以在分片集合中使用;同樣, renameCollection也不能在分片集合中使用。在否定了其它可能性之后(基于性能問題),我們編寫了一個Python腳本用以復制數據,和另一個用于比較原始和目標數據的腳本。在這個過程中,我們還發現了許多有意思的事情,比如 gevent及 pymongo復制大數據集的時間是 mongodump(C++編寫)的一半,即使MongoDB客戶端和服務器在同臺主機上。通過最終努力,我們開發了 Hydra,用于MongoDB遷移的工具集,現已開源。首先,我們建立了MongoDB集合的原始快照。
問題1:悲劇的性能
早期我做了一個實驗以測試MongoDB API運作所能達到的極限速度——啟用一個簡單的使用MongoDB C++ 軟件開發工具包的速度。一方面對C++ 感覺厭煩,一方面希望我大多數熟練使用Python的同事可以在其他用途上使用或適應這種代碼,我沒有更進一步的探索C++的使用,而是發現,如果是針對少量數據,在處理相同任務上,簡單的C++應用速度是簡單Python應用的5-10倍。
所以,我的研究方向回到了Python,這個Dropbox默認語言。此外,進行了諸如對mongod查詢等的一系列遠程網絡請求時,客戶端往往需要耗費大量時間等待服務器響應;似乎也沒有很多copy_collection.py (我的MongoDB集合復制工具)需要的CPU密集型操作(部分)。initialcopy_collection.py占很少的CPU使用率也證實了這一點。
然后,MongoDB請求到copy_collection.py.。最初的工作線程實驗結果并不理想。但接下來,我們通過Python Queue對象來實現工作線程通信。這樣的性能依舊不是很好,因為IPC上的開銷讓并發帶來的提升黯然失色。使用Pipes和其他IPC機制也并沒有多大幫助。
接下來,我們嘗試了使用單線程Python進行MongoDB異步查詢,看看可以有多少性能結余。其中Gevent是實現這個途徑常用庫之一,我們對它進行了嘗試。Gevent 修改了標準Python模塊以實現異步操作,比如socket。比較好的一點是,你可以簡單的編寫異步讀取代碼,就像同步代碼一樣。
通常情況下,兩個集合之間復制文檔的異步代碼會是:
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
這種簡單的代碼可以根據它們的_idfields,從MongoDB源集合拷取代碼到目標位置,它們的_idfields是每個MongoDB文檔的唯一標識符。opy_documents 會產委派greenlets運行runcopy_document()做文檔復制。當greenlets執行一項阻塞操作,比如對MongoDB的任何需求,它會將控制放給其它準備執行的greenlet。因為所有greenlets都在相同的線程和進程中執行,你一般不需要任何形式的內部鎖定。
有了gevent,就能夠找到比工作者線程池或工作者進程池更快的方法。下面總結了每種方法的性能:
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和工作者進程(每個分片一個)可以在性能上得到一個線性提升。有效使用工作進程的關鍵是盡可能使用更少的IPC。
問題2:快照后的復制修改
因為MongoDB不支持事務,如果你對正在執行修改的大數據集進行讀取,你得到的結果可能會因時而異。舉個例子,你使用MongoDB find()進行整個數據集上的讀取,你的結果集可能是:
此外,為了在Mailbox后端指向新副本集時能最小化故障時間,盡可能減少從源集群應用到新集群過程中所耗費的時間則至關重要。
類似多數的異步復制存儲,MongoDB使用了操作日志oplog記錄下了mongod實例上發生的增、改、刪操作,用以分配給這個mongod實例的所有副本。鑒于快照,oplog記錄下快照發生后的所有改變。
所以這里的工作就變成了在目標集群上應用源集群的oplog記錄,從 Kristina Chodorow的教學博客上,我們清楚了oplog的格式。鑒于序列化的格式,增和刪都非常容易執行,而改則成為了其中的難點。
改操作的oplog日志記錄結構并不是非常友好:在MongoDB 2.2中使用了duplicate key,然而這些duplicate key并 不能通過Mongo shell呈現,更不必說大部分的MongoDB驅動。深思熟慮之后,選擇了一個簡單的變通方案:將_id嵌入修改源文檔,以觸發其它的文檔副本。因為只是針對修改,雖然不能做到副本集和源實例的完全同步,但是卻可以盡可能的減少副本集實時狀態與快照之間的差距。下面這個圖表顯示為何中間版本(v2)并不一定完全相同,但是源副本與目的副本仍能保持最終一致:
在這里同樣出現了目標集群的性能問題:雖然為每個分片的ops使用了獨立的進程,但是連續的ops性能仍然匹配不了Mailbox的需求。
這樣ops的并行就成了必選之路,然而其中的正確性保證卻并不容易。特別的是,同_id操作必須被順序執行。這里采用了一個Python集去維持正在執行修改ops的_id集:當copy_collection.py上發生一個請求正在執行修改操作的文檔時,系統會阻塞后申請的所有ops(不管是修改或者是其它),直到舊的操作結束。如圖所示:
>
驗證復制數據
比較副本集與源實例數據通常是個簡單的操作,但是在多進程與多命名空間中進行卻是個非常大的挑戰。同時基于數據正在不斷的被修改,需要考慮的事情就更多了:
首先使用compare_collections.py(為對比數據開發的工具)對最近修改的文檔進行數據校驗,如果出現不一致則進行提醒,隨后再進行復查。然而這對文檔的刪除并不有效,因為沒有最后修改的時間戳。
其次想到的是“ 最終一致性”,因為這在異步場景中非常流行,比如MongoDB的副本集和MySQL的主/從復制。經過非常多的嘗試之后(除下大故障情景下),源數據和副本都會保持最終一致。因此又進行了一些反復對比,在連續的重試中不斷的增加backoff。發現仍然有一些問題存在,比如數據在兩個值之間搖擺不定;然而在修改模式下,遷移的數據并不會出現任何問題。
在執行新舊MongoDB集群的最終轉換之前,必須確保最近ops已經被應用,因此我們在compare_collections.py增加了命令行選項,用以對比文檔被修改的最近N個操作,這樣可以有效的避免不一致性。這個操作并不用耗費太多的時間,單分片執行數十萬的ops對比只需短短的幾分鐘,還能緩和對比和重試途徑的壓力。
意外情況處理
盡管使用了多種途徑去處理錯誤(重試、發現可能的異常、日志),在產品遷移之前的最終測試中仍然出現了許多未預計的錯誤。出現了一些不定期的網絡問題,一個特定的文檔集會一直導致mongos斷開與copy_collection.py連接,以及與mongod的偶然連接重置。
而在嘗試之后,我們發現針對這些問題制定出專門的解決方案,所以快速的轉到了故障恢復方面。我們記錄了這些compare_collections.py 檢測出的文檔_id,然后專門建立了針對這些_id的文檔重復制工具。
最終遷移時刻
在產品遷移過程中,copy_collection.py建立了一個上千萬電子郵件的原始快照,并且重現了過億的MongoDB ops。執行原始快照、建立索引,整個復制過程持續了大約9個小時,而我們設定的時限是24個小時。期間我們又使用copy_collection.py重復3次,對需要復制的數據核查了3次。
全部轉換直到今日才完成,與MongoDB相關的工作其實很少(只有幾分鐘)。在一個簡潔的維護窗口中,我們使用compare_collections.py對比每個分片的最近的50萬個ops。在確保最后操作中沒有不一致后,我們又做了一些相關測試,然后將Mailbox后端指向了新集群,并將服務重新為用戶開放。而在轉換之后,我們未收到任何用戶反饋的問題。讓用戶感覺不到遷移,就是最大的成功。遷移后的提升如下圖所示:
寫鎖上的時間減少遠高于50%(原預計)
開源Hydra
Hydra是上文操作所用到的所有工具合集,現已在 GitHub上開源。
Scaling MongoDB at Mailbox(編譯/仲浩 審校/周小璐)
更多內容請關注CSDN云計算頻道 及@CSDN云計算微博
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com