MongoDB是當前比較流行的文檔型數據庫,其擁有易使用、易擴展、功能豐富、性能卓越等特性。MongoDB本身就擁有高可用及分區的解決方案,分別為副本集(Replica Set)和分片(sharding),下面我們主要看這兩個特性。
1.副本集
有人說MongoDB副本集至少需要三個節點,但其實這句是有問題的,因為副本集中節點最少可以是一台,3.0之前最多12個節點,3.0開始節點數量能夠達到50個。但節點數1個或者2個的時候,MongoDB就無法發揮副本集特有的優勢,因此我們一般建議節點數大於3個。

首先,我們看一下MongoDB副本集的各種角色。
Primary: 主服務器,只有一組,處理客戶端的請求,一般是讀寫
Secondary: 從服務器,有多組,保存主服務器的數據副本,主服務器出問題時其中一個從服務器可提升為新主服務器,可提供只讀服務
Hidden:一般只用於備份節點,不處理客戶端的讀請求
Secondary-Only:不能成為primary節點,只能作為secondary副本節點,防止一些性能不高的節點成為主節點
Delayed:slaveDelay來設置,為不處理客戶端請求,一般需要隱藏
Non-Voting:沒有選舉權的secondary節點,純粹的備份數據節點。
Arbiter: 仲裁節點,不存數據,只參與選舉,可用可不用
然后我們思考一下MongoDB副本集是通過什么方式去進行同步數據的,我們了解Oracle的DataGuar同步模式,我們也了解MySQL主從同步模式,他們都是傳輸日志到備庫然后應用的方法,那么不難想象,MongoDB的副本集基本也是這個路子,這里就不得不提到同步所依賴的核心Oplog。Oplog其實就像MySQL的Binlog一樣,記錄着主節點上執行的每一個操作,而Secondary通過復制Oplog並應用的方式來進行數據同步。Oplog的大小是固定的,默認分配5%的可用空間(64位),當然我們也可以用–oplogSize選項指定具體大小,設置合適的大小在生產應用中是非常重要的一個環節,大家可能疑惑為什么?這是因為Oplog和MySQL Binary不同,它是循環復用的,它又和Oracle的日志不同,沒有多組重做日志,也沒有歸檔日志。Oplog就是一個大小固定、循環復用的日志文件,當Secondary落后Primary很多,直到oplog被復寫,那只能重新全量同步,而拉取全量同步代價特別高,直接影響Primary的讀寫性能。
大家還可能會問MongoDB副本集是實時同步嗎?這其實也是在問數據庫一致性的問題。MySQL的半同步復制模式保證數據庫的強一致,Oracle DataGuard的最大保護模式也能夠保證數據庫的強一致,而MongoDB可以通過getLastError命令來保證寫入的安全,但其畢竟不是事務操作,無法做到數據的強一致。
MongoDB副本集Secondary通常會落后幾毫秒,如果有加載問題、配置錯誤、網絡故障等原因,延遲可能會更大。

MongoDB副本集本身就持有故障切換(Failover)、手動切換(Switchover)以及讀寫分離的功能,大家可能會關心MongoDB副本集如何選舉、如何防止腦裂等等問題,這個先別着急,放到下面去說。MongoDB副本集默認是把讀寫壓力都請求到Primary節點上,但我們可以通過設置setSlaveOk來把讀壓力放在各個Secondary上,MongoDB驅動還提供五種讀取策略(Read Preferences),如下:
- primary:默認參數,只從主節點上進行讀取操作;
- primaryPreferred:大部分從主節點上讀取數據,只有主節點不可用時從secondary節點讀取數據;
- secondary:只從secondary節點上進行讀取操作,存在的問題是secondary節點的數據會比primary節點數據“舊”;
- secondaryPreferred:優先從secondary節點進行讀取操作,secondary節點不可用時從主節點讀取數據;
- nearest:不管是主節點、secondary節點,從網絡延遲最低的節點上讀取數據。
下面看一下MongoDB副本集選舉的方法,選舉我們可以簡單理解為如何從集群節點中選擇合適的節點提升為Primary的過程。跟很多NoSQL數據庫一樣,MongoDB副本集采用的是Bully算法,具體說明請見wiki文檔。
大致思想是集群中每個成員都能夠聲明自己是主節點並通知到其他節點,被其他節點接受的節點才能成為主節點。MongoDB副本集有着“大多數”的概念,在進行選舉時必須遵循”大多數”規則,節點在得到大多數支持時才能成為主節點,而副本集中節點存活數量必須大於“大多數”的數量。

