轉載:MongoDB 在 58 同城百億量級數據下的應用實踐

爲何要使用 MongoDB?

MongoDB 這個來源英文單詞「humongous」,homongous 這個單詞的意思是「巨大的」、「奇大無比的」,從 MongoDB 單詞自己能夠看出它的目標是提供海量數據的存儲以及管理能力。MongoDB 是一款面向文檔的 NoSQL 數據庫,MongoDB 具有較好的擴展性以及高可用性,在數據複製方面,支持 Master-Slaver(主從)和 Replica-Set(副本集)等兩種方式。經過這兩種方式可使得咱們很是方便的擴展數據。ios

MongoDB 較高的性能也是它強有力的賣點之一,存儲引擎使用的內存映射文件(MMAP 的方式),將內存管理工做交給操做系統去處理。MMAP 的機制,數據的操做寫內存便是寫磁盤,在保證數據一致性的前提下,提供了較高的性能。web

除此以外,MongoDB 還具有了豐富的查詢支持、較多類型的索引支持以及 Auto-Sharding 的功能。在全部的 NoSQL 產品中,MongoDB 對查詢的支持是最相似於傳統的 RDBMS,這也使得應用方能夠較快的從 RDBMS 轉換到 MonogoDB。mongodb

在 58 同城,咱們的業務特色是具備較高的訪問量,並能夠按照業務進行垂直的拆分,在每一個業務線內部經過 MongoDB 提供兩種擴展機制,當業務存儲量和訪問量變大,咱們能夠較易擴展。同時咱們的業務類型對事務性要求低,綜合業務這幾點特性,在 58 同城使用 MongoDB 是較合適的。數據庫

如何使用 MongoDB?

MongoDB 做爲一款 NoSQL 數據庫產品,Free Schema 是它的特性之一,在設計咱們的數據存儲時,不須要咱們固定 Schema,提供給業務應用方較高的自由度。那麼問題來了,Free Schema 真的 Free 嗎?服務器

第一:Free Schema 意味着重複 Schema。在 MongoDB 數據存儲的時候,不但要存儲數據自己,Schema(字段 key)自己也要重複的存儲(例如:{「name」:」zhuanzhuan」, 「infoid」:1,「infocontent」:」這個是轉轉商品」}),必然會形成存儲空間的增大。網絡

第二:Free Schema 意味着 All Schema,任何一個須要調用 MongoDB 數據存儲的地方都須要記錄數據存儲的 Schema,這樣才能較好的解析和處理,必然會形成業務應用方的複雜度。架構

