mongo——分片(sharding)

概念

隨着應用系統規模的增長,成本會變得越來越高,而且有時候無法實現使用單臺機器來處理負載壓力。這種問題的一個解決方案就是匯聚大量低價、低處理能力的機器來解決問題。mongodb的分片就是爲解決這種問題而設計的:把超大數據使用更小的片進行分區存儲,這樣就不需要在單個機器上存儲所有的數據或者承擔全部壓力。mongodb分片對於應用系統是透明的,這意味着對於分片集羣的查詢與可複製或者單個mongo服務器實例的查詢完全一樣。

什麼時候分片?

什麼時候分片的問題在理論上非常簡單,但是必須完全理解系統是怎麼使用的。通常有兩種分片情況:

1.存儲分片式

1.負載分片式:

分片架構

1.分片:存儲了應用程序的數據。在分片集羣中,只有mongos路由器或者系統管理員可以直接連接分片服務器節點。與不分片的部署一樣,每個分片可以單獨作爲開發和測試的節點,但是生產環境下應該是個可複製集。

2.mongos路由器(中心):緩存了集羣的元數據並使用它來路由操作到正確的分片服務器。

3.配置服務器(右上):一直存儲集羣的元數據,包括哪個分片包含哪些數據集

分片:存儲應用程序數據

分片,如圖12.1左上角所示的,或者是單個mongod服務器或者是一個存儲部分應用數據的可複製集。事實上,分片是分片集羣中應用程序數據存儲的唯一位置。用於測試,一個片可以是單個的mongod服務器,但是在生產環境中應該是可複製集,因爲它會有自己的複製機制並且可以自動災備和故障轉移。可以連接到一個片,也可以連接到單個服務器節點或者可複製集。但是如果嘗試在這個片上直接運行操作,則只能看到整個集羣數據中的一部分。

mongos路由:路由操作

因爲每個分片包含的只是總數據的一部分,所以需要某個東西來路由操作到適當的片上。這就是mongos的作用。mongos進程如上圖所示,是個可以直接轉發所有讀、寫命令到正確的分片上的路由器。mongos提供給客戶端單點連接集羣的方式,這使得整個集羣看起來就像單個節點一樣。

mongos進程是輕量級的、不持久化的。因此,它們通常部署在應用程序服務器上,以確保只有一個網絡節點來轉發請求,。換句話來說,應用程序連接本地的mongos,而mongos管理與每個分片服務器的連接

配置服務器:存儲元數據

mongos進程是非持久化的,這意味着必須有某個東西來存儲集羣的元數據。這個工作就由配置服務器來做,如圖右上角所示。這些元數據包含全局的集羣配置信息,每個數據庫,集合、特定數據的範圍的位置,以及保存了跨片數據遷移歷史的一個修改日誌。

配置服務器保存的元數據是集羣正常工作和更新維護的關鍵。例如,每次mongos啓動,mongos都會從配置服務器獲取一份元數據拷貝。沒有這些數據,就沒有辦法完整預覽整個集羣。這些元數據的重要性體現在通過配置服務器來保存分片集羣設計和部署的策略教程。

上圖有3臺配置服務器,但不是可複製集。它們需要比異步複製更強大的東西;當mongos進程寫入它們時,使用的是兩階段提交協議。這樣可以確保跨配置服務器的一致性。我們必須在分片集羣生產環境下部署3臺配置服務器,而且這些服務器必須部署在單獨的冗餘機器上。

在分片集羣中分散數據

從最裏面依次向外看,可以看到mongodb有4種不同的分片級別:文檔、塊、集合、數據庫。這4種不同的粒度級別代表着mongodb中不同的數據單位。

1.文檔:mongodb中最小的數據單元。文檔表示系統中不能夠再分了的單個對象。我們可以把它與關係型數據庫中的行相比較。注意,我們把文檔及其字段作爲一個原子性單位。

2.塊:根據某個字段的值包換的一組文檔,塊只是在集羣組裏存在的概念。這是根據一個或者一組字段的值進行邏輯分組,這些字段稱爲分片鍵。如上圖,塊就是所有username的值在‘bakkum’和‘verch’之間的文檔。

3.集合:數據庫中一組命名的文檔集合。允許用戶把數據庫分組爲有意義的邏輯組,mongodb提供了集合概念。這就是一組命名的文檔,而且它必須被應用顯示指定才能進行查詢。

