讀古今文學網 > MongoDB實戰 > 9.1 分片概述 >

9.1 分片概述

在你構建第一個分片集群之前,有必要理解什麼是分片以及為什麼有時它能適用。為什麼分片很重要?對此的說明是整個MongoDB項目的核心選擇理由之一。一旦理解了為什麼分片如此重要,你將欣喜地瞭解到組成分片集群的核心組件,還有構成MongoDB分片機制的關鍵概念。

9.1.1 何謂分片

到目前為止,你都是把MongoDB當做一台服務器在用,每個mongod實例都包含應用程序數據的完整副本。就算使用了複製,每個副本也都完整克隆了其他副本的數據。對於大多數應用程序而言,在一台服務器上保存完整數據集是完全可以接受的。但隨著數據量的增長,以及應用程序對讀寫吞吐量的要求越來越高,普通服務器漸漸顯得捉襟見肘了。尤其是這些服務器可能無法分配足夠的內存,或者沒有足夠的CPU核數來有效處理工作負荷。除此之外,隨著數據量的增長,要在一塊磁盤或一組RAID陣列上保存和管理備份如此大規模的數據集也變得不太現實了。如果還想繼續使用普通硬件或者虛擬硬件來托管數據庫,那麼針對這類問題的解決方案就是將數據庫分佈在多台服務器上。這種方法稱為分片。

為數眾多的Web應用程序,知名的有Flickr和LiveJournal,都實現了手動分片,將負載分佈到多台MySQL數據庫上。在這些實現中,分片邏輯都寄生於應用程序之中。要明白這是如何實現的,想像一下,假設你有很多用戶,需要將Users表分佈到多台數據庫服務器上。你可以指定一台數據庫作為元數據庫。這台數據庫包含每個用戶ID(或者用戶ID範圍)到指定分片映射關係的元數據。因此,要查詢一個用戶實際涉及兩次查詢:第一次查詢訪問元數據庫以獲得用戶的分片位置,第二次查詢直接訪問包含用戶數據的分片。