MongoDB到了3.0之后副本集成員個數突破到了50個,但12個以上節點開始“大多數”都為7。
MongoDB在下面幾個條件觸發之下進行選舉:
- 初始化副本集時;
- 備份節點無法和主節點通訊時(可能主節點宕或網絡原因);
- Primary手動降級,rs.stepDown(sec),默認60s。
接着看下選舉的步驟:
- 得到每個服務器節點的最后操作時間戳。每個mongodb都有oplog機制會記錄本機的操作,方便和主服務器進行對比數據是否同步還可以用於錯誤恢復;
- 如果集群中大部分服務器宕機了,保留活着的節點都為 secondary狀態並停止選舉;
- 如果集群中選舉出來的主節點或者所有從節點最后一次同步時間看起來很舊了,停止選舉等待人工操作;
- 如果上面都沒有問題就選擇最后操作時間戳最新(保證數據最新)的服務器節點作為主節點。
有些人可能在設計MongoDB副本集架構過程中會產生成員節點必須是奇數個的誤區,MongoDB副本集成員節點數量為偶數個會有問題嗎?

上圖中我們很清晰的看到,在單機房內不管副本集成員節點數為偶數還是奇數都是沒有問題的,但如果是兩個機房,每個機房的成員節點數量一致,在兩個機房之間心跳中斷時,整個集群就會出現無法選舉Primary的問題,這就是MongoDB副本集中的腦裂。那如何防止腦裂?從架構角度去看的話,我們如下推薦:

左圖為”大多數”成員都在一個數據中心
需求:副本集的Primary總在主數據中心
缺點:如果主數據中心掛了,沒有可用Primary節點
右圖為兩個數據中心成員數量相同,第三個地方放一個用於決定勝負的副本節點(可是仲裁節點)
需求:跨機房容災
缺點:額外需要第三個機房
所以說,MongoDB副本集成員數量奇數個的說法其實針對的是多機房部署的場景下。
另外,在設計MongoDB副本集的過程中,我們還需要考慮過載的問題,因為過載導致MongoDB數據庫性能極差。因此,一定衡量好讀取的量,充分考慮讀寫節點宕機的可能性。

MongoDB副本集還有些同步、心跳、回滾等概念,我簡單整理了下。
同步
初始化同步:從副本集中其他節點全量同步一次,觸發條件:
- Secondary節點首次加入時;
- Secondary節點落后oplog大小以上的數據時;
- 回滾失敗時。
保持同步:初始化同步之后的增量同步
注:同步源並非是Primary節點,MongoDB根據Ping時間選擇同步源。選擇同步源時,會選擇一個離自己比較近的而且數據比自己新的成員。
心跳
- Primary節點是? 哪個節點宕了?哪個節點可以作為同步源? — 心跳來解決;
- 每個節點每2s向其他節點發送心跳請求,根據其結果來維護自己的狀態視圖;
- Primary節點通過心跳來知道自己是否滿足”大多數”條件,如果不滿足,它就會退位變成Secondary。
回滾
Primary執行了寫請求之后宕機,Secondary節點還沒來得及復制本次的寫操作,也就意味着新選舉的Primary上沒有這個寫操作。這時候原Primary恢復並成為Secondary時,需要回滾這個寫操作以能夠重新進行同步。回滾數據量大於300M或者需要回滾的時間超過30分鍾,回滾就會失敗,必須重新全量同步。
2.分片
分片(sharding)其實就是數據拆分,把數據分散在多個節點上,也就是水平拆分。MongoDB支持自動分片,無論自動分片有多優點或缺點,MongoDB依然擁有該特性而引以為傲。
MongoDB分片適用於如下幾個場景:
- 單個服務器無法承受壓力時,壓力包括負載、頻繁寫、吞吐量等;
- 服務器磁盤空間不足時;
- 增加可用內存大小,以更多的數據在內存中訪問。