4.數據庫:包含文檔的集合。這是系統中最頂級的命名組。因爲數據庫包含文檔的集合,所以集合也必須指定在文檔上執行的操作。如上圖所示,數據庫名字是cloud-docs。要運行查詢,集合必須指定爲——spreadsheets。組合數據庫名字和集合的名字提供了系統中的唯一性,這通常叫做命名空間。它通常是集合名字和數據庫名字連接在一起,用圓點分割 如,cloud-docs.spreadsheets.

分片集羣中的數據分散方式

mongodb分片集羣更強大的形式:分片單個集合。這也是自動分片一詞的本意,因爲這是mongodb做出分區決定的分片形式,不需要應用參與。

爲分片單個集合,mongodb定義了塊的概念,它是一個基於預定義字段值或分片鍵的邏輯分組文檔。選擇分片鍵是用戶的職責。

如果集合中的所有文檔都包含這種格式,就可以選擇_id和username字段作爲分片鍵。然後mongodb就會使用每個文檔中的這些信息來決定文檔存儲到哪個塊中。

mongodb是怎麼做出決定的呢?在底層核心中,mongodb的分片是基於範圍的,這意味着每個塊表示一個範圍的鍵值。當mongodb查看某個文檔以確定它屬於哪個塊時,首選它會抽取分片鍵的值,然後找出包含分片建所在的塊。

看一下具體的例子,假設我們已經爲spreadsheets集合選擇了username作爲分片建,而且我們有2個片,"A","B"。我們塊分佈可能有點類似於下表

從上表就會明白分片集羣中的塊的作用了。如果我們有個文檔的username值爲Babbage,則看一下上面的表就知道它應該屬於A服務器。事實上,如果我們給大家任意包含username字段的文檔,此字段是分片集羣的鍵,你就可以使用上表確定此文檔所屬 的數據塊,然後決定它應該被髮送到哪個 分片服務器上。

 

查詢路由

在圖左邊,可以看到查詢選擇器包含username的目標查詢。此時,mongos路由可以使用username字段直接把查詢轉發到正確的分片節點上。

在圖右側,可以看到全局或分散/集中查詢,在查詢選擇器中它不包含分片鍵的任意部分。此時,mongos必須廣播查詢到兩個分片中。

目標查詢的性能不能誇大。如果所有的查詢都是全局的,這意味着每個分片節點都必須響應集羣中的每個查詢。相反如果所有的查詢是目標性的,每個分片只需要處理與自己相關的請求。伸縮性的含義非常清晰。

但是定位並非是影響分片集羣性能的唯一因素。正如我們下面看到的,每個在未分片部署下性能問題都會適用於分片集羣,只是分散到單獨的機器上。

分片集羣中建立索引

無論插入定義多麼完美,最終都必須運行在一個分片上。這意味着,如果分片響應查詢慢,則集羣也會慢。

未分片模式下,索引是優化查詢性能的重要手段。

關於分片集羣的索引只需要記住以下幾個要點:

1.每個分片維護自己的索引。當在分片集合上聲明索引時,每個分片都會爲自己的集合部分定義單獨的索引。例如,當mongos使用db.spreadsheets.createIndex()命令時,每個分片進程都會單獨處理索引創建的命令。

2.它遵循分片集合在每個分片上應該擁有相同的索引原則。如果不是,就無法保證查詢性能的一致性。

3.分片集合只允許在_id字段和分片鍵上建議唯一索引。禁止其他地方建立唯一索引,因爲強制唯一性需要在分片之間進行通信,這是由mongodb分片集羣底層工作機制決定的。

選擇分片鍵

在我們爲spreadsheet應用選擇最佳分片鍵的過程中,會看到3個主要的陷阱:

1.熱點:某些分片鍵會導致所有的讀或者寫都操作在單個數據塊或單個分片上。這可能導致單個分片服務器嚴重不堪重負,而其他分片服務器閒置,無所事事。

2.不可分割數據塊,過於粗粒度的分片建可能導致許多文檔使用相同的分片建。因爲分片是基於分片鍵值的範圍,所以意味着這些文檔不能被分割爲多個數據塊,這個最終會限制mongodb均勻分佈數據的能力。