那麼咱們如何應對呢?在字段名 Key 選取方面,咱們儘量減小字段名 Key 的長度,好比:name 字段名使用 n 來代替,infoid 字段名使用 i 來代替,infocontent 字段名使用 c 來代替(例如:{「n」:」zhuanzhuan」, 「i」:1, 「c」:」這個是轉轉商品」})。使用較短的字段名會帶來較差的可讀性,咱們經過在使用作字段名映射的方式( #defineZZ_NAME  ("n")),解決了這個問題;同時在數據存儲方面咱們啓用了數據存儲的壓縮,儘量減小數據存儲的量。併發

MongoDB 提供了自動分片(Auto-Sharding)的功能,通過咱們的實際測試和線上驗證,並無使用這個功能。咱們啓用了 MongoDB 的庫級 Sharding;在 CollectionSharding 方面,咱們使用手動 Sharding 的方式,水平切分數據量較大的文檔。框架

MongoDB 的存儲文檔必需要有一個「_id」字段,能夠認爲是「主鍵」。這個字段值能夠是任何類型,默認一個 ObjectId 對象,這個對象使用了 12 個字節的存儲空間,每一個字節存儲兩位 16 進制數字,是一個 24 位的字符串。這個存儲空間消耗較大,咱們實際使用狀況是在應用程序端,使用其餘的類型(好比 int)替換掉到,一方面能夠減小存儲空間,另一方面能夠較少 MongoDB 服務端生成「_id」字段的開銷。運維

在每個集合中,每一個文檔都有惟一的「_id」標示,來確保集中每一個文檔的惟一性。而在不一樣集合中,不一樣集合中的文檔「_id」是能夠相同的。好比有 2 個集合 Collection_A 和 Collection_B,Collection_A 中有一個文檔的「_id」爲 1024,在 Collection_B 中的一個文檔的「_id」也能夠爲 1024。

MongoDB 集羣部署

MongoDB 集羣部署咱們採用了 Sharding+Replica-Set 的部署方式。整個集羣有 Shard Server 節點(存儲節點,採用了 Replica-Set 的複製方式)、Config Server 節點(配置節點)、Router Server(路由節點、Arbiter Server(投票節點)組成。每一類節點都有多個冗餘構成。知足 58 業務場景的一個典型 MongoDB 集羣部署架構以下所示 [圖 2]:

圖 2 58 同城典型業務 MongoDB 集羣部署架構

在部署架構中,當數據存儲量變大後,咱們較易增長 Shard Server 分片。Replica-Set 的複製方式,分片內部能夠自由增減數據存儲節點。在節點故障發生時候,能夠自動切換。同時咱們採用了讀寫分離的方式,爲整個集羣提供更好的數據讀寫服務。

圖 3 Auto-Sharding MAY is not that Reliable

針對業務場景咱們在 MongoDB 中如何設計庫和表

MongoDB 自己提供了Auto-Sharding的功能,這個智能的功能做爲 MongoDB 的最具賣點的特性之一,真的很是靠譜嗎(圖 3)?也許理想是豐滿的,現實是骨幹滴。

首先是在 Sharding Key 選擇上,若是選擇了單一的 Sharding Key,會形成分片不均衡,一些分片數據比較多,一些分片數據較少,沒法充分利用每一個分片集羣的能力。爲了彌補單一 Sharding Key 的缺點,引入複合 Sharing Key,然而複合 Sharding Key 會形成性能的消耗;

第二count 值計算不許確的問題,數據 Chunk 在分片之間遷移時,特定數據可能會被計算 2 次,形成 count 值計算偏大的問題;

第三Balancer 的穩定性 & 智能性問題,Sharing 的遷移發生時間不肯定,一旦發生數據遷移會形成整個系統的吞吐量急劇降低。爲了應對 Sharding 遷移的不肯定性,咱們能夠強制指定 Sharding 遷移的時間點,具體遷移時間點依據業務訪問的低峯期。好比 IM 系統,咱們的流量低峯期是在凌晨 1 點到 6 點,那麼咱們能夠在這段時間內開啓 Sharding 遷移功能,容許數據的遷移,其餘的時間不進行數據的遷移,從而作到對 Sharding 遷移的徹底掌控,避免掉未知時間 Sharding 遷移帶來的一些風險。

如何設計庫(DataBase)?

咱們的 MongoDB 集羣線上環境所有禁用了 Auto-Sharding 功能。如上節所示,僅僅提供了指定時間段的數據遷移功能。線上的數據咱們開啓了庫級的分片,經過 db.runCommand({「enablesharding」: 「im」}); 命令指定。而且咱們經過 db.runCommand({movePrimary:「im」, to: 「sharding1」}); 命令指定特定庫到某一固定分片上。經過這樣的方式,咱們保證了數據的無遷移性,避免了 Auto-Sharding 帶來的一系列問題,數據徹底可控,從實際使用狀況來看,效果也較好。

既然咱們關閉了 Auto-Sharding 的功能,就要求對業務的數據增長狀況提早作好預估,詳細瞭解業務半年甚至一年後的數據增加狀況,在設計 MongoDB 庫時須要作好規劃:肯定數據規模、肯定數據庫分片數量等,避免數據庫頻繁的重構和遷移狀況發生。

那麼問題來了,針對 MongoDB,咱們如何作好容量規劃?

MongoDB 集羣高性能本質是 MMAP 機制,對機器內存的依賴較重,所以咱們要求業務熱點數據和索引的總量要能所有放入內存中,即:Memory > Index + Hot Data。一旦數據頻繁地 Swap,必然會形成 MongoDB 集羣性能的降低。當內存成爲瓶頸時,咱們能夠經過 Scale Up 或者 Scale Out 的方式進行擴展。

第二:咱們知道 MongoDB 的數據庫是按文件來存儲的:例如:db1 下的全部 collection 都放在一組文件內 db1.0,db1.1,db1.2,db1.3……db1.n。數據的回收也是以庫爲單位進行的,數據的刪除將會形成數據的空洞或者碎片,碎片太多,會形成數據庫空間佔用較大,加載到內存時也會存在碎片的問題,內存使用率不高,會形成數據頻繁地在內存和磁盤之間 Swap,影響 MongoDB 集羣性能。所以將頻繁更新刪除的表放在一個獨立的數據庫下,將會減小碎片,從而提升性能。

第三:單庫單表絕對不是最好的選擇。緣由有三:表越多,映射文件越多,從 MongoDB 的內存管理方式來看,浪費越多;同理,表越多,回寫和讀取的時候,沒法合併 IO 資源,大量的隨機 IO 對傳統硬盤是致命的;單表數據量大,索引佔用高,更新和讀取速度慢。

第四:Local 庫容量設置。咱們知道 Local 庫主要存放 oplog,oplog 用於數據的同步和複製,oplog 一樣要消耗內存的,所以選擇一個合適的 oplog 值很重要,若是是高插入高更新,並帶有延時從庫的副本集須要一個較大的 oplog 值(好比 50G);若是沒有延時從庫,而且數據更新速度不頻繁,則能夠適當調小 oplog 值(好比 5G)。總之,oplog 值大小的設置取決於具體業務應用場景,一切脫離業務使用場景來設置 oplog 的值大小都是耍流氓。

如何設計表(Collection)?

MongoDB 在數據邏輯結構上和 RDBMS 比較相似,如圖 4 所示:MongoDB 三要素:數據庫(DataBase)、集合(Collection)、文檔(Document)分別對應 RDBMS(好比 MySQL)三要素:數據庫(DataBase)、表(Table)、行(Row)。

圖 4 MongoDB 和 RDBMS 數據邏輯結構對比

MongoDB 做爲一支文檔型的數據庫容許文檔的嵌套結構,和 RDBMS 的三範式結構不一樣,咱們以「人」描述爲例,說明二者之間設計上的區別。「人」有如下的屬性:姓名、性別、年齡和住址;住址是一個複合結構,包括:國家、城市、街道等。針對「人」的結構,傳統的 RDBMS 的設計咱們須要 2 張表:一張爲 People 表 [圖 5],另一張爲 Address 表 [圖 6]。這兩張表經過住址 ID 關聯起來(即 Addess ID 是 People 表的外鍵)。在 MongoDB 表設計中,因爲 MongoDB 支持文檔嵌套結構,我能夠把住址複合結構嵌套起來,從而實現一個 Collection 結構 [圖 7],可讀性會更強。

圖 5 RDBMSPeople 表設計

圖 6 RDBMS Address 表設計

圖 7 MongoDB 表設計

MongoDB 做爲一支 NoSQL 數據庫產品,除了能夠支持嵌套結構外,它又是最像 RDBMS 的產品,所以也能夠支持「關係」的存儲。接下來會詳細講述下對應 RDBMS 中的一對1、一對多、多對多關係在 MongoDB 中咱們設計和實現。

IM 用戶信息表,包含用戶 uid、用戶登陸名、用戶暱稱、用戶簽名等,是一個典型的一對一關係,在 MongoDB 能夠採用類 RDBMS 的設計,咱們設計一張 IM 用戶信息表 user:{_id:88, loginname:musicml, nickname:musicml,sign:love},其中 _id 爲主鍵,_id 實際爲 uid。IM 用戶消息表,一個用戶能夠收到來自他人的多條消息,一個典型的一對多關係。

咱們如何設計?

一種方案,採用 RDBMS 的「多行」式設計,msg 表結構爲:{uid,msgid,msg_content},具體的記錄爲:123, 1, 你好;123,2,在嗎。

另一種設計方案,咱們可使用 MongoDB 的嵌套結構:{uid:123, msg:{[{msgid:1,msg_content: 你好},{msgid:2, msg_content: 在嗎}]}}。

採用 MongoDB 嵌套結構,會更加直觀,但也存在必定的問題:更新複雜、MongoDB 單文檔 16MB 的限制問題。採用 RDBMS 的「多行」設計,它遵循了範式,一方面查詢條件更靈活,另外經過「多行式」擴展性也較高。

在這個一對多的場景下,因爲 MongoDB 單條文檔大小的限制,咱們並沒採用 MongoDB 的嵌套結構,而是採用了更加靈活的類 RDBMS 的設計。

在 User 和 Team 業務場景下,一個 Team 中有多個 User,一個 User 也可能屬於多個 Team,這種是典型的多對多關係。

在 MongoDB 中咱們如何設計?一種方案咱們能夠採用類 RDBMS 的設計。一共三張表:Team 表{teamid,teamname, ……},User 表{userid,username,……},Relation 表{refid, userid, teamid}。其中 Team 表存儲 Team 自己的元信息,User 表存儲 User 自己的元信息,Relation 表存儲 Team 和 User 的所屬關係。

在 MongoDB 中咱們能夠採用嵌套的設計方案:一種 2 張表:Team 表{teamid,teamname,teammates:{[userid, userid, ……]},存儲了 Team 全部的 User 成員和 User 表{useid,usename,teams:{[teamid, teamid,……]}},存儲了 User 全部參加的 Team。

在 MongoDB Collection 上咱們並無開啓 Auto-Shariding 的功能,那麼當單 Collection 數據量變大後,咱們如何 Sharding?對 Collection Sharding 咱們採用手動水平 Sharding 的方式,單表咱們保持在千萬級別文檔數量。當 Collection 數據變大,咱們進行水平拆分。好比 IM 用戶信息表:{uid, loginname, sign, ……},可用採用 uid 取模的方式水平擴展,好比:uid%64,根據 uid 查詢能夠直接定位特定的 Collection,不用跨表查詢。

經過手動 Sharding 的方式,一方面根據業務的特色,咱們能夠很好知足業務發展的狀況,另一方面咱們能夠作到可控、數據的可靠,從而避免了 Auto-Sharding 帶來的不穩定因素。對於 Collection 上只有一個查詢維度(uid),經過水平切分能夠很好知足。

可是對於 Collection 上有 2 個查詢維度,咱們如何處理?好比商品表:{uid, infoid, info,……},存儲了商品發佈者,商品 ID,商品信息等。咱們須要即按照 infoid 查詢,又能支持按照 uid 查詢。爲了支持這樣的查詢需求,就要求 infoid 的設計上要特殊處理:infoid 包含 uid 的信息(infoid 最後 8 個 bit 是 uid 的最後 8 個 bit),那麼繼續採用 infoid 取模的方式,好比:infoid%64,這樣咱們既能夠按照 infoid 查詢,又能夠按照 uid 查詢,都不須要跨 Collection 查詢。

數據量、併發量增大,遇到問題及其解決方案

大量刪除數據問題及其解決方案

咱們在 IM 離線消息中使用了 MongoDB,IM 離線消息是爲了當接收方不在線時,須要把發給接收者的消息存儲下來,當接收者登陸 IM 後,讀取存儲的離線消息後,這些離線消息再也不須要。已讀取離線消息的刪除,設計之初咱們考慮物理刪除帶來的性能損耗,選擇了邏輯標識刪除。IM 離線消息 Collection 包含以下字段:msgid, fromuid, touid, msgcontent, timestamp, flag。其中 touid 爲索引,flag 表示離線消息是否已讀取,0 未讀,1 讀取。

當 IM 離線消息已讀條數積累到必定數量後,咱們須要進行物理刪除,以節省存儲空間,減小 Collection 文檔條數,提高集羣性能。既然咱們經過 flag==1 作了已讀取消息的標示,第一時間想到了經過 flag 標示位來刪除:db.collection.remove({「flag」 :1}}; 一條簡單的命令就能夠搞定。表面上看很容易就搞定了?!實際狀況是 IM 離線消息表 5kw 條記錄,近 200GB 的數據大小。

悲劇發生了:晚上 10 點後部署刪除直到早上 7 點還沒刪除完畢;MongoDB 集羣和業務監控斷續有報警;從庫延遲大;QPS/TPS 很低;業務沒法響應。過後分析緣由:雖然刪除命令 db.collection.remove({「flag」 : 1}}; 很簡單,可是 flag 字段並非索引字段,刪除操做等價於所有掃描後進行,刪除速度很慢,須要刪除的消息基本都是冷數據,大量的冷數據進入內存中,因爲內存容量的限制,會把內存中的熱數據 swap 到磁盤上,形成內存中全是冷數據,服務能力急劇降低。

遇到問題不可怕,咱們如何解決呢?首先咱們要保證線上提供穩定的服務,採起緊急方案,找到還在執行的 opid,先把此命令殺掉(kill opid),恢復服務。長期方案,咱們首先優化了離線刪除程序 [圖 8],把已讀 IM 離線消息的刪除操做,每晚定時從庫導出要刪除的數據,經過腳本按照 objectid 主鍵(_id)的方式進行刪除,而且刪除速度經過程序控制,從避免對線上服務影響。其次,咱們經過用戶的離線消息的讀取行爲來分析,用戶讀取離線消息時間分佈相對比較均衡,不會出現比較密度讀取的情形,也就不會對 MongoDB 的更新帶來太大的影響,基於此咱們把用戶 IM 離線消息的刪除由邏輯刪除優化成物理刪除,從而從根本上解決了歷史數據的刪除問題。

圖 8 離線刪除優化腳本

大量數據空洞問題及其解決方案

MongoDB 集羣大量刪除數據後(好比上節中的 IM 用戶離線消息刪除)會存在大量的空洞,這些空洞一方面會形成 MongoDB 數據存儲空間較大,另一方面這些空洞數據也會隨之加載到內存中,致使內存的有效利用率較低,在機器內存容量有限的前提下,會形成熱點數據頻繁的 Swap,頻繁 Swap 數據,最終使得 MongoDB 集羣服務能力降低,沒法提供較高的性能。

經過上文的描述,你們已經瞭解 MongoDB 數據空間的分配是以 DB 爲單位,而不是以 Collection 爲單位的,存在大量空洞形成 MongoDB 性能低下的緣由,問題的關鍵是大量碎片沒法利用,所以經過碎片整理、空洞合併收縮等方案,咱們能夠提升 MongoDB 集羣的服務能力。

那麼咱們如何落地呢?

方案一:咱們可使用 MongoDB 提供的在線數據收縮的功能,經過 Compact 命令(db.yourCollection.runCommand(「compact」);)進行 Collection 級別的數據收縮,去除 Collectoin 所在文件碎片。此命令是以 Online 的方式提供收縮,收縮的同時會影響到線上的服務,其次從咱們實際收縮的效果來看,數據空洞收縮的效果不夠顯著。所以咱們在實際數據碎片收縮時沒有采用這種方案,也不推薦你們使用這種空洞數據的收縮方案。

既然這種數據方案不夠好,咱們能夠採用 Offline 收縮的方案二:此方案收縮的原理是:把已有的空洞數據,remove 掉,從新生成一份無空洞數據。那麼具體如何落地?先預熱從庫;把預熱的從庫提高爲主庫;把以前主庫的數據所有刪除;從新同步;同步完成後,預熱此庫;把此庫提高爲主庫。

具體的操做步驟以下:檢查服務器各節點是否正常運行 (ps -ef |grep mongod);登入要處理的主節點 /mongodb/bin/mongo--port 88888;作降權處理 rs.stepDown(),並經過命令 rs.status() 來查看是否降權;切換成功以後,停掉該節點;檢查是否已經降權,能夠經過 web 頁面查看 status,咱們建議最好登陸進去保證有數據進入,或者是 mongostat 查看; kill 掉對應 mongo 的進程: kill 進程號;刪除數據,進入對應的分片刪除數據文件,好比: rm -fr /mongodb/shard11/*;從新啓動該節點,執行重啓命令,好比:如:/mongodb/bin/mongod --config /mongodb/shard11.conf;經過日誌查看進程;數據同步完成後,在修改後的主節點上執行命令 rs.stepDown() ,作降權處理。

經過這種 Offline 的收縮方式,咱們能夠作到收縮率是 100%,數據徹底無碎片。固然作離線的數據收縮會帶來運維成本的增長,而且在 Replic-Set 集羣只有 2 個副本的狀況下,還會存在一段時間內的單點風險。經過 Offline 的數據收縮後,收縮先後效果很是明顯,如 [圖 9, 圖 10] 所示:收縮前 85G 存儲文件,收縮後 34G 存儲文件,節省了 51G 存儲空間,大大提高了性能。

圖 9 收縮 MongoDB 數據庫前存儲數據大小

圖 10 收縮 MongoDB 數據庫後存儲數據大小

MongoDB 集羣監控

MongoDB 集羣有多種方式能夠監控:mongosniff、mongostat、mongotop、db.xxoostatus、web 控制檯監控、MMS、第三方監控。咱們使用了多種監控相結合的方式,從而作到對 MongoDB 整個集羣徹底 Hold 住。

第一是 mongostat[圖 11],mongostat 是對 MongoDB 集羣負載狀況的一個快照,能夠查看每秒更新量、加鎖時間佔操做時間百分比、缺頁中斷數量、索引 miss 的數量、客戶端查詢排隊長度(讀|寫)、當前鏈接數、活躍客戶端數量 (讀|寫) 等。

圖 11 MongoDB mongostat 監控

mongstat 能夠查看的字段較多,咱們重點關注 Locked、faults、miss、qr|qw 等,這些值越小越好,最好都爲 0;locked 最好不要超過 10%;形成 faults、miss 緣由主要是內存不夠或者內冷數據頻繁 Swap,索引設置不合理;qr|qw 堆積較多,反應了數據庫處理慢,這時候咱們須要針對性的優化。

第二是 web 控制檯,和 MongoDB 服務一同開啓,它的監聽端口是 MongoDB 服務監聽端口加上 1000,若是 MongoDB 的監聽端口 33333,則 Web 控制檯端口爲 34333。咱們能夠經過 http://ip:port(http://8.8.8.8:34333)訪問監控了什麼 [圖 12]:當前 MongoDB 全部的鏈接數、各個數據庫和 Collection 的訪問統計包括:Reads, Writes, Queries 等、寫鎖的狀態、最新的幾百行日誌文件。

圖 12  MongoDB Web 控制檯監控

第三是 MMS(MongoDBMonitoring Service),它是 2011 年官方發佈的雲監控服務,提供可視化圖形監控。工做原理以下:在 MMS 服務器上配置須要監控的 MongoDB 信息(ip/port/user/passwd 等);在一臺可以訪問你 MongoDB 服務的內網機器上運行其提供的 Agent 腳本;Agent 腳本從 MMS 服務器獲取到你配置的 MongoDB 信息;Agent 腳本鏈接到相應的 MongoDB 獲取必要的監控數據;Agent 腳本將監控數據上傳到 MMS 的服務器;登陸 MMS 網站查看整理事後的監控數據圖表。具體的安裝部署,能夠參考:http://mms.10gen.com。

圖 13 MongoDB MMS 監控

第四是第三方監控,MongoDB 開源愛好者和團隊支持者較多,能夠在經常使用監控框架上擴展,好比:zabbix,能夠監控 CPU 負荷、內存使用、磁盤使用、網絡情況、端口監視、日誌監視等;nagios,能夠監控監控網絡服務(HTTP 等)、監控主機資源(處理器負荷、磁盤利用率等)、插件擴展、報警發送給聯繫人(EMail、短信、用戶定義方式)、手機查看方式;cacti,能夠基於 PHP,MySQL,SNMP 及 RRDTool 開發的網絡流量監測圖形分析工具。

最後我要感謝公司和團隊,在 MongoDB 集羣的大規模實戰中積累了寶貴的經驗,才能讓我有機會撰寫了此文,因爲 MongoDB 社區不斷髮展,特別是 MongoDB 3.0,對性能、數據壓縮、運維成本、鎖級別、Sharding 以及支持可插拔的存儲引擎等的改進,MongoDB 愈來愈強大。文中可能會存在一些不妥的地方,歡迎你們交流指正。

講師介紹

孫玄,極客邦培訓專家講師,58 同城系統架構師、技術委員會架構組主任、產品技術學院優秀講師,58 同城即時通信、C2C 技術負責人,負責 58 核心系統的架構以及優化工做。分佈式系統存儲專家,2007 年開始從事大規模高性能分佈式存儲系統架構設計實現工做。涉及自主研發分佈式存儲系統、MongoDB、MySQL、Memcached、Redis 等。前百度高級工程師,參與社區搜索部多個基礎系統的設計與實現。

相關文章
相關標籤/搜索