如上圖所示,MongoDB分片共有三個組件,介紹如下:
mongos:數據庫集群請求的入口,起到路由作用,它負責把對應的數據請求請求轉發到對應的shard服務器上。生產環境中需要多個mongos。
config server:保存集群和分片的元數據,mongos啟動時會加載配置服務器上的配置信息,以后如果配置服務器信息變化會通知到所有的 mongos 更新自己的狀態,。生產環境需要多個 配置服務器。也需要定期備份。
shard server:實際存儲數據的分片。生產環境要求是副本集。
下面我簡單畫了一下分片的過程:

在分片之前,可以把一個集合看成是單一整塊兒,所有文檔都包含在這個塊當中。

在選擇片鍵進行分片之后,集合被拆分成為多個數據塊兒,這時候第一個塊和最后一個塊兒中將出現$minKey和$maxKey,分別表示負無窮和正無窮,當然這都是MongoDB分片內部使用的,我們只要了解即可。

接下來拆分出來的數據塊兒將會均勻分布到各個節點上。
那有些人可能會疑問塊是怎么拆分的?我依然畫了4個圖來解釋。

- mongos記錄每個塊中的數據量,達到某個閾值,就檢查是否需要拆分塊;
- 如拆分塊,mongos更新config server的塊元數據;
- config server誕生新塊,修改舊塊的范圍(拆分點);
- 拆分完畢,mongos重置原始塊的追蹤以及新建新塊追蹤;
注:這里的塊(chunk)是邏輯的概念,一個 chunk 並不是實際存儲的一個頁或者一個文件之類,而是僅僅在 config 節點中的元數據中體現。也就是說,拆分塊只修改元數據,並不進行數據移動。
拆分塊兒的過程也是有隱患的,比如找不到拆分點而導致產生特大塊兒,還有配置服務器不可達導致拆分風暴等。

選擇片鍵不合理:mongos發現塊達到閾值,然后請求分片拆分塊,但分片卻找不到拆分點,這樣導致塊越來越大。
連鎖反應:片鍵不合理–>特大塊(無法拆開)–>塊無法移動–>造成數據分布不均衡–>進而數據寫入不均衡–>進一步加劇了數據分布不均衡
預防:正確選擇片鍵
config server不可達:mongos進行拆分時無法和配置服務器通訊,也就無法更新元數據,這導致一個循環的現象:嘗試拆分和拆分失敗之間來回切換,進而會影響mongos和當前分片的性能。這種不斷重復發起拆分請求卻無法進行拆分的過程,叫做拆分風暴(split storm)。
預防:
1) 保證配置服務器的可用狀態
2) 重啟mongos,重置寫入計數器
說了這么多,我們還不知道怎么創建分片,分為兩種:
- 從零開始創建分片:一般是新業務上線,架構設計初就選用分片;
- 將副本集轉換為分片:服務運行一段時間,單個副本集已無法滿足需求,需要轉換為分片;
第一種從零開始創建分片沒什么可說的,選擇好片鍵尤為關鍵,第二種副本集轉換為分片,有如下過程:
- 部署好config server和mongos;
- 連接mongos,將原有的副本集添加到集群,該副本集將會成為第一個分片;
- 部署好其他副本集,也添加到集群中;
- 修改客戶端配置,所有訪問入口改為mongos;
- 選擇片鍵,啟用分片。
注:在已存在的集合中進行分片,需保證片鍵上有索引,如果沒有,需要先創建。
MongoDB分片中有個非常重要的組件叫均衡器(balancer),實際上是由mongos去扮演這個角色的。均衡器負責塊(chunk)的遷移,它會周期性檢查分片之間塊的均衡情況,如不均衡,就開始塊遷移。塊的遷移並不影響應用程序的訪問與使用,在遷移之前,讀寫都會請求到舊的塊兒上。如果元數據更新完成,那所有試圖訪問舊位置數據的mongos進程都會得到一個錯誤,這些錯誤對客戶端是無感知的,mongos會靜默處理掉這些錯誤,在新的分片上重演一次。
這里大家可能會產生一個誤區 — 分片依據數據大小,請記住,分片間衡量均衡的標准是塊的數量,並非是數據的大小。
有些場景下塊遷移也會導致影響性能,比如使用熱點片鍵時,因為所有的新塊都在熱點上創建,系統就需要處理源源不斷寫入到熱點分片上的數據;再比如向集群添加新的分片時,均衡器觸發一系列遷移過程。
說半天我們還不知道塊遷移是怎么做的,簡單整理過程如下:
- 均衡器的進程向源分片發送moveChunk指令;
- 源分片開始移動塊,期間在此塊上的所有操作都路由到源分片上;
- 目標分片創建源分片上所有的索引,除非目標分片上已有索引;
- 目標分片開始請求塊中的文檔並接收數據副本;
- 在接收完最后一條文檔之后,目標分片開始同步移動塊期間產生的所有變化;
- 當完全同步之后,目標分片更新配置服務器的元數據(塊的新地址);
- 更新完元數據,確認塊兒上沒有打開的游標,源分片就會刪除數據副本。
上面介紹了MongoDB架構及分片的過程,但其實MongoDB分片中最重要的環節就是正確選擇片鍵。何為片鍵?集合中選擇一或兩個字段進行數據的拆分,這個鍵叫作片鍵。我們應該分片初期選好片鍵,運行之后修改片鍵非常難。
如何選好片鍵?我們首先從數據分發的角度去分析一下片鍵,數據分發常用方式:
- 升序片鍵:隨着時間穩定增長的鍵,比如date、或ObjectId。
** MongoDB本身沒有自增主鍵。
現象:新增數據集中在某一個分片上
弊端:MongoDB忙於處理數據的均衡
- 隨機分發的片鍵:數據集中沒有規律的鍵,如用戶名、MD5值、郵件地址、UUID等
現象:各分片增長速度基本相同,減少遷移的次數
弊端:隨機請求數據超出可用內存大小時效率不高
- 基於位置的片鍵:此處”位置”是抽象的概念,如IP地址、經緯度或者地址。
現象:所有與該鍵值接近的文檔都會保存在同一范圍的塊中。
我們還可以根據應用類型不同選擇合適的片鍵,其策略如下:
- 散列片鍵(Hashed Shard Key):隨機分發。
應用類型:追求數據加載速度快,在大量查詢中使用升序鍵,同時也希望寫入數據隨機分發
弊端:無法通過散列片鍵做指定目標的范圍查詢
注:不能使用unique選項,不能使用數組字段,浮點型的值會先被取整
- GridFS的散列片鍵:GridFS非常適合做分片,因為可以包含大量的文件數據
- 流水策略:集群中某服務器性能更加(如SSD),使用標簽+升序片鍵方案讓該服務器處理更多的負載
弊端:如果請求超出了強大服務器的處理能力,想要負載均衡到其他服務器並不簡單
OK,關於片鍵我們研究到這里,總而言之,如果選用MongoDB分片,從分片初期就根據你的應用類型正確選擇片鍵,這樣才能讓分片發揮最佳性能,進而你的應用擁有出色的表現能力。
MongoDB分片的講解也接近尾聲了,最后我們簡單看一下自動分片(auto-sharding)和手動分片(pre-spliting)。
auto-sharding
雖然官方說數據遷移操作對讀寫影響很小,但是在這個過程中可能會把內存中的熱數據擠出去,這也就增大了IO壓力。因此可以考慮平時關閉自動平衡,選擇壓力小的時間去進行。
並且mongos移動Chunk是單線程的,單個mongos每次只能移動一個塊。
pre-spliting
通常也叫manual-sharding,需要提前關閉auto balance。在這種場景下我們需要充分了解自己的數據分布情況,對數據進行預先划分,也就是為每個分片划分出合適大小的片鍵范圍,然后配合手動move chunk來實現手動分片。
自動分片是一個非常理想的選擇,但自動分片在真實的應用場景中還是會有很多的坑,除非我們在這條路上不斷踩坑並不斷填坑,擁有足夠的實力、足夠的經驗,掌控好其每個細節,那我們不妨可以選擇自動分片。但很多公司還是避開這條路,選擇手動分片方式,其最大原因就是手動分片可控能力強。