3.糟糕的定位:即使寫壓力可以完美分佈到集羣中,如果我們的分片建與某些查詢沒有關聯,也會導致糟糕的查詢性能。

非平衡寫入(熱點)

 

第一個可能想到的分片建是{"_id" : 1},它會在_id字段上進行分片。

咋一看,_id字段好像是最佳的候選:它肯定會出現在每個文檔中,默認就有索引,在許多查詢裏會使用到,而且mongodb會使用bson object類型自動生成它,本質上是GUID(全局的唯一標識符)

使用object id作爲分片鍵值有個顯著的問題:它的值是嚴格升序的

爲了簡單起見,使用自增字段n來表示_id,正如圖所示,我們已經插入1-1000的文檔,現在將要插入1001,1002,1003三個文檔。因爲mongodb還沒有見到過大於1000的號碼,所以沒有理由來分割從1000到$maxKey的數據塊。這意味着新的3個文檔也全部屬於當前的數據塊,而且所有的新文檔都是這樣的。這意味着每個新創建的spreadsheet文檔都屬於同一個數據塊,也就是每個新的文檔都被寫入到單個片中。最重要的是,mongodb會全力以赴從過載的片上遷移數據,這樣就更加劇了性能惡化。

這也大大抵消了分片的最大優勢:跨機器自動分佈插入負載壓力。所以說,如果仍然想繼續使用_id字段作爲分片建,則有2個選擇

1.使用自定義非自增的標識符重寫_id字段,如果這樣做,記住,即使在分片集羣裏,_id也必須唯一

2.在哈希鍵值上設置分片建,這樣會告訴mongodb使用哈希函數的結果作爲分片建,而不是直接使用分片建。哈希函數用來產生隨機結果,它可以確保插入更加均勻地分佈在集羣中,但是也意味着範圍查詢需要掃描多個分片,因爲雖然分片建的值相似,但是他們的哈希值完全不同。

不可分割的數據庫(粗粒度)

現在我們已經知道了{_id:1}不會作爲分片鍵,但是{username:1}會怎麼樣呢?這看起來是個很好的候選者,因爲當我們查詢或者更新spreadsheet電子表格的時候,通常我們已經知道了spreadsheet所屬的用戶,所以我們可以在查詢裏包含username字段,實現良好的目標地位。此外,在username字段上分片可以導致相對的插入均衡,因爲集羣中分散插入文檔數據的壓力應該是相對均勻的。

使用這個字段只有一個問題:它如此粗粒度,可能導致的極端情況就是數據塊無線增長。假設一個用戶“verch”要 插入10GB的spreadsheet電子表格數據。這會讓包含username爲“”verch”的文檔包含超過64MB最大值的數據塊。

正常情況下,當數據塊變得太大時,mongodb可以把他們分割爲較小的數據塊,然後跨集羣進行平衡遷移數據。但是,此時已經沒有空間可以分割數據塊了,因爲它只包含了一個單獨的分片鍵值。這會導致許多技術問題,但是最終的結果就是集羣變得不平衡了,而且mongodb無法再進行高效的平衡操作。

糟糕的地位(分片鍵不在查詢中)

在看了這些問題後,你可能會想,好吧,那我就只能選擇完全隨機的分片鍵值。它是唯一的並且不是升序的。雖然它可以解決寫入的均衡問題,但是如果你打算從集羣中讀取數據,則它不是一個好的分片建。如果它們包含分片鍵值,則查詢只能被路由到正確的分片上。如果分片建完全是隨機的,在查詢文檔的時候,我們就不知道分片鍵值是什麼意思。

理想的分片鍵

我們已經看過選擇分片鍵時3種需要考慮的因素,一個是如何定位讀取數據,第二個是如何更好地分佈數據,最後一個是如何在應用中高效地分割和遷移數據。

此時,理想的分片鍵就是{username: 1, _id:1}。這個組合鍵可以很好地定位目標,因爲username字段經常出現在讀取操作中,而且可以有很好的平衡性;因爲username的值具備良好的均衡性,而且包含了唯一的_id字段,所以mongodb能良好粒度地分割數據塊

這裏我們可以分割之前只使用username字段無法分割的數據塊。現在,因爲我們包含了_id字段,所以我們可以把所有從‘’verch‘’

開始的文檔分爲2個數據塊,這在使用{username:1}作爲分片鍵時是無法完成的。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章