為什麼我們選擇了MongoDB?
我司是一家正處於高速發展,目前擁有數百萬用户,年銷售額近五十億的社交電商公司。
圖片來自 Pexels
公司技術部建立之初,為了適應用户量的高速增長,與業務的不斷變更迭代,在選用數據庫的時候,經過調研對比我們選擇了 MongoDB。
是的,你沒看錯,All in MongoDB!
本文將圍繞如下幾個部分進行分享:
為什麼使用 MongoDB(選擇數據的時候我們是怎麼考慮的?)
MongoDB 架構(99.99% 高可用,晚上安心睡大覺!)
MongoDB 分片(海量數據應對之道!)
MongoDB 文檔模型介紹(靈活!靈活!靈活!)
為什麼使用 MongoDB
因為我司主要做社交電商的業務,所以對數據庫的性能有一定的要求,加上商品交易是公司主要盈利來源,所以對數據庫的高可用也有一定的要求。
總結一下我們對數據庫的要求:
安全,穩定
高可用
高性能
我們在考慮數據庫選型的時候主要考慮什麼?
數據規模
支持讀寫併發量
延遲與吞吐量
從數據規模來説訂單和商品 SKU,還有會員信息這些重要的數據記錄肯定會隨着時間源源不斷的增長。
所以我們需要的不僅僅是滿足當下要求,更需要為半年一年後海量數據更為方便的擴容做考量!
下面我們從 MongoDB 的架構,性能,和文檔模型來介紹一下我們選擇 MongoDB 的理由!
MongoDB 架構
①關於高可用
數據庫作為系統核心,要保證 99.99% 的可用性,而高可用的保證來自於 MongoDB 冗餘數據的複製集模式。
MongoDB 自帶多副本高可用,只需要合理的配置,就能避免單數據庫節點故障導致服務的不可用。
圖例説明:
一個 Primary 主節點,主要接受來自 server 的讀寫。
兩個 Secondary 從節點,用於同步來自 Primary 的數據。
關於高可用:當主節點發生故障的時候,兩個從節點會進行選舉,投票產生一個新的主節點,進而保證服務的可用性。
PS:在選舉過程中數據不可寫入,但是如果 Secnondary 節點配置可讀,那麼此時是可以讀取數據的。
這就是 MongoDB 的高可用,配置簡單,不需要引入額外的中間件或者插件去輔助數據庫節點間的故障轉移。
②關於選舉算法《分佈式一致性算法---raft》
raft 協議是在 leader 節點發生故障或者網絡分區導致腦裂時如何保證分佈式數據一致性的一個算法,MongoDB 採用了該算法來保證當主節點故障或者網絡分區的情況下,數據的一致性。
當然 MongoDB 用的和 raft 原版算法肯定會略有不同,MongoDB 會採用 Secondary 向 Primary 拉數據,而不是 Primary 向 Secondary 推數據的方式來減輕 Primary 的壓力等等有利於數據庫操作的方式對 raft 進行改進使用。
raft 算法動畫演示:
http://thesecretlivesofdata.com/raft/
③關於超大規模複製集(集羣)
Non-Voting Members
上圖是一個擁有 7 個可投票從節點,一個主節點,兩個不可投票從節點。
{ "_id" : , "host" : , "arbiterOnly" : false, "buildIndexes" : true, "hidden" : false, "priority" : 0, // 設置為0 "tags" : { }, "slaveDelay" : NumberLong(0), "votes" : 0 // 設置為0 }
MongoDB 最多允許 50 個節點,但是最多隻有 7 個節點有投票權,一個節點可以配置 7 個無投票權的 Non-Voting 節點,加上一個 Primary 節點。
為什麼只能允許存在 7 個投票節點呢?參考上節的 raft 算法,節點越多,投票時間越長,選舉出來的 Primary 節點時間也就越長,這個過程中我們是無法進行寫操作的,因為沒有主節點。
那麼多非投票節點有什麼用呢?大家應該都聽過 MySQL 的讀寫分離吧,利用讀寫分離來提高數據庫性能。
MongoDB 這裏其實也可以,Primary 用來寫,Secondary 用來讀,可以給 BI 部門一個 Secondary,給財務部門一個 Secondary,給運營部門一個 Secondary······
④WriteConcern
既然我們的數據庫擁有至少超過三個節點(1Primary 2Secondary),Secondary 通過同步 Primary 的數據來保持一致性,那麼當我們寫操作的時候,如何保證數據安全的落盤呢?
有以下幾種情況:
寫 Primary 成功,返回客户端寫成功,Secondary 還未同步 Primary 的時候,Primary 掛了,數據丟失!
寫 Primary 成功,數據同步一個 Secondary 成功,返回客户端寫成功。此時 Primary 掛了,數據不會丟失。但是恰好 Primary 與同步的 Secondary 同時掛了,數據丟失!
寫 Primary 成功,數據同步兩個 Secondary 成功,返回客户端寫成功。此時 Primary 掛了,數據不會丟失。
我們對以上三種情況進行分析:
第一種情況有風險會造成數據丟失。
第二種情況還是會出現數據丟失,但是數據丟失的概率大大降低。
第三種情況是最安全的做法,但是節點數目多了,同步非常耗時,用户需要等待的時間過長,一般不考慮。
MongoDB 在這裏推薦折衷方案就是使用 Write Concern---在數據可靠性與效率之間的權衡!
db.products.insert( { item: "envelopes", qty : 100, type: "Clasp" }, { writeConcern: { w: "majority" , wtimeout: 5000 } } // 設置writeConcern為majority,超時時間為5000毫秒 )
MongoDB 分片
①大規模數據是如何影響數據庫效率的?
數據庫的性能還與數據庫本身規模息息相關。拿關係型數據庫舉例:
查詢百萬表和千萬表甚至過億的表效率相差很大,查詢性能急劇惡化。
插入的時候創建索引可能會引起索引樹的調整與頁分裂。
②面對海量數據如何提升數據讀寫效率?
為了在海量數據中提升數據庫的效率,我們採用分而治之的思想,將大表拆成小表,大庫拆成小庫。
關係型數據庫中我們常用分表分庫來解決:
例如將訂單庫分為在線庫和離線庫,近三個月是在線庫,遠期的訂單數據放入離線庫,這樣在線庫的數據就大大減少,數據庫性能就得到了提升。
又例如當我們的用户量過多超過千萬行記錄,單表查詢效率下降,我們將一張用户表拆成多張用户表,這個就是水平拆分。
MongoDB 中我們是如何做的呢?
③MongoDBSharding
MongoDB 的分片
通過將同一個集合(Collection1)的數據按片鍵(shard keys)分到不同的分片(shard)上面,減少同一個數據文件上的數據量,已達到拆分數據規模的目的。
Shard 優勢:在線擴容,動態擴容
Shard:用於存儲實際的數據塊,實際生產環境中一個 shard server 角色可由幾台機器組個一個 replica set 承擔,防止主機單點故障。
Config Server:配置服務器 mongodb 實例,存儲了整個集羣的元數據與配置,其中包括 chunk 信息,在 MongoDB 3.4 中,配置服務器必須部署為一個副本集。
Mongos:mongos 充當查詢路由器,提供客户端應用程序和切分集羣之間的接口。
服務器插入的數據通過 Mongos 路由到具體地址,這也是 MongoDB 的便利之處,不需要自己關注路由,也不需要使用第三方提供的中間件輔助路由,可靠,放心。
分片的負載均衡
當我們的 MongoDB 副本集變成分片集羣后,隨着數據量的增長,各個分片也會越來越大。
這裏就會出現兩種情況:
冷熱數據,某個分片數據量過大。
數據總量大,分片集羣的分片過大。
當出現問題(1)的時候,MongoDB 的負載均衡器(Balancer)會自動將大分片中的數據遷往小分片。
注意這並不意味我們可以高枕無憂了,恰恰相反,我們應該反思是不是自己片鍵選擇失誤而造成的數據不均勻!
因為對分片遷移也是消耗性能的,應用服務器寫一次到 Shard B,然後 Shard B 重寫到 Shard C 無形之中數據被寫了兩次,這是極大的浪費!
當出現問題(2)的時候,當然是給過大的分片集合添加新的分片以此分攤分片集羣的壓力。
注意:MongoDB 分片雖然是可在線的,但是多少都會對正常的讀寫操作性能有一定的影響,建議在非繁忙時間段進行分片部署!
MongoDB 文檔模型介紹
數據庫建模的挑戰在於平衡應用的需要,適合該數據庫引擎發揮的結構以及數據的檢索模式。
當我們設計數據模型的時候,需要考慮應用使用數據的情況(查詢,更新,和數據處理)以及該數據本身的結構。
①靈活的 Schema
在關係型數據庫中,必須按照確定的表結構去插入數據。但是,由於 MongoDB 是文檔型數據庫,在插入數據的時候默認並不對此做要求。
其表現在於:
同一個集合中不同文檔不一定需要有相同的字段,並且字段類型也可以不同。
在集合中改變文檔的結構,例如增加一個字段,刪除一個字段,或者改變一個字段的類型,只需要對該文檔更新即可。
②舉例 1:N 模型設計
在電商業務中,一個用户可能有多個收件人以及收件地址。在關係型數據庫中,我們需要建立聯繫人表,地址表,並且將其關聯。但是在 MongoDB 中,我們只需要一個集合就能將此搞定!
數據關係如下:
// patron document { _id: "joe", name: "Joe Bookreader" } // address documents { patron_id: "joe", // reference to patron document street: "123 Fake Street", city: "Faketon", state: "MA", zip: "12345" } { patron_id: "joe", street: "1 Some Other Street", city: "Boston", state: "MA", zip: "12345" }
在 MongoDB 中我們可以這樣進行設計:
{ "_id": "joe", "name": "Joe Bookreader", "addresses": [ { "street": "123 Fake Street", "city": "Faketon", "state": "MA", "zip": "12345" }, { "street": "1 Some Other Street", "city": "Boston", "state": "MA", "zip": "12345" } ] }
沒錯,以上就是集合中的一個 document(文檔),是不是感覺很靈活很方便!
你可以在 SKU 集合中添加分類信息,或者商品標籤,還可以在庫存集合中冗餘 SKU 的基本信息,還可以在訂單集合中冗餘部分下單者信息···沒錯,就是這麼靈活!
這也是我們選擇 MongoDB 的一個重要原因之一,讓開發者的心智負擔少了很多,不需要成為 SQL 高手,你也能在 MongoDB 中寫出性能優異的查詢語句。
當然,“冗餘一時爽,重構火葬場”的段子也不是沒聽過,因為過多的冗餘最終會造成數據的過於臃腫,性能降低等各種問題,這個要控制住開發者的冗餘衝動,也依賴於團隊技術 Leader 對此的把關。
總結
互聯網業務不是一成不變的,產品和用户的需求還有市場都一直在變!我們沒有技術實力打造一個能夠適應靈活多變的業務的中台,但是目前我們可以選擇一個可靠,強大並且靈活的數據庫 MongoDB!
作者:唐銀鵬
簡介:開源愛好者、Gopher。從事電商、IM 系統深度研發,MongoDB 愛好者,公眾號《從菜鳥到大佬》作者。
編輯:陶家龍