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