天天看點

MongoDB的真正性能-實戰百萬使用者一:一億的道具

開始之前,我們先設定這樣一個情景:

1.一百萬注冊使用者的頁遊或者手遊,這是不溫不火的一個狀态,剛好是資料量不上不下的一個情況。也剛好是傳統mysql資料庫性能開始吃緊的時候。

2.資料庫就用一台很普通的伺服器,隻有一台。讀寫分離、水準擴充、記憶體緩存都不談。一百萬注冊使用者如果貢獻度和活躍度都不高,恐怕公司的日子還不是那麼寬裕,能夠在資料庫上的投資也有限。

以此情景為例,設每個使用者都擁有100個道具,使用者随時會獲得或失去道具。

我們就來看看這一億的道具怎麼搞。

道具一般要使用原型、執行個體的設計方法,這個不屬于資料庫的範疇。

道具類型001 是屠龍刀,屠龍刀價格1500,基礎攻擊150,這些,我們把它們稱為道具原型,儲存在原型資料檔案中。

這個原型資料檔案,無論是存在何種資料庫或者本地檔案中,對伺服器來說都不是問題,也不幹擾資料庫設計,是以我們不去讨論他。

<a target="_blank"></a>

典型的關系資料庫設計方法:

使用者表:字段 xxx userid xxx   ,記錄數量100萬

xxx是其他字段,userid标示使用者

使用者道具表:字段 xxx userid itemtype xxx ,記錄數量一億

xxx是其他字段,userid 标示

一個億的記錄數是不是看起來有點頭疼,mysql這個時候就要想各種辦法了。

但我們用mongodb來實作這個需求,直接就沒有問題

首先第一個集合:users集合,用username 作為_id ,記錄數100萬

然後道具的組織,我們有兩種選擇

1.在users集合的值中建立items對象,用bson數組儲存道具(mongo官方稱為bson,和json一模一樣的存儲方法)

方法一,沒有額外的記錄數

2.建立useritems集合,同樣用username作為_id 每個useritems集合的值中建立一個item對象,使用一個bson數組來儲存道具

方法二,多了一個集合和100萬記錄數

我們的道具資料看起來像下面這樣:

{_id:xxx,items:[

{itemtype:xxx,itempower:xxx},

...

]}

測試方法如下:測試用戶端随機檢查一個使用者的道具數量,小于100加一個道具,大于100 删除一個道具。

連續100萬次,采用10個線程并發。

如果用關系資料庫設計方法+mysql來實作,這是一個很壓力很大的資料處理需求。

可是用文檔資料庫設計方法+mongodb來實作,這個測試根本算不上有壓力。

即使我們用了一個如此勝之不武的設計方式,你依然有可能還是能把他寫的很慢。

因為mongodb在接口設計上并沒有很好的引導和限制,如果你不注意,你還是能把他用的非常慢。

mongodb的索引代價很大,大到什麼程度:

1.巨大的記憶體占用,100萬條索引約占50m記憶體,如果這個設計中,你一個道具一條記錄,5g記憶體将用于索引。

我們的屌絲情景不可能給你這樣的伺服器,

2.巨大的性能損失,作為一個資料庫,所有的東西終将被寫入硬碟,沒有關系資料庫那樣的表結構,mongodb的索引寫入性能看起來很差,如果記錄資料較小的時候,你可以觀測到這樣震撼的景象,加一個索引,性能變成了1/2,加兩個索引,性能變成了1/3。

隻有當第二個索引的查詢不可避免,才值得增加額外索引。因為沒索引的資料,查詢性能是加幾個零的慢,比加索引更慘。

我們既然選擇了key-value資料庫,應盡量避免需要多個索引的情況。

所有的索引隻能存在于記憶體中,而讀取記錄時,也需要将bson在記憶體中處理,記憶體還承擔着更重要的作用:讀取緩存。

本來就不充裕的記憶體,應該嚴格控制我們的記錄條數,能夠用bson存儲的,盡量用之。

那麼我們之前在mongodb的設計中怎麼還考慮第二種設計方法呢?獨立一個useritems 集合,不是又多出100萬條記錄了嗎?

這基于另兩個考慮:a.bson的處理是要反複硬碟和記憶體交換的,如果每條記錄更小,則io壓力更小。記憶體和硬碟對伺服器來說都是稀缺資源,至于多大的資料拆分到另一個集合中更劃算,這需要根據業務情況,伺服器記憶體、硬碟情況來測試出一個合适大小,我們暫時使用1024這個數值,單使用者的道具表肯定是會突破1024位元組的,是以我們要考慮将他獨立到一個集合中

b.可以不部署分片叢集,将另一個集合挪到另一個伺服器上去。隻要伺服器可以輕松承載100萬使用者,200萬還會遠麼?在有錢部署分片叢集以前,考慮第二組伺服器更現實一些。

毋庸置疑,findone({_id:xxx})就是最直接的用key取value。

也的确,用key取value 就是我們能用的唯一通路value的方式,其他就不叫key-value資料庫了。

但是,由于我們要控制key的數量,單個value就會比較大。

不要被findone({_id:xxx}).items[3].itemtype這優雅的代碼欺騙,這是非常慢的,他幾乎謀殺你所有的流量。

無論後面是什麼 findone({_id:xxx})總是傳回給你完整的value,我們的100條道具,少說也有6~8k.

這樣的查詢流量已經很大了,如果你采用mongodb方案一設計,你的單個value是包含一個使用者的所有資料的,他會更大。

如果查詢用戶端和資料庫伺服器不在同一個機房,流量将成為一個很大的瓶頸。

我們應該使用的查詢函數是findone({_id:xxx},filter),filter裡面就是設定傳回的過濾條件,這會在發送給你以前就過濾掉

比如findone({_id:xxx},{items:{"$slice":[3,1]}}),這和上面那條優雅的代碼是完成同樣功能,但是他消耗很少的流量

這和問題二相對的,不要暴力的findone,也盡量不要暴力的update一整個節點。雖然mangodb的性能挺暴力的,io性能極限約等于mongodb性能,暴力的update就會在占用流量的同時迎接io的性能極限。

除了建立節點時的insert或者save之外,所有的update都應該使用修改器精細修改.

比如update({_id:xxx},{$set:{"items.3.item.health":38}});//修改第三把武器的健康值

至于一次修改和批量修改,mongodb預設100ms flush一次(2.x),隻要兩次修改比較貼近,被一起儲存的可能性很高。

但是合并了肯定比不合并強,合并的修改肯定是一起儲存,這個也要依賴于是用的開發方式,如果使用php做資料用戶端,緩存起來多次操作合并了一起送出,實作起來就比較複雜。

注意以上三點,一百萬注冊使用者并不算很多,4g記憶體,200g硬碟空間的mongodb伺服器即可輕松應對。性能瓶頸是硬碟io,可以很容易的使用raid和固态硬碟提升幾倍的吞吐量。不使用大量的js計算,cpu不會成為問題,不要讓索引膨脹,記憶體不會成為問題。你根本用不着志強的一堆核心和海量的記憶體,更多的記憶體可以讓緩存的效果更好一些,可是比讀寫分離還是差遠了。如果是高并發時查詢性能不足,就要采用讀寫分離的部署方式。當io再次成為瓶頸時,就隻能采用叢集部署mongodb啟用分片功能,或者自行進行分集合與key散列的工作。

<b> 原文釋出時間為:2013-05-10</b>

<b>本文來自雲栖社群合作夥伴“linux中國”</b>