MongoDB 是一種流行的非關係型數據庫。做爲一種文檔型數據庫,除了有無 schema 的靈活的數據結構,支持複雜、豐富的查詢功能外,MongoDB 還自帶了至關強大的 sharding 功能。mysql
要說 MongoDB 的 sharding,首先說說什麼是 sharding。所謂 sharding 就是將數據水平切分到不一樣的物理節點。這裏着重點有兩個, 一個是水平切分,另外一個是物理節點。通常咱們說數據庫的分庫分表有兩種類型。一種是水平劃分,好比按用戶 id 取模,按餘數劃分用戶的數據,如博客文章等;另外一種是垂直劃分,好比把用戶信息放一個節點,把文章放另外一個節點,甚至能夠把文章標題基本信息放一個節點,正文放另外一個節點。sharding 指的是前一種。而物理節點,主要是和如 mysql 等提供的表分區區分。表分區雖然也對數據進行了劃分,可是這些分區仍然是在同一個物理節點上。sql
那麼,爲何要使用 sharding 呢?sharding 解決了什麼問題,帶來了什麼好處呢?很多人都經歷過本身的網站、應用由小到大,用戶愈來愈多,訪問量愈來愈大,數據量也愈來愈大。這固然是好事。可是,之前一個服務器就能夠抗下的數據庫如今不行了。開始還能夠作作優化,再加個緩存。可是再後來,不管如何都不是一個服務器能承受了。數據量也會很快超過服務器的硬盤容量。這時候,就不得不進行拆分,作 sharding 了。這裏,sharding 能夠利用上更多的硬件資源來解決了單機性能極限的問題。此外,將數據進行水平切分後,還會減少每一個索引的體積。因爲通常數據庫的索引都是 B樹 結構,索引體積減少後,索引深度也會隨之減少,索引查找的速度也會隨之提升。對於一些比較費時的統計查詢,還能夠由此把計算量分攤到多個機器上同時運算,經過分佈式來提升速度。mongodb
雖然 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 節點中去。app
圖中紫色的 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,也只是保證在每一個節點中惟一。有一個辦法是,把索引設爲 {<shard_key>:1, <unique_key>:1} 。可是這樣並不必定知足業務邏輯需求。
第三,啓用 sharding 後,沒法直接使用 group() 。可是能夠用 map reduce 功能做爲替代。
第四,雖然數據遷移操做對讀寫影響很小,可是這個過程須要先把數據從磁盤中換入內存才能進行,因此可能會破壞熱數據緩存。此外,數據遷移也仍是會增大 io 壓力,因此能夠考慮平時關閉自動平衡,在凌晨壓力小的時候再進行。
最後,config 節點的元數據同步對時鐘準確性要求比較高,一旦各 config 時鐘偏差大了,就會出現沒法上鎖,從而沒法更改,致使數據集中。所以 ntp 時鐘同步時必不可少的。
在這裏再說一下 sharding 集羣的備份問題。因爲後端數據節點仍然是普通的 mongod 或 replica set,因此備份其實和原先差很少。只是須要注意的是,備份前須要中止自動平衡,保證備份期間 sharding 的元數據不會變更,而後備份 shard 節點和 config 節點數據便可。