點(diǎn)評(píng): MongoDB的使用依然要注重設(shè)計(jì),甚至是對(duì)使用者的要求更高,而不是相反; MongoDB協(xié)議問(wèn)題,這個(gè)在c++代碼集成的時(shí)候可能沒(méi)有redis簡(jiǎn)潔;但MongoDB通信協(xié)議也只是對(duì)socket的簡(jiǎn)單封裝,并不復(fù)雜; 游標(biāo)迭代問(wèn)題,迭代我findnext肯定是要拋異常的的,是不
點(diǎn)評(píng):
MongoDB的使用依然要注重設(shè)計(jì),甚至是對(duì)使用者的要求更高,而不是相反;
MongoDB協(xié)議問(wèn)題,這個(gè)在c++代碼集成的時(shí)候可能沒(méi)有redis簡(jiǎn)潔;但MongoDB通信協(xié)議也只是對(duì)socket的簡(jiǎn)單封裝,并不復(fù)雜;
游標(biāo)迭代問(wèn)題,迭代我findnext肯定是要拋異常的的,是不會(huì)繼續(xù)查詢的,不知道作者遇到的是什么情況;
MongoDB片鍵顯然沒(méi)設(shè)計(jì)好,即使最后使用log時(shí)間+自增id,依然不理想。
我們公司開(kāi)始用 mongodb 并不是因?yàn)殚_(kāi)始的技術(shù)選型,而是我們代理的第一款游戲《 狂刃 》的開(kāi)發(fā)商選擇了它。這款游戲在我們代理協(xié)議簽訂后,就進(jìn)入了接近一年的共同開(kāi)發(fā)期。期間發(fā)現(xiàn)了很多和數(shù)據(jù)庫(kù)相關(guān)的問(wèn)題,迫使我們熟悉了 mongodb 。在那個(gè)期間,我們搭建的運(yùn)營(yíng)平臺(tái)自然也選擇了 mongodb 作為數(shù)據(jù)庫(kù),這樣維護(hù)人員就可以專心一種數(shù)據(jù)庫(kù)了。
經(jīng)過(guò)一些簡(jiǎn)單的了解,我發(fā)現(xiàn)國(guó)內(nèi)很多游戲開(kāi)發(fā)者都不約而同的采用了 mongodb ,這是為什么呢?我的看法是這樣的:
游戲的需求多變,很難在一開(kāi)始就把數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)清楚。而游戲領(lǐng)域的許多程序員的技術(shù)背景又和其他領(lǐng)域不同。在設(shè)計(jì)游戲服務(wù)器前,他們更多的是在設(shè)計(jì)游戲的客戶端:畫面、鍵盤鼠標(biāo)交互、UI 才是他們花精力最多的地方。對(duì)該怎么使用數(shù)據(jù)庫(kù)沒(méi)有太多了解。這個(gè)時(shí)候,出現(xiàn)了 mongodb 這樣的 NOSQL 數(shù)據(jù)庫(kù)。mongodb 是基于文檔的,不需要你設(shè)計(jì)數(shù)據(jù)表,和動(dòng)態(tài)語(yǔ)言更容易結(jié)合??雌饋?lái)很美好,你只需要把隨便一個(gè)結(jié)構(gòu)的數(shù)據(jù)對(duì)象往數(shù)據(jù)庫(kù)里一塞,然后就祈禱數(shù)據(jù)庫(kù)系統(tǒng)會(huì)為你搞定其它的事情。如果數(shù)據(jù)庫(kù)干的不錯(cuò),性能不夠,那是數(shù)據(jù)庫(kù)的責(zé)任,和我無(wú)關(guān)。看到那些評(píng)測(cè)數(shù)據(jù)又表明 mongodb 的性能非常棒,似乎沒(méi)有什么可擔(dān)心的了。
其實(shí)無(wú)論什么系統(tǒng),在對(duì)性能有要求的環(huán)境下,完全當(dāng)黑盒用都是不行的。
游戲更是如此。上篇我就談過(guò),我們絕對(duì)不可能把游戲里數(shù)據(jù)的變化全部扔到數(shù)據(jù)庫(kù)中去做。傳統(tǒng)數(shù)據(jù)庫(kù)并非為游戲設(shè)計(jì)的。
比如,你把一群玩家的坐標(biāo)同步到數(shù)據(jù)庫(kù),能夠把具體某個(gè)玩家附近玩家列表查詢出來(lái)么?mongodb 倒是提供了 geo 類型,可以用 near 或 within 指令查詢得到附近用戶??伤軡M足 10Hz 的更新頻率么?
我們可以把玩家的 buf 公式一一送入數(shù)據(jù)庫(kù),然后修改一些屬性值,就可以查詢到通過(guò) buf 運(yùn)算得到的結(jié)果么?
這類問(wèn)題有很多,即使你能找到方法讓數(shù)據(jù)庫(kù)為你工作,那么性能也是堪憂的。當(dāng)我們能在特定的數(shù)據(jù)庫(kù)服務(wù)內(nèi)一一去解決她們,最終數(shù)據(jù)庫(kù)就是一個(gè)游戲服務(wù)器了。
狂刃這個(gè)項(xiàng)目在我們公司是負(fù)責(zé)平臺(tái)建設(shè)的蝸牛同學(xué)跟的。我從他那里聽(tīng)來(lái)了許多錯(cuò)誤使用 mongodb 的趣聞。
一開(kāi)始,整個(gè)數(shù)據(jù)庫(kù)完全沒(méi)有為查詢建索引。在沒(méi)什么數(shù)據(jù)的情況下,即使所有的查詢都是 O(N) 的,遍歷整個(gè)數(shù)據(jù)庫(kù),也不會(huì)有問(wèn)題??上攵脩袅恳簧蟻?lái),性能會(huì)下降的多快。
然后,數(shù)據(jù)庫(kù)又被建立了大量的無(wú)用的索引,和一些錯(cuò)誤的復(fù)合索引,同樣惡化了系統(tǒng)。感覺(jué)就是哪里似乎有點(diǎn)性能問(wèn)題,那就是少了個(gè)索引的緣故。這種病急亂投醫(yī)的現(xiàn)象,在項(xiàng)目開(kāi)發(fā)后期很容易出現(xiàn)。其實(shí)解決方法很簡(jiǎn)單:主導(dǎo)設(shè)計(jì)的人只要靜下心來(lái)好好想一想,數(shù)據(jù)庫(kù)系統(tǒng)其實(shí)也就是一個(gè)管理數(shù)據(jù)的封閉模塊。如果你來(lái)管理這些數(shù)據(jù),怎樣的數(shù)據(jù)結(jié)構(gòu)更利于滿足特定的檢索,需要哪些索引數(shù)據(jù)輔助。
最終的問(wèn)題依舊是算法和數(shù)據(jù)結(jié)構(gòu),不同的是,不需要你實(shí)現(xiàn)它,而需要你理解它。
另外,數(shù)據(jù)庫(kù)是被設(shè)計(jì)成可以并發(fā)訪問(wèn)的,而并發(fā)永遠(yuǎn)是復(fù)雜的東西。mongodb 缺乏事務(wù)操作,需要用文檔操作的原子性來(lái)模擬。這很容易被沒(méi)經(jīng)驗(yàn)的人用錯(cuò)(這是個(gè)怪圈,越是沒(méi)數(shù)據(jù)庫(kù)經(jīng)驗(yàn)的人越喜歡 mongodb ,因?yàn)橄拗粕?,看起?lái)更自然。)。
狂刃出過(guò)這樣一個(gè) bug :想讓用戶注冊(cè)的時(shí)候用戶名唯一,所以在用戶注冊(cè)的時(shí)候先查一下數(shù)據(jù)庫(kù)看用戶名是否存在,如果不存在就允許創(chuàng)建一個(gè)這個(gè)名字的用戶??上攵?,上線運(yùn)營(yíng)不出一天,同名用戶就會(huì)出現(xiàn)了。
因?yàn)楣卷?xiàng)目需要,我給 skynet 增加了 mongo driver 。老實(shí)說(shuō),實(shí)現(xiàn)這個(gè) driver 的時(shí)候,我對(duì) mongo 就興趣寥寥。最后只實(shí)現(xiàn)了最底層的通訊協(xié)議,光這個(gè)部分,它的協(xié)議設(shè)計(jì)就已經(jīng)是很難看的了。但是即使這樣,我也耐著性子把這部分做完,而不想使用現(xiàn)成的 driver 。
mongo 的官方 driver 都是內(nèi)置 socket 通訊模塊的。這種做法很難單獨(dú)把協(xié)議解析部分提取出來(lái),附加到自己項(xiàng)目的 IO 模型中去。(btw, redis 這方面就好的多,因?yàn)樗膮f(xié)議足夠簡(jiǎn)單,你可以用幾十行代碼就實(shí)現(xiàn)它的通訊協(xié)議,而不需要依賴 driver 模塊。)
狂刃服務(wù)器的 IO 采用的 boost.asio ,我很好奇他是怎樣把 mongodb 官方 C++ driver 整合進(jìn)去的。不出所料,他們開(kāi)了一個(gè)獨(dú)立線程處理 mongo 的數(shù)據(jù),然后把數(shù)據(jù)對(duì)象跨線程發(fā)出來(lái)。細(xì)究這個(gè)實(shí)現(xiàn)就能看出問(wèn)題來(lái)。程序員很容易誤解 mongodb client api 的內(nèi)在含義。
一開(kāi)始,狂刃的開(kāi)發(fā)同學(xué)以為從 mongo 中取到一組查詢結(jié)果后,調(diào)用 cursor 的 findnext 只在對(duì)象內(nèi)存中迭代,所有結(jié)果都是一開(kāi)始一次性返回的。以為把一開(kāi)始的 bson 對(duì)象從 mongo 線程轉(zhuǎn)移到主線程中就好了??墒聦?shí)并不是這樣,mongo 一次只會(huì)返回一組查詢結(jié)果,當(dāng)結(jié)果迭代完時(shí),findnext 還會(huì)自動(dòng)提交新的查詢請(qǐng)求。這時(shí),對(duì)象已經(jīng)不在原有的 mongo 線程中了。
學(xué)過(guò) C++ 的同學(xué)可以想像一下,讓你去 code review 不是你參于的 C++ 項(xiàng)目去找到 bug 需要多少功夫?對(duì)了,你還要在想像中要加上被各種 boost.asio 回調(diào)函數(shù)拆得支離破碎的業(yè)務(wù)流程。所以去年有那么一段日子,我們需要完全停下手頭其他的工作,認(rèn)真的從頭閱讀那數(shù)以萬(wàn)行計(jì)的 C++ 代碼。
老八卦別人似乎不太厚道,下面來(lái)談?wù)勎覀冏约悍傅腻e(cuò)誤。
陌陌爭(zhēng)霸出的第一起服務(wù)器事故是在 2014 年一月中旬的一個(gè)周末。準(zhǔn)確說(shuō),這次算不上重大運(yùn)營(yíng)事故,因?yàn)闆](méi)有玩家數(shù)據(jù)受損,也沒(méi)有意外停服。但卻是我們第一次發(fā)現(xiàn)早先設(shè)計(jì)中有考慮不足的地方。
1 月 12 日周日。下午 17 點(diǎn)左右,我們的 SA Aply 發(fā)現(xiàn)我們運(yùn)營(yíng)用的 log 延遲了 3 個(gè)小時(shí)才到運(yùn)營(yíng)平臺(tái)。但數(shù)據(jù)還是源源不斷的進(jìn)入,系統(tǒng)也很穩(wěn)定,就沒(méi)有特別深究。
到了晚上 20 點(diǎn)半,平臺(tái)組的劉陽(yáng)報(bào)告說(shuō)運(yùn)營(yíng)數(shù)據(jù)已經(jīng)延遲了 5 個(gè)小時(shí)了,這才引起了大家的警覺(jué)。由于是周末,開(kāi)發(fā)人員都回家休息了,曉靖 21 點(diǎn)上線檢查,這時(shí)發(fā)現(xiàn)游戲服務(wù)器內(nèi)存占用比平常同期高了 10G 之多,并在持續(xù)上升。
我大約是在 21 點(diǎn)接到電話的,在電話中討論分析了一下,覺(jué)得是 log 數(shù)據(jù)從 skynet 的 log 服務(wù)發(fā)走,可能被積壓在 socket server 的一個(gè)鏈表上。這段代碼并不復(fù)雜,插入新的寫入數(shù)據(jù)是 O(1) 操作,所以沒(méi)有阻塞玩家游戲的風(fēng)險(xiǎn)。而輸出 log 的頻率還不至于短期把所有內(nèi)存吃光。游戲服務(wù)器暫時(shí)是安全的。
晚 21 點(diǎn) 40 分,雖然沒(méi)能分析出事故的源頭,但我們立刻采取了應(yīng)急方案。重新啟動(dòng)了一套游戲服務(wù)器,在線將舊服務(wù)器上的 80% 玩家導(dǎo)到新的備用服務(wù)器上。并同時(shí)啟動(dòng)了新的 log 數(shù)據(jù)庫(kù)集群。打算挺到周一再在固定維護(hù)時(shí)間處理。
晚 23 點(diǎn),新啟動(dòng)的游戲服務(wù)器也出現(xiàn)了 log 輸出延遲。因?yàn)檫\(yùn)營(yíng) log 是輸出到一個(gè) mongos 管理的集群中的,我們嘗試在舊的集群(已無(wú)新數(shù)據(jù)寫入,但依舊沒(méi)有消化完滯留的舊數(shù)據(jù))做了刪除部分索引的嘗試,沒(méi)有什么效果。
凌晨 0:45 ,開(kāi)啟了新的備機(jī)群,取消了 mongos ,讓每臺(tái)機(jī)器獨(dú)立連接一個(gè)單獨(dú)的 mongodb ,情況終于好轉(zhuǎn)了。
以上,是當(dāng)時(shí)事故記錄的節(jié)選。
徹底搞明白事故起源是周二的事情了。
表面上看起來(lái)是在 mongos 服務(wù)上堆積了大量的數(shù)據(jù)庫(kù)插入操作。讓這個(gè)單點(diǎn)過(guò)載了。我們起初的運(yùn)營(yíng) log 輸出是有點(diǎn)偏多,比如每個(gè)士兵的訓(xùn)練都有一條單獨(dú)的 log ,而陌陌爭(zhēng)霸游戲中這種 log 是巨量的。我們裁減并精簡(jiǎn)了一部分 log 但似乎并不能從根本上解釋這起事故。
問(wèn)題出在 mongos 的 shard key 的選擇上。mongo 可以指定 document 的若干字段為 shard key ,mongos 把這個(gè) key 當(dāng)成一個(gè)整數(shù),按整數(shù)區(qū)間把 document 分成若干個(gè)桶。再把桶均勻分配到背后的從機(jī)上。
如果你的 key 是有規(guī)律的數(shù)字,而你又需要這種規(guī)律不至于破壞桶分配的公平性,你還可以將一個(gè) hash 算法應(yīng)用于原始選擇的 key 上,讓 key 足夠散列開(kāi)。我們一開(kāi)始就是按自增 id 的散列結(jié)果做 key 的。
錯(cuò)誤的 shard key 選擇就是這起事故的罪魁禍?zhǔn)住?/p>
因?yàn)槲覀兪谴罅康捻樞驅(qū)懖僮?,?yīng)該優(yōu)先保證寫入的流暢。如果用隨機(jī)散列的方式去看待這些 document 的話,新舊 log 就很大幾率被分配到一起。而 mongo 并不是一條一個(gè)單位將數(shù)據(jù)落地的,而是一塊塊的進(jìn)行。這種冷熱數(shù)據(jù)的交織會(huì)導(dǎo)致寫盤 IO 量遠(yuǎn)遠(yuǎn)大于 log 實(shí)際的輸出量。
最后我們調(diào)整了 shard key ,按 log 時(shí)間和自增 id 分開(kāi),就把 mongo 數(shù)據(jù)落地的 IO 量下降了幾個(gè)數(shù)量級(jí)。
看吧,理解系統(tǒng)如何工作的很重要。讀文檔也很重要,這個(gè)問(wèn)題在 mongoDB 文檔中被討論過(guò) 。
ps, 這起事故后,我給 skynet 加了更多的監(jiān)控,方便預(yù)警單個(gè)模塊的過(guò)載。這幫助我們更快的定位后面出現(xiàn)的問(wèn)題。那些關(guān)于 redis 的故事,且聽(tīng)下回分解。
來(lái)源:http://blog.codingnow.com/2014/03/mmzb_mongodb.html
原文地址:談?wù)勀澳盃?zhēng)霸在數(shù)據(jù)庫(kù)方面踩過(guò)的坑(MongoDB篇), 感謝原作者分享。
聲明:本網(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