MongoDB 是一種流行的非關系型數據庫。作為一種文檔型數據庫,除了有無 schema 的靈活的數據結構,支持復雜、豐富的查詢功能外,MongoDB 還自帶了相當強大的 sharding 功能。 要說 MongoDB 的 sharding,首先說說什么是 sharding。所謂 sharding 就是將數據
MongoDB 是一種流行的非關系型數據庫。作為一種文檔型數據庫,除了有無 schema 的靈活的數據結構,支持復雜、豐富的查詢功能外,MongoDB 還自帶了相當強大的 sharding 功能。
要說 MongoDB 的 sharding,首先說說什么是 sharding。所謂 sharding 就是將數據水平切分到不同的物理節點。這里著重點有兩個, 一個是水平切分,另一個是物理節點。一般我們說數據庫的分庫分表有兩種類型。一種是水平劃分,比如按用戶 id 取模,按余數劃分用戶的數據,如博客文章等;另一種是垂直劃分,比如把用戶信息放一個節點,把文章放另一個節點,甚至可以把文章標題基本信息放一個節點,正文放另一個節點。sharding 指的是前一種。而物理節點,主要是和如 mysql 等提供的表分區區分。表分區雖然也對數據進行了劃分,但是這些分區仍然是在同一個物理節點上。
那么,為什么要使用 sharding 呢?sharding 解決了什么問題,帶來了什么好處呢?不少人都經歷過自己的網站、應用由小到大,用戶越來越多,訪問量越來越大,數據量也越來越大。這當然是好事。但是,以前一個服務器就可以抗下的數據庫現在不行了。開始還可以做做優化,再加個緩存。但是再后來,無論如何都不是一個服務器能承受了。數據量也會很快超過服務器的硬盤容量。這時候,就不得不進行拆分,做 sharding 了。這里,sharding 可以利用上更多的硬件資源來解決了單機性能極限的問題。此外,將數據進行水平切分后,還會減小每個索引的體積。由于一般數據庫的索引都是 B樹 結構,索引體積減小后,索引深度也會隨之減小,索引查找的速度也會隨之提高。對于一些比較費時的統計查詢,還可以由此把計算量分攤到多個機器上同時運算,通過分布式來提高速度。
雖然 sharding 有很多好處,但是傳統數據庫做 sharding 會遇到很多麻煩事。首先是擴容和初始化的問題。比如,原來按用戶 id 模 5,分了 5 個節點,后來隨著數據增長,這 5 個也不夠用了,需要再增加。這時候如果改成 10 個,則至少要挪動一半數據。如果不是整倍數,比如擴展到 7 個節點,那絕大部分數據都會被挪一遍。對于已經做了 sharding 的大規模數據庫來說,這是一件相當可怕的事情。而且在數據遷移期間,通常都無法繼續提供服務,這將造成很長時間的服務中斷。對從一個未做 sharding 的數據庫開始創建也是同樣。如果使用虛擬節點,比如將數據劃分成 1000 個虛擬節點,然后通過映射關系來找到對應的物理節點,可以有所改善。但仍然無法避免遷移過程中的服務中斷。另一個麻煩事是數據路由。數據拆分以后,應用程序就需要去定位數據位于哪個節點。還需要將涉及多個節點的查詢的結果合并起來。這個工作如果沒有使用數據庫中間件的話,就需要花不少功夫自己實現,即使使用了中間件,也很難做到透明。由于關系型數據庫功能的復雜性,很多功能在 sharding 上將無法正常使用。比如 join 、事務等。因此會造成應用層的大量修改測試工作。
sharding 會有這許多麻煩事,那么 MongoDB 的 sharding 又如何呢?
MongoDB 的 sharding 的特色就是自動化。具體體現為可以動態擴容、自動平衡數據、以及透明的使用接口。可以從一個普通的 replica set,或者單個實例平滑升級,可以動態增加刪除節點,響應數據快速增長。可以自動在節點間平衡數據量,避免負載集中在少數節點,而在這期間不影響數據庫讀寫訪問。對客戶端,可以使用完全相同的驅動,大部分功能可用,基本不需要更改任何代碼。
MongoDB 的 sharding 有如此強大的功能,它的實現機制是怎樣的呢?下圖就是 MongoDB sharding 的結構圖。
從圖中可以看出,MongoDB sharding 主要分為 3 大部分。shard 節點、config 節點和 config 節點。對客戶端來說,直接訪問的是 圖中綠色的 mongos 節點。背后的 config 節點和 shard 節點是客戶端不能直接訪問的。mongos 的主要作用是數據路由。從元數據中定位數據位置,合并查詢結果。另外,mongos 節點還負責數據遷移和數據自動平衡,并作為 sharding 集群的管理節點。它對外的接口就和普通的 mongod 一樣。因此,可以使用標準 mongodb 客戶端和驅動進行訪問。mongos 節點是無狀態的,本身不保存任何數據和元數據,因此可以任意水平擴展,這樣任意一個節點發生故障都可以很容易的進行故障轉移,不會造成嚴重影響。
其中藍色的 shard 節點就是實際存放數據的數據節點。每個 shard 節點可以是單個 mongod 實例,也可以使一個 replica set 。通常在使用 sharding 的時候,都會同時使用 replica set 來實現高可用,以免集群內有單個節點出故障的時候影響服務,造成數據丟失。同時,可以進一步通過讀寫分離來分擔負載。對于每個開啟 sharding 的 db 來說,都會有一個 默認 shard 。初始時,第一個 chunk 就會在那里建立。新數據也就會先插入到那個 shard 節點中去。
圖中紫色的 config 節點存儲了元數據,包括數據的位置,即哪些數據位于哪些節點,以及集群配置信息。config 節點也是普通的 mongod 。如圖所示,一組 config 節點由 3 個組成。這 3 個 config 節點并非是一個 replica set。它們的數據同步是由 mongos 執行兩階段提交來保證的。這樣是為了避免復制延遲造成的元數據不同步。config 節點一定程度上實現了高可用。在一個或兩個節點發生故障時,config 集群會變成只讀。但此時,整個 sharding 集群仍然可以正常讀寫數據。只是無法進行數據遷移和自動均衡而已。
config 節點里存放的元數據都有些啥呢?連上 mongos 后,
use config; show collections
結果是
settings shards databases collections chunks mongos changelog
還可以進一步查看這些東西的數據。這個 config 庫就是后端 config 節點上的數據的映射,提供了一個方便的讀取元數據的入口。這些 collection 里面都是什么呢? settings 里是 sharding 的配置信息,比如數據塊大小,是否開啟自動平衡。shards 里存放的是后端 shard 節點的信息,包括 ip,端口等。databases 里存放的是數據庫的信息,是否開啟 sharding,默認 shard 等。collections 中則是哪些 collection 啟用了 sharding,已經用了什么 shard key。chunks 里是數據的位置,已經每個 chunk 的范圍等。mongos 里是關于 mongos 的信息,changelog 是一個 capped collection,保存了最近的 10m 元數據變化記錄。
mongodb sharding 的搭建也很容易。簡單的幾步就能完成。
先啟動若干 shard 節點
mongod --shardsvr
啟動 3 個 config 節點
mongod --configsvr
啟動 mongos
mongos --configdb=192.168.1.100, 192.168.1.101, 192.168.1.102
這里,–shardsvr 參數只起到修改默認端口為 27018 作用,–configsvr 則修改默認端口為 27019 以及默認路徑為 /data/configdb。此外并沒有什么直接作用。實際使用時,也可以自己指定端口和數據路徑。此外,這兩個參數的另一個作用就是對進程進行標記,這樣在 ps aux 的進程列表里,就很容易確定進程的身份。–configdb 參數就是 config 節點的地址。如果更改了默認端口,則需要在這里加上。
然后我們把數據節點加入集群:在 mongos 上運行
use admin sh.addShard(’ [hostname]:[port]’)
如果使用的事 replicaSet,則是
use admin sh.addShard(’replicaSetName/,,’)
接著就是啟用 sharding 了。
sh.enableSharding(dbname) sh.shardCollection(fullName, key, unique)
這樣就可以了。還是很簡單的吧。如果 collection 里有數據,則會自動進行數據平衡。
之前說過,mongodb 的 sharding 把數據分成了數據塊(chunk)來進行管理。現在來看看 chunk 究竟是怎么回事。在 mongodb sharding 中,chunk 是數據遷移的基本單位。每個節點中的數據都被劃分成若干個 chunk 。一個 chunk 本質上是 shard key 的一個連續區間。chunk 實際上是一個邏輯劃分而非物理劃分。sharding 的后端就是普通的 mongod 或者 replica set,并不會因為是 sharding 就對數據做特殊處理。一個 chunk 并不是實際存儲的一個頁或者一個文件之類,而是僅僅在 config 節點中的元數據中體現。mongodb 的sharding 策略實際上就是一個 range 模式。
如圖,第一個 chunk 的范圍就是 uid 從 -∞ 到 12000 范圍內的數據。第二個就是 12000 到 58000 。以此類推。對于一個剛配置為 sharding 的 collection ,最開始只有一個 chunk,范圍是從 -∞ 到 +∞。
隨著數據的增長,其中的數據大小超過了配置的 chunk size,默認是 64M 則這個 chunk 就會分裂成兩個。因為 chunk 是邏輯單元,所以分裂操作只涉及到元數據的操作。數據的增長會讓 chunk 分裂得越來越多。這時候,各個 shard 上的 chunk 數量就會不平衡。這時候,mongos 中的一個組件 balancer 就會執行自動平衡。把 chunk 從 chunk 數量最多的 shard 節點挪動到數量最少的節點。
最后,各個 shard 節點上的 chunk 數量就會趨于平衡。當然,balance 不一定會使數據完全平均,因為移動數據本身有一定成本,同時為了避免極端情況下早晨數據來回遷移,只有在兩個 shard 的 chunk 數量之差達到一定閾值時才會進行。默認閾值是 8 個。也就是說,默認情況下,只有當兩個節點的數據量差異達到 64M * 8 == 256M 的時候才會進行。這樣就不用對剛建好的 sharding ,插入了不少數據,為什么還是都在一個節點里感到奇怪了。那只是因為數據還不夠多到需要遷移而已。
在數據遷移的過程中,仍然可以進行數據讀寫,并不會因此而影響可用性。那么 mongodb 是怎么做到的呢?在數據遷移過程中,數據讀寫操作首先在源數據節點中進行。待遷移完畢后,再將這期間的更新操作同步到新節點中去。最后再更新 config 節點,標記數據已經在新的地方,完成遷移。只有在最后同步遷移期間的操作的時候,需要鎖定數據更新。這樣就講鎖定時間盡可能縮小,大大降低數據遷移對服務的影響。
mongodb 的 sharding 和傳統 sharding 的最大區別就在于引入了元數據。看似增加了復雜度,并增加了一些額外的存儲,但是由此帶來的靈活性卻是顯而易見的。傳統的 sharding 本質上是對數據的靜態映射,所有那些數據遷移的困難都是由此而來。而引入元數據以后,就變靜態映射為動態映射。數據遷移就不再是難事了。從而從根本上解決了問題。另一方面,用元數據實現 chunk 則降低了實現難度,后端節點仍然可以使用原有的技術。同時,因為不需要對后端數據進行變動,也使部署遷移變得更容易,只需要另外加上 mongos 節點和 config 節點即可。
再說說數據路由功能。mongos 的最主要功能就是作為數據路由,找到數據的位置,合并查詢結果。來看看它是如何處理的。如果查詢的條件是 shard key ,那么 mongos 就能從元數據直接定位到 chunk 的位置,從目標節點找到數據。
如果查詢條件是 shard key 的范圍,由于 chunk 是按 shard key 的范圍來劃分的,所以 mongos 也可以找到數據對應 chunk 的位置,并把各個節點返回的數據合并。
如果查詢的條件不是任何一個索引,原來的全 collection 遍歷仍然不可避免。但是會分發到所有節點進行。所以,還是可以起到分擔負載的作用。
如果查詢的條件是一個索引,但不是 shard key,查詢也會被分發到所有節點,不過在每個節點上索引仍然有效。
如果是按查詢 shard key 進行排序,同樣由于 chunk 是一個 shard key 的范圍,則會依次查詢各 chunk 所在節點,而無需返回所有數據再排序。如果不是按 shard key 排序,則會在每個節點上執行排序操作,然后由 mongos?進行歸并排序。由于是對已排序結果的歸并排序,所以在 mongos 上不會有多少壓力,查詢結果的游標也會變成在每個節點上的游標。并不需要把所有數據都吐出來。
從上面可以看到,對 sharding 集群來說,shard key 的選擇是至關重要的。shard key 其實就相當于數據庫的聚簇索引,所以選擇聚簇索引的原則和選擇 shard key 的原則是差不多的。同樣, shard key 一旦設定就無法再更改,所以,選擇的時候就要謹慎。shard key 的選擇主要就這么幾點。
首先,shard key 的值要是固定的,不會被更改的。因為一旦這個值被更改,就有可能會從一個節點被挪動到另一個節點,從而帶來很大的開銷。
第二,shard key 要有足夠的區分度。同樣因為?chunk 是一個?shard key 的范圍,所以 shard key 相同的值只能位于同一個 chunk 。如果 shard key 相同的值很大,致使一個 chunk 的大小超過了 chunk size,也無法對 chunk 進行分裂,數據均衡。同時,和一般的數據庫索引一樣,更好的區分度也能提高查詢性能。
第三,shard key 還要有一定的隨機性而不是單向增長。單向增長的 shard key 會導致新插入的數據都位于一個 chunk 中,在在某一個 shard 節點中產生集中的寫壓力。所以,最好避免直接使用 _id ,時間戳 這種單向增長的值作為 shard key。
mongodb 的 sharding 有很多優勢,但是也同樣有其局限性。
首先,mongodb 只提供了 range 模式的 sharding。這種模式雖然可以對按 shard key進行 range 查詢、排序進行優化,但是也會造成使用單向增長的值時,寫入集中的結果。
第二,啟用了 sharding 之后,就無法保證除 shard key 以為其他的索引的唯一性。即使設為 unique,也只是保證在每個節點中唯一。有一個辦法是,把索引設為?{
第三,啟用 sharding 后,無法直接使用 group() 。但是可以用 map reduce 功能作為替代。
第四,雖然數據遷移操作對讀寫影響很小,但是這個過程需要先把數據從磁盤中換入內存才能進行,所以可能會破壞熱數據緩存。此外,數據遷移也還是會增大 io 壓力,所以可以考慮平時關閉自動平衡,在凌晨壓力小的時候再進行。
最后,config 節點的元數據同步對時鐘準確性要求比較高,一旦各 config 時鐘誤差大了,就會出現無法上鎖,從而無法更改,導致數據集中。因此 ntp 時鐘同步時必不可少的。
在這里再說一下 sharding 集群的備份問題。由于后端數據節點仍然是普通的 mongod 或 replica set,所以備份其實和原先差不多。只是需要注意的是,備份前需要停止自動平衡,保證備份期間 sharding 的元數據不會變動,然后備份 shard 節點和 config 節點數據即可。
P.S. 這篇東西是十月份我在 thinkinlamp 第三屆數據庫大會上的 topic 的內容的整理。
原文地址:MongoDB Sharding 機制分析, 感謝原作者分享。
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com