對於這些Web應用程序而言,手動分片解決了負載問題,但其實現並非無懈可擊。最明顯的問題是遷移數據非常困難。如果單個分片負載過重,將其中的數據遷移到其他分片的過程完全是手動的。手動分片的第二個問題在於要編寫可靠的應用程序代碼對讀寫請求進行路由,並且將數據庫作為一個整體進行管理,這也是非常困難的。最近出現了一些管理手動分片的框架,最著名的就是Twitter的Gizzard(詳見http://mng.bz/4qvd)。

但正如那些手動分片數據庫的人所說的,要把事情做好並非易事。MongoDB中有一大塊工作就是為了解決該問題。因為分片是MongoDB的核心內容,所以用戶無需擔心在需要水平擴展時要自己設計外置分片框架。在處理困難的跨分片數據均衡問題時,這點尤為重要。這些代碼並非那種大多數人能在一個週末裡寫出來的東西。

也許最值得一提的是MongoDB在設計時為應用程序提供了統一的接口,無論是在分片前,還是在分片後。也就是說,在數據庫需要轉換為分片架構時,應用程序代碼幾乎無需改動。

現在你應該對自動分片背後的邏輯有點感覺了。在詳細描述MongoDB的分片過程前,讓我們停下腳步,回答另一個擺在面前的問題:何時需要分片?

何時分片

這個問題的答案比你想的要簡單得多。我們之前已經說過把索引和工作數據集放在內存裡是很重要的,這也是分片的主要原因。如果應用程序的數據集持續無限增長,那麼遲早有一天,內存會容納不下這些數據。如果你正使用亞馬遜的EC2,那麼這個閾值是68 GB,因為這是本書編寫時EC2最大的實例所能提供的內存總數。或者,你可以運行自己的硬件,並使用遠高於68 GB的內存,這樣便能延後一段時間再做分片。但沒有哪台機器的內存是無限的,因此你早晚都會用到分片。

不可否認,還有一些其他的應對措施。舉例來說,如果你有自己的硬件,而且可以將所有數據都保存在固態硬盤上(它的成本越來越能為人所接受了),那麼可以增加數據內存比,而不會為性能帶來負面影響。還有一種情況,工作集是總數據量中的一部分,這時可以使用相對較小的內存。另一方面,如果有特殊的寫負載要求,那麼可以在數據達到內存大小之前先進行適當的分片,原因是需要將負載分到多台機器上,以便能夠獲得想要的寫吞吐量。

無論哪種情況,對現有系統進行分片的決定都要基於以下幾點——磁盤活動、系統負載以及最重要的工作集大小與可用內存的比例。

9.1.2 分片的工作原理

要理解分片是如何工作的,你需要瞭解構成分片集群的組件,理解協調那些組件的軟件進程,這就是接下來的主題。

1. 分片組件

分片集群由分片、mongos路由器和配置服務器組成。我們所要討論的組件如圖9-1所示。

圖9-1 MongoDB分片集群中的組件

  • 分片

MongoDB分片集群將數據分佈在一個或多個分片上。每個分片都部署成一個MongoDB副本集,該副本集保存了集群整體數據的一部分。因為每個分片都是一個副本集,所以它們擁有自己的複製機制,能夠自動進行故障轉移。你可以直接連接單個分片,就像連接單獨的副本集那樣。但是,如果連接的副本集是分片集群的一部分,那麼你只能看到部分數據。

  • mongos路由器

如果每個分片都包含部分集群數據,那麼還需要一個接口連接整個集群,這就是mongosmongos進程是一個路由器,將所有的讀寫請求指引到合適的分片上。如此一來,mongos為客戶端提供了一個合理的系統視圖。

mongos進程是輕量級且非持久化的。它們通常運行於與應用服務器相同的機器上,確保對任意分片的請求只經過一次網絡跳轉。換言之,應用程序連接本地的mongos,而mongos管理了指向單獨分片的連接。

  • 配置服務器

如果mongos進程是非持久化的,那麼必須有地方能持久保存集群的公認狀態;這就是配置服務器的工作,其中持久化了分片集群的元數據,該數據包括:全局集群配置;每個數據庫、集合和特定範圍數據的位置;一份變更記錄,保存了數據在分片之間進行遷移的歷史信息。

配置服務器中保存的元數據是某些特定功能和集群維護時的重中之重。舉例來說,每次有mongos進程啟動,它都會從配置服務器獲取一份元數據的副本。沒有這些數據,就無法獲得一致的分片集群視圖。該數據的重要性對配置服務器的設計和部署策略也有影響。

查看圖9-1,你會看到三個配置服務器,但它們並不是以副本集的形式部署的。它們比異步複製要求更嚴格;mongos進程向配置服務器寫入時,會使用兩階段提交。這能保證配置服務器之間的一致性。在各種生產環境的分片部署中,必須運行三個配置服務器,這些服務器都必須部署在獨立的機器上以實現冗余。1

1. 你也可以運行單個配置服務器,但這只能作為簡單測試分片的一種手段。在生產環境裡只用一台配置服務器就好比乘坐單引擎噴氣飛機橫跨大西洋:它能帶你飛過去,但是一旦失去一個引擎,你就得游泳了。

現在你瞭解了分片集群的構成,但也許還對分片機制本身心存疑惑。數據究竟是如何分佈的?接下來我會介紹一些核心分片操作,對此做出解釋。

2. 核心分片操作

MongoDB分片集群在兩個級別上分佈數據。較粗的是以數據庫為粒度的,在集群裡新建數據庫時,每個數據庫都會被分配到不同的分片裡。如果不進行什麼別的設置,數據庫以及其中的集合永遠都會在創建它的分片裡。

因為大多數應用程序都會把所有的數據保存在一個物理數據庫裡,因此這種分佈方式帶來的幫助不大。你需要更細粒度的分佈方式,集合的粒度剛好能滿足要求。MongoDB的分片是專門為了將單獨的集合分佈在多個分片裡而設計的。要更好地理解這點,讓我們一起想像一下在真實的應用程序裡這是如何工作的。

假設你正在構建一套基於雲的辦公套件,用於管理電子錶格,並且要將所有的數據都保存在MongoDB裡。2 用戶可以隨心所欲地創建大量文檔,每個文檔都會保存為單獨的MongoDB文檔,放在一個spreadsheets集合裡。隨著時間的流逝,假設你的應用程序發展到了擁有100萬用戶。現在再想想那兩個主要集合:usersspreadsheetsusers集合還比較容易處理。就算有100萬用戶,每個用戶文檔1 KB,整個集合大概也就1 GB,一台機器就能搞定了。但spreadsheets集合就大不一樣了,假設每個用戶平均擁有50張電子錶格,平均大小是50 KB,那麼我們所談論的就是1 TB的spreadsheets集合。要是這個應用程序的活躍度很高,你會希望將數據放在內存裡。要將數據放在內存裡並且分佈讀寫負載,你就必須將集合分片。這時分片就該登場了。

2. 可以參考一下Google Docs之類的產品,Google Docs允許用戶創建電子錶格和演示幻燈片。

  • 分片一個集合

MongoDB的分片是基於範圍的。也就是說分片集合裡的每個文檔都必須落在指定鍵的某個值範圍裡。MongoDB使用所謂的分片鍵(shard key)讓每個文檔在這些範圍裡找到自己的位置。3從假想的電子錶格管理應用程序裡拿出一個示例文檔,這樣你能更好地理解分片鍵:

3. 其他的分佈式數據庫裡可能使用分區鍵(partition key)或分佈鍵(distribution key)來代替分片鍵這個術語。

{
  _id: ObjectId(\"4d6e9b89b600c2c196442c21\")
  filename: \"spreadsheet-1\",
  updated_at: ISODate(\"2011-03-02T19:22:54.845Z\"),
  username: \"banks\",
  data: \"raw document data\"
}
  

在對該集合進行分片時,必須將其中的一個或多個字段聲明為分片鍵。如果選擇_id,那麼文檔會基於對像ID的範圍進行分佈。但是,出於一些原因(稍後會做說明的),你要基於username_id聲明一個復合分片鍵;因此,這些範圍通常會表示為一系列用戶名。

現在你需要理解塊(chunk)的概念,它是位於一個分片中的一段連續的分片鍵範圍。舉例來說,可以假設docs集合分佈在兩個分片A和B上,它被分成了如表9-1所示的多個塊。每個塊的範圍都由起始值和終止值來標識。

表9-1 塊與分片

起 始 值終 止 值分  片 -∞abbotB abbotdaytonA daytonharrisB harrisnorrisA norris∞B

粗略掃一眼表9-1,你會發現塊的一個重要的、有些違反直覺的屬性:雖然每個單獨的塊都表示一段連續範圍的數據,但這些塊能出現在任意分片上。

關於塊,第二個要點是它們是種邏輯上的東西,而非物理上的。換言之,塊並不表示磁盤上連續的文檔。從一定程度上來說,如果一個從harris開始到Norris結束的塊存在於分片A上,那麼就認為可以在分片A的docs集合裡找到分片鍵落在這個範圍裡的文檔。這和集合裡那些文檔的排列沒有任何必然關係。

  • 拆分與遷移

分片機制的重點是塊的拆分(splitting)與遷移(migration)。

首先,考慮一下塊拆分的思想。在初始化分片集群時,只存在一個塊,這個塊的範圍涵蓋了整個分片集合。那該如何發展到有多個塊的分片集群呢?答案就是塊大小達到某個閾值時會對塊進行拆分。默認的塊的最大塊尺寸是64 MB或者100 000個文檔,先達到哪個標準就以哪個為準。在向新的分片集群添加數據時,原始的塊最終會達到某個閾值,觸發塊的拆分。這是一個簡單的操作,基本就是把原來的範圍一分為二,這樣就有了兩個塊,每個塊都有相同數量的文檔。

請注意,塊的拆分是個邏輯操作。當MongoDB進行塊拆分時,它只是修改塊的元數據就能讓一個塊變成兩個。因此,拆分一個塊並不影響分片集合裡文檔的物理順序。也就是說拆分既簡單又快捷。

你可以回想一下,設計分片系統時最大的一個困難就是保證數據始終均勻分佈。MongoDB的分片集群是通過在分片中移動塊來實現均衡的。我們稱之為遷移,這是一個真實的物理操作。

遷移是由名為均衡器(balancer)的軟件進程管理的,它的任務就是確保數據在各個分片中保持均勻分佈。通過跟蹤各分片上塊的數量,就能實現這個功能。雖然均衡的觸發會隨總數據量的不同而變化,但是通常來說,當集群中擁有塊最多的分片與擁有塊最少的分片的塊數差大於8時,均衡器就會發起一次均衡處理。在均衡過程中,塊會從塊較多的分片遷移到塊較少的分片上,直到兩個分片的塊數大致相等為止。

如果現在你還不太理解,不用擔心。下一節裡我會通過一個示例集群來演示分片,通過實踐來進一步闡述分片鍵和塊的概念。