MongoDB的水平擴展,你做對了嗎?

作者|趙翼

編輯|Natalie

AI前線出品| ID:ai-front

關於 MongoDB 水平擴展的文章很多,但是我查閱了大部分資料並沒有看到行之有效的解決方案。大部分都是泛泛的介紹一些概念性的內容,沒有提供具體的實例或者性能方面的考量。經過一段時間的實踐和總結,我覺得有必要和大家分享一下數據水平擴展方面的最佳實踐,以及需要注意和規避的潛在問題。

分散式資料庫的前世今生

當人們一開始使用資料庫系統的時候,所有數據都是跑在一台伺服器上,即所謂的單機資料庫伺服器。在企業級應用中,我們會搭建一台應用程序伺服器,一般它會被運行在一台伺服器或者工作站上,大多數情況下採用 Linux/Unix/Windows 操作系統,也有人把這樣的伺服器稱之為應用程序伺服器。顧名思義,他的作用是處理複雜的業務邏輯。但是一點需要注意的是,在這樣的構架中,這台應用程序伺服器不會存儲任何業務數據,也就是說,他只負責邏輯運算,處理用戶請求,真正存放數據的地方是前面提到的那台資料庫伺服器。應用程序伺服器將用戶的請求轉換成資料庫語言(通常是 SQL),運行在資料庫中,從而進行數據的增刪改查。資料庫伺服器不會對外直接開放,管理人員也不允許直接在資料庫層面操作數據。所有的操作都會經過應用程序伺服器來完成。應用程序層、資料庫層再加上 UI 層,被稱為傳統的 Web 三層構架。

Replication

隨著數據量的增大,技術的不斷進步以及需求的增加,安全性、可靠性、容錯性、可恢復性等因素被人們考慮進資料庫的設計中。於是出現了分散式資料庫系統。以前在存儲數據的時候,都是採用單體構架模式,及數據全部存儲在一台資料庫中,一旦資料庫出現問題,所有的應用請求都會受到影響。資料庫的恢復也是一個令人頭疼的問題。有時一次資料庫的全恢復會運行幾個小時甚至幾天的時間。在互聯網應用不斷普及的今天,業務需求對構架產生了嚴峻的挑戰。沒有哪個互聯網應用會允許若干小時的宕機時間。分散式資料庫的產生,為我們提供了技術上的解決方案。在部署資料庫的時候,不用於以前的單體應用,分散式下資料庫部署包括多點部署,一套業務應用資料庫被分布在多台資料庫伺服器上,分主從伺服器。主伺服器處理日常業務請求,從伺服器在運行時不斷的對主伺服器進行備份,當主伺服器出現宕機、或者運行不穩定的情況時,從伺服器會立刻替換成主伺服器,繼續對外提供服務。此時,開發運維人員會對出現問題的伺服器進行搶修、恢復,之後再把它投入到生產環境中。這樣的構架也被稱作為高可用構架,它支持了災難恢復,為業務世界提供了可靠的支持,也是很多企業級應用採用的主流構架之一。需要指出的是,在這樣的主從設計中,從資料庫常常被設計成只讀,主資料庫支持讀寫操作。一般會有一台主資料庫連接若干台從資料庫。在互聯網產品的應用中,人們大多數情況下會對應用伺服器請求讀操作,這樣應用伺服器可以把讀操作請求分發到若干個從資料庫中,這樣就避免了主資料庫的並發請求次數過高的問題。至於為什麼大多數應用都是讀操作,你可以想一下在你使用微信或者微博的時候,你是看別人發布的圖片多還是自己發布的時候多。當你不斷下滑屏幕,刷新朋友圈,這些都是讀請求。只有當評論、點贊、分享的時候才會進行寫操作。

我們的世界就是這樣,當技術為人們解決了現實問題以後,新的需求會層出不窮。智能手機,互聯網 +,創業潮的不斷興起,點燃了這樣一個擁有幾千年文明歷史的民族的激情。各種新點子、新概念不斷的湧現,誰的手機里沒有安裝幾十個互聯網應用,從訂餐,快遞,到住房,旅遊,再到教育,養老,那一個環節沒有互聯網的支持,沒有技術的成分。我們就是生存在這樣一個的平凡而又不乏豪情的社會中。許許多多的需求和數據充斥著我們的構架,挑戰著我們的存儲。

對此,你可能已經想到,前面提到的分散式資料庫多點部署是不是會存在大量的瓶頸。比如,在主從資料庫結構中,從資料庫的內容基本上可以說是主資料庫的一份全拷貝,這樣的技術稱之為

Replication。Replication在實現主從數據同步時,通常採用Transaction Log的方式,比如,當一條數據插入到主資料庫的時候,主資料庫會像Trasaction Log中插入一條記錄來聲明這次資料庫寫紀錄的操作。之後,一個Replication Process會被觸發,這個進程會把Transaction Log中的內容同步到從資料庫中。整個過程如下圖所示:

對於資料庫的擴展來說,通常有兩種方法,水平擴展和垂直擴展。

  • 垂直擴展:這種擴展方式比較傳統,是針對一台伺服器進行硬體升級,比如添加強大的 CPU,內存或者添加磁碟空間等等。這種方式的局限性是僅限於單台伺服器的擴容,儘可能的增加單台伺服器的硬體配置。優點是構架簡單,只需要維護單台伺服器。
  • 水平擴展:這種方式是目前構架上的主流形式,指的是通過增加伺服器數量來對系統擴容。在這樣的構架下,單台伺服器的配置並不會很高,可能是配置比較低、很廉價的 PC,每台機器承載著系統的一個子集,所有機器伺服器組成的集群會比單體伺服器提供更強大、高效的系統容載量。這樣的問題是系統構架會比單體伺服器複雜,搭建、維護都要求更高的技術背景。MongoDB 中的 Sharding 正式為了水平擴展而設計的,下面就來擠開 shard 面紗,探討一下 shard 中不同分片的技術區別以及對資料庫系統的影響。

分片 (Shard)

前面提到的 Replication 結構可以保證資料庫中的全部數據都會有多分拷貝,資料庫的高可用可以保障。但是新的問題是如果要存儲大量的數據,不論主從伺服器,都需要存儲全部數據,這樣檢索必然會出現性能問題。可以這樣講,Replication只能算是分散式資料庫的第一階段。主要解決的是資料庫高可用,讀數據可以水平擴展,部分解決了主數據並發訪問量大的問題。但是它並沒有解決資料庫寫操作的分散式需求,此外在資料庫查詢時也只限制在一台伺服器上,並不能支持一次查詢多台資料庫伺服器。我們假設,如果有一種構架,可以實現資料庫水平切分,把切分的數據分布存儲在不同的伺服器上,這樣當查詢請求發送到資料庫時,可以在多台資料庫中非同步檢索符合查詢條件的語句,這樣不但可以利用多台伺服器的 CPU,而且還可以充分利用不同伺服器上的 IO,顯而易見這樣的構架會大大提高查詢語句的性能。但是這樣的實現卻給資料庫設計者代碼不少麻煩,首先要解決的就是事務(Transaction),我們知道在進行一次資料庫寫操作的時候,需要定一個事務操作,這樣在操作失敗的時候可以回滾到原始狀態,那當在分散式資料庫的情況下,事務需要跨越多個資料庫節點以保持數據的完整性,這給開發者帶來不少的麻煩。此外,在關係型資料庫中存在大量表關聯的情況,分散式的查詢操作就會牽扯到大量的數據遷移,顯然這必將降低資料庫性能。但是,在非關係型資料庫中,我們弱化甚至去除了事務和多表關聯操作,根據 CAP 理論:在分散式資料庫環境中,為了保持構架的擴展性,在分區容錯性不變的前提下,我們必須從一致性和可用性中取其一,那麼,從這一點上來理解「NoSQL 資料庫是為了保證 A 與 P,而犧牲 C」的說法,也是可以講得通的。同時,根據該理論,業界有一種非常流行的認識,那就是:關係型資料庫設計選擇了一致性與可用性,NoSQL 資料庫設計則不同。其中,HBase選擇了一致性與分區可容忍性,Cassandra選擇了可用性與分區可容忍性。本文關注於非關係型資料庫中分區的技巧和性能,以 MongoDB 為例進行說明,在下面的章節中就圍繞這一點展開討論。

MongoDB 分片原理

MongoDB 中通過 Shard 支持伺服器水平擴展,通過 Replication 支持高可用(HA)。這兩種技術可以分開來使用,但是在大資料庫企業級應用中通常人們會把他們結合在一起使用。

MongoDB Sharding

首先我們簡要概述一下分片在 MongoDB 中的工作原理。通過分片這個單詞我們可以看出,他的意思是將資料庫表中的數據按照一定的邊界分成若干組,每一組放到一台 MongoDB 伺服器上。拿用戶數據舉例,比如你有一張數據表存放用戶基本信息,可能由於你的應用很受歡迎,短時間內就積攢了上億個用戶,這樣當你在這張表上進行查詢時通常會耗費比較長的時間,這樣這個用戶表就稱為了你的應用程序的性能瓶頸。很顯然的做法是對這張用戶表進行拆分,假設用戶表中有一個age年齡欄位,我們先做一個簡單的拆分操作,按照用戶的年齡段把數據放到不同的伺服器上,以 20 為一個單位,20 歲以下的用戶放到 server1,20 到 40 歲的用戶放到 server2,40-60 歲的用戶放到 server3,60 歲以上放到 server4,後面我們會講這樣的拆分是否合理。在這個例子中,用戶年齡age就是我們進行Sharding(切分)的Shard Key(關於 Shard Key 的選擇後面會詳細介紹),拆分出來的server1,server2,server3和server4就是這個集群中的 4 個Shard(分區)伺服器。好,Shard 集群已經有了,並且數據已經拆分完好,當用戶進行一次查詢請求的時候我們如何向這四個 Shard 伺服器發送請求呢?例如:我的查詢條件是用戶年齡在 18 到 35 歲之間,這樣個查詢請求應當發送到server1和server2,因為他們存儲了用戶年齡在 40 以下的數據,我們不希望這樣的請求發送到另外兩台伺服器中因為他們並不會返回任何數據結果。此時,另外一個成員就要登場了,mongos,它可以被稱為 Shard 集群中的路由器,就像我們網路環境中使用的路由器一樣,它的作用就是講請求轉發到對應的目標伺服器中,有了它我們剛才那條查詢語句就會正確的轉發給server和server2,而不會發送到server3和server4上。mongos根據用戶年齡(Shard Key)分析查詢語句,並把語句發送到相關的 shard 伺服器中。除了mongos和shard之外,另一個必須的成員是配置伺服器,config server,它存儲 Shard 集群中所有其他成員的配置信息,mongos會到這台config server查看集群中其他伺服器的地址,這是一台不需要太高性能的伺服器,因為它不會用來做複雜的查詢計算,值得注意的是,在 MongoDB3.4 以後,config server必須是一個replica set。理解了上面的例子以後,一個 Shard 集群就可以部署成下圖所示的結構:

其中:

  • shard: 每一個 Shard 伺服器存儲數據的一個子集,例如上面的用戶表,每一個 Shard 存儲一個年齡段的用戶數據。
  • mongos: 處理來自應用伺服器的請求,它是在應用伺服器和Shard 集群之間的一個介面。
  • config server: 存儲 shard 集群的配置信息,通常部署在一個 replica set 上。

MongoDB Shard 性能分析

環境準備

這樣的伺服器構架是否合理,或者說是否能夠滿足數據量不斷增長的需求。如果僅僅是通過理論解釋恐怕很難服眾,我已經信奉理論結合實際的工作方式,所以在我的文章中除了闡述理論之外,一定會有一定的示例為大家驗證理論的結果。接下來我們就根據上面的例子做一套本地運行環境。由於 MongoDB 的便捷性,我們可以在任何一台 PC 上搭建這樣一個資料庫集群環境,並且不限制操作系統類型,任何 Windows/Linux/Mac 的主流版本都可以運行這樣的環境。在本文中,我才用 MongoDB3.4 版本。對於如何創建一個 MongoDB Shard 環境,網上有很多教程和命令供大家選擇,創建一個有 3 個 Mongos,每個 Mongos 連接若干個 Shards,再加上 3 個 config server cluster,通常需要 20 幾台 MongoDB 伺服器。如果一行命令一行命令的打,即便是在非常熟練的情況下,沒有半個小時恐怕搭建不出來。不過幸運的是有第三方庫幫我們做這個事情,大家可以查看一下mtools。他是用來創建各種 MongoDB 環境的命令行工具,代碼使用python寫的,可以通過pip install安裝到你的環境上。具體的使用方法可以參考github.com/rueckstiess/。也可以通過ttps://github.com/zhaoyi0113/m上面的腳本把環境搭載 Docker 上面。下面的命令用來在本地創建一個 MongoDB Shard 集群,包含 1 個mongos路由,3 個shardreplica,每個 replica 有 3 個shard伺服器,3 個config伺服器。這樣一共創建 13 個進程。

mlaunch init --replicaset --sharded 3 --nodes 3 --config 3 --hostname localhost --port 38017 --mongos 1

伺服器創建好以後我們可以連接到mongos上看一下 shard 狀態,埠是上面制定的 38017。

mongos> sh.status()

--- Sharding Status ---

...

shards:

t{ "_id" : "shard01", "host" : "shard01/localhost:38018,localhost:38019,localhost:38020", "state" : 1 }

t{ "_id" : "shard02", "host" : "shard02/localhost:38021,localhost:38022,localhost:38023", "state" : 1 }

t{ "_id" : "shard03", "host" : "shard03/localhost:38024,localhost:38025,localhost:38026", "state" : 1 }

active mongoses:

t"3.4.0" : 1

...

可以看到剛才創建的 shard 伺服器已經加入到這台 mongos 中了,這裡有 3 個 shard cluster,每個 cluster包含 3 個 shard 伺服器。除此之外,我們並沒有看到關於 Shard 更多的信息。這是因為這台伺服器集群還沒有任何數據,而且也沒有進行數據切分。

數據準備

首先是數據的錄入,為了分析我們伺服器集群的性能,需要準備大量的用戶數據,幸運的是mtools提供了mgenerate方法供我們使用。他可以根據一個數據模版向 MongoDB 中插入任意條 json 數據。下面的 json 結構是我們在例子中需要使用的數據模版:

{

"user": {

"name": {

"first": {"$choose": ["Liam", "Aubrey", "Zoey", "Aria", "Ellie", "Natalie", "Zoe", "Audrey", "Claire", "Nora", "Riley", "Leah"] },

"last": {"$choose": ["Smith", "Patel", "Young", "Allen", "Mitchell", "James", "Anderson", "Phillips", "Lee", "Bell", "Parker", "Davis"] }

},

"gender": {"$choose": ["female", "male"]},

"age": "$number",

"address": {

"zip_code": {"$number": [10000, 99999]},

"city": {"$choose": ["Beijing", "ShangHai", "GuangZhou", "ShenZhen"]}

},

"created_at": {"$date": ["2010-01-01", "2014-07-24"] }

}

}

把它保存為一個叫user.json的文件中,然後使用mgenerate插入一百條隨機數據。隨機數據的格式就按照上面json文件的定義。你可以通過調整--num的參數來插入不同數量的 Document。(Link to mgenerate wiki)

mgenerate user.json --num 1000000 --database test --collection users --port 38017

上面的命令會像test資料庫中users collection 插入一百萬條數據。在有些機器上,運行上面的語句可能需要等待一段時間,因為生成一百萬條數據是一個比較耗時的操作,之所以生成如此多的數據是方便後面我們分析性能時,可以看到性能的顯著差別。當然你也可以只生成十萬條數據來進行測試,只要能夠在你的機器上看到不同find語句的執行時間差異就可以。

插入完數據之後,我們想看一下剛剛插入的數據在伺服器集群中是如何分配的。通常,可以通過sh.status() MongoDB shell 命令查看。不過對於一套全新的集群伺服器,再沒有切分任何 collection 之前,我們是看不到太多有用的信息。不過,可以通過 explain 一條查詢語句來看一下數據的分布情況。這裡不得不強調一下在進行數據性能分析時一個好的 IDE 對工作效率有多大的影響,我選擇 dbKoda 作為 MongoDB 的 IDE 主要原因是他是目前唯一一款對 MongoDB Shell 的完美演繹,對於 MongoDB Shell 命令不太熟悉的開發人員來說尤為重要,幸運的是這款 IDE 還支持 Windows/Mac/Linux 三種平台,基本上覆蓋了絕大多數操作系統版本。下面是對剛才建立的一百萬條 collection 的一次 find 的 explain 結果。(對於 Explain 的應用,大家可以參考我的另外一片文章:如何通過 MongoDB 自帶的 Explain 功能提高檢索性能?)

從上圖中可以看到,我們插入的一百萬條數據全部被分配到了第一個 shard 伺服器中,這並不是我們想看到的結果,不要著急,因為我還沒有進行數據切分,MongoDB 並不會自動的分配這些數據。下面我們來一點一點分析如何利用 Shard 實現高效的數據查詢。

配置 Shard 資料庫

環境搭建好並且數據已經準備完畢以後,接下來的事情就是配置資料庫並切分數據。方便起見,我們把用戶分為三組,20 歲以下(junior),20 到 40 歲(middle)和 40 歲以上(senior),為了節省篇幅,我在這裡不過多的介紹如何使用 MongoDB 命令,按照下面的幾條命令執行以後,我們的數據會按照用戶年齡段拆分成若干個 chunk,並分發到不同的 shard cluster 中。如果對下面的命令不熟悉,可以查看 MongoDB 官方文檔關於 Shard Zone/Chunk 的解釋。

db.getSiblingDB(test).getCollection(users).createIndex({user.age:1})

sh.setBalancerState(false)

sh.addShardTag(shard01, junior)

sh.addShardTag(shard02, middle)

sh.addShardTag(shard03, senior)

sh.addTagRange(test.users, {user.age: MinKey}, {user.age:20}, junior) sh.addTagRange(test.users, {user.age: 21}, {user.age:40}, middle) sh.addTagRange(test.users, {user.age: 41}, {user.age: MaxKey}, senior)

sh.enableSharding(test)

sh.shardCollection(test.users, {user.age:1})

sh.setBalancerState(true)

從上面的命令中可以看出,我們首先要為 Shard Key 創建索引,之後禁止 Balancer 的運行,這麼做的原因是不希望在 Shard Collection 的過程中還運行 Balancer。之後將數據按照年齡分成三組,分別標記為junior,middle,senior並把這三組分別分配到三個 Shard 集群中。 之後對 test 庫中的 users collection 進行按用戶年齡欄位的切分操作,如果 Shard collection 成功返回,你會得到下面的輸出結果:{ "collectionsharded" : "test.users", "ok" : 1 }。

關於 Shard 需要注意的幾點

  • 一旦你對一個 Colleciton 進行了 Shard 操作,你選擇的 Shard Key 和它對應的值將成為不可變對象,所以:
  • 你無法在為這個 collection 重新選擇 Shard Key
  • 你不能更新 Shard key 的取值

隨後不要忘記,我們還需要將 Balancer 打開:sh.setBalancerState(true)。剛打開以後運行sh.isBalancerRunning()應當返回true,說明 Balancer 服務正在運行,他會調整 Chunk 在不同 Shards 伺服器中的分配。一般 Balancer 會運行一段時間,因為他要對分組的數據重新分配到指定的 shard 伺服器上,你可以通過sh.isBalancerRunning()命令查看 Balancer 是否正在運行。現在可以稍事休息一下喝杯咖啡或看看窗外的風景。

為了理解數據如何分布在 3 個 shard 集群中,我們有必要分析一下 chunk 和 zone 的劃分,下圖是在 dbKoda 上顯示 Shard Cluster 統計數據,可以看到數據總共被分成 6 個 chunks,每個 shard 集群存儲 2 個 chunk。

對此有些同學會有疑問,為什麼我們的數據會被分為 6 個 chunks,而且每個 shard 集群個分配了 2 個 chunk。是誰來保證數據的均勻分配?下面我就給大家解釋一下他們的概念以及我們應當如何使用。

Chunk

我們已經知道 MongoDB 是通過 shard key 來對數據進行切分,被切分出來的數據被分配到若干個 chunks 中。一個 chunk 可以被認為是一台 shard 伺服器中數據的子集,根據 shard key,每個 chunk 都有上下邊界,在我們的例子中,邊界值就是用戶年齡。chunk 有自己的大小,數據不斷插入到 mongos 的過程中,chunk 的大小會發生變化,chunk 的默認大小是 64M。當然 MongoDB 允許你對 chunk 的大小進行設置,你也可以把一個 chunk 切分成若干個小 chunk,或者合併多個 chunk。一般我不建議大家手動操作 chunk 的大小,或者在 mongos 層面切分或合併 chunk,除非真有合適的原因才去這麼做。原因是,在數據不斷插入到我們的集群中時,mongodb 中的 chunk 大小會發生很大的變化,當一個 chunk 的大小超過了最大值,mongo 會根據 shard key 對 chunk 進行切分,在必要的時候,一個 chunk 可能會被切分成多個小 chunk,大多數情況下這種自動行為已經滿足了我們日常的業務需求,無需進行手動操作,另一點原因是當進行 chunk 切分後,直接的結果會導致數據分配的不均勻,此時 balancer 會被調用來進行數據重新分配,很多時候這個操作會運行很長時間,無形中導致了內部結構的負載平衡,因此不建議大家手動拆分。當然,理解 chunk 的分配原理還是有助於大家分析資料庫性能的必要條件。我在這裡不過多的將如何進行這些操作,有興趣的讀者可以參考 MongoDB 官方文檔,上面有比較全面的解釋。這裡我只強調在進行 chunk 操作的時候,要注意一下幾個方面,這些都是影響你 MongoDB 性能的關鍵因素。

  • 如果存在大量體積很小的 chunk,他可以保證你的數據均勻的分布在 shard 集群中但是可能會導致頻繁的數據遷移。這將加重 mongos 層面上的操作。
  • 大的 chunk 會減少數據遷移,減輕網路負擔,降低在 mongos 路由層面上的負載,但弊端是有可能導致數據在 shard 集群中分布的不均勻。
  • Balancer 會在數據分配不均勻的時候自動運行,那麼 Balancer 是如何決定什麼情況下需要進行數據遷移呢?答案是 Migration Thresholds,當 chunk 的數量在不同 shard replica 之間超過一個定值時,balancer 會自動運行,這個定值根據你的 shard 數量不同而不同。

Zones

可以說 chunk 是 MongoDB 在多個 shard 集群中遷移數據的最小單元,有時候數據的分配不會按照我們臆想的方向進行,就拿上面的例子來說,雖然我們選擇了用戶年齡作為 shard key,但是 MongoDB 並不會按照我們設想的那樣來分配數據,如何進行數據分配就是通過 Zones 來實現。Zones 解決了 shard 集群與 shard key 之間的關係,我們可以按照 shard key 對數據進行分組,每一組稱之為一個 Zone,之後把 Zone 在分配給不同的 Shard 伺服器。一個 Shard 可以存儲一個或多個 Zone,前提是 Zone 之間沒有數據衝突。Balancer 在運行的時候會把在 Zone 里的 chunk 遷移到關聯這個 Zone 的 shard 上。

理解了這些概念以後,我們對數據的分配就有了更清楚的認識。我們對前面提到的問題就有了充分的解釋。表面上看,數據的分布貌似均勻,我們執行幾個查詢語句看看性能怎樣。這裡再次用到 dbKoda 中的 explain 視圖。

上圖中查找年齡在 18 周歲以上的用戶,根據我們的分組定義,三個 shard 上都有對應的紀錄,但是 shard1 對應的年齡組是 20 歲以下,應該包括數量較少的數據,所以在圖中 shard 里表裡現實的 shard01 返回了 9904 條記錄,遠遠少於其他兩個 shard,這也符合我們的數據定義。在上面性能描述中也可以看出,這條語句在 shard01 上面運行的時間也是相對較少的。

再看看下面的例子,如果我們查找 25 周歲以上的用戶,得到的結果中並沒有出現 shard1 的身影,這也是符合我們的數據分配,因為 shard1 只存儲了年齡小於 20 周歲的用戶。

你選擇的 Shard Key 合適嗎?

了解了數據是如何分布的以後,咱們再回過頭來看看我們選擇的 shard key 是否合理。細心的讀者已經發現,上面運行的 explain 結果中存在一個問題,就是 shard3 存儲了大量的數據,如果我們看一下每個年齡組的紀錄個數,會發現 shard1、shard2、shard3 分別包括 198554, 187975, 593673,顯然年齡大於 40 歲的用戶佔了大多數。這並不是我們希望的結果,因為 shard3 成為了集群中的一個瓶頸,資料庫操作語句在 shard3 上運行的速度會大大超過另外兩個 shard,這點從上面的 explain 結果中也可以看到,查詢語句在 shard3 上的運行時間是另外兩個 shard 的兩倍以上。更重要的是,隨著用戶數量的不斷增加,數據的分布也會出現顯著變化,在系統運行一段時間以後,可能 shard2 的用戶數超過 shard3,也有可能 shard1 稱為存儲數據量最多的伺服器。這種數據不平衡是我們不希望看到的。原因在哪裡呢?是不是覺得我們選擇的用戶年齡作為分組條件是一個不太理想的 key。那麼什麼樣的 key 能夠保證數據的均勻分布呢?接下來我們分析一下 shard key 的種類。

Ranged Shard Key

我們上面選擇的年齡分組就是用的這種 shard key。根據 shard key 的取值,它把數據切分成連續的幾個區間。取值相近的紀錄會放進同一個 shard 伺服器。好處是查詢連續取值紀錄時,查詢效率可以得到保證。當資料庫查詢語句發送到 mongos 中時,mongos 會很快的找到目標 shard,而且不需要將語句發送到所有的 shard 上,一般只需要少量的 shard 就可以完成查詢操作。缺點是不能保證數據的平均分配,在數據插入和修改時會產生比較嚴重的性能瓶頸。

Hashed Shard Key

於 Ranged Shard Key 對應的一種被稱之為 Hashed Shard Key,它採用欄位的索引哈希值作為 shard key 的取值,這樣做可以保證數據的均勻分布。在 mongos 和各個 shard 集群之間存在一個哈希值計算方法,所有的數據在遷移時都是根據這個方法來計算數據應當被遷移到什麼地方。當 mongos 接收到一條語句時,通常他會把這條語句廣播到所有的 shard 上去執行。

有了上面的認識,我們如何在 Ranged 和 Shard 之間進行選擇呢?下面兩個屬性是我們選擇 shard key 的關鍵。

Shard Key Cardinality (集)

Cardinality指的是 shard key 可以取到的不同值的個數。他會影響到 Balancer 的運行,這個值也可以被看做是 Balancer 可以創建的最大 chunk 個數。以我們年齡欄位為例,假如一個人的年齡在 100 歲以下,那麼這個欄位的 cardinality 可以取 100 個不同的值。對於一個唯一的年齡數據,不會出現在不同的 chunk 中。如果你選擇的 Shard Key 的 cardinality 很小,比如只有 4 個,那麼數據最多會被分發到 4 個不同的 shard 中,這樣的結構也不適合伺服器的水平擴展,因為不會有數據被分割到第五個 shard 伺服器上。

Shard Key Frequency(頻率)

Frequency指的是 shard key 的重複度,也就是對於一個欄位,有多少取值相同的紀錄。如果大部分數據的 shard key 取值相同,那麼存儲他們的 chunk 會成為資料庫的一個瓶頸。而且,這些 chunk 也變成了不可再切分的 chunk,嚴重影響了資料庫的水平擴展。在這種情況下應當考慮使用組合索引的方式來創建 shard key。所以,盡量選擇低頻率的欄位作為 shard key。

Shard Key Increasing Monotonically (單調增長)

單調增長在這裡的意思是在數據被切分以後,新增加的數據會按照其 shard key 取值向 shard 中插入,如果新增的數據的 key 值都是向最大值方向增加,那麼這些新的數據會被插入到同一個 shard 伺服器上。例如我們前面的用戶年齡分組欄位,如果系統的新增用戶都是年齡大於 40 歲的,那麼 shard3 將會存儲所有的新增用戶,shard3 會成為系統的性能瓶頸。在這種情況下,應當考慮使用 Hashed Shard Key。

重新設計 Shard Key

通過上面的分析我們可以得出結論,前面例子中的用戶年齡欄位是一個很糟糕的方案。有幾個原因:

  • 用戶的年齡不是固定不變的,由於 shard key 是不可變欄位,一旦確定下來以後不能進行修改,所以年齡欄位顯然不是很合適,畢竟沒有年齡永遠不增長的用戶。
  • 一個系統的用戶在不同年齡階段的分布是不一樣的,對於像遊戲、娛樂方面的應用可能會吸引年輕人多一些。而對於醫療、養生方面也許會有更多老年人關注。從這一點上說,這樣的切分也是不恰當的。
  • 選擇年齡欄位並沒有考慮到未來用戶增長方面帶來的問題,有可能在數據切分的時候年齡是均勻分布的,但是系統運行一段時間以後有可能出現不平等的數據分布,這點會給數據維護帶來很大的困擾。

那麼我們應當如何進行選擇呢?看一下用戶表的所有屬性可以發現,其中有一個created_at欄位,它指的是紀錄創建的時間戳。如果採用 Ranged Key 那麼在數據增長方向上會出現單調增長問題,在分析一下發現這個欄位重複的紀錄不多,他有很高的cardinality和非常低的頻率,這樣 Harded key 就成為了很好的備選方案。

分析完理論以後咱們實踐一下看看效果,不幸的是我們並不能修改 shard key,最好的方法就是備份數據,重新創建 shard 集群。創建和數據準備的過程我就不在重複了,你們可以根據前面的例子自己作一遍。

下圖中是我新建的一個userscollection,並以created_at為索引創建了 Hashed Shard Key,注意

created_at必須是一個 hash index 才能成為 hashed shard key。下面是針對用戶表的一次查詢結果。

從圖中可以看到,explain 的結果表示了三個 shard 伺服器基本上均勻分布了所有的數據,三個 shard 上執行時間也都基本均勻,在 500 到 700 多毫秒以內。還記得上面的幾次查詢結果嗎?在數據比較多的 shard 上的運行時間在 1 到 2 毫秒。可以看到總的性能得到了顯著提高。

選擇完美的 Shard Key

在 shard key 的選擇方面,我們需要考慮很多因素,有些是技術的,有些是業務層面的。通常來講應當注意下面幾點:

  • 所有增刪改查語句都可以發送到集群中所有的 shard 伺服器中
  • 任何操作只需要發送到與其相關的 shard 伺服器中,例如一次刪除操作不應當發送到沒有包括要刪除的數據的 shard 伺服器上

權衡利弊,實際上沒有完美的 shard key,只有選擇 shard key 時應當注意和考慮的要素。不會出現一種 shard key 可以滿足所有的增刪改查操作。你需要從給你的應用場景中抽象出用來選擇 shard key 的元素,考量這些要素並作出最後選擇,例如:你的應用是處理讀操作多還是寫操作多?最常用的寫操作場景是什麼樣子的?

小結

在 shard key 的選擇方面沒有一個統一的方法,要根據具體的需求和數據增長的方向來設計。在我們日常的開發過程中,並不是所有技術問題都應當由技術人員來解決,這個世界是一個業務驅動的時代,而技術主要是為業務服務,我們要提高對需求變化的相應速度。像本文中如何選擇 Shard Key 的問題,我覺得並不能單純的通過技術來考量,更多的是要和業務人員討論各個數據欄位的意義,使用的業務價值以及未來業務的增長點。如果在一開始 shard key 的選擇出現錯誤,那麼在接下來的應用過程中想要改變 shard key 是一件極其繁瑣的過程。可能你需要備份你的 collection,然後重新創建 shard 服務並恢複數據,這個過程很可能需要運行很長一段時間。在互聯網應用的今天,伺服器的宕機事件都是以秒為單位計算,很可能錯誤的 shard key 選擇會給你的應用帶來災難性的後果。希望此文能給各位一點啟示,在項目初期的設計階段充分考慮到各方面的因素。

References

1.MongoDB Shard:

docs.mongodb.com/manual

2.Shard Keys:

docs.mongodb.com/manual

3.Next Generation Databases: NoSQLand Big Data, Guy Harrison:

dbKoda:

dbkoda.com

4.MongoDB Docker Cluster:

github.com/zhaoyi0113/m

5.CAP theorem:

en.wikipedia.org/wiki/C

由於微信不能添加外鏈,「新聞來源」相關鏈接可請點擊【閱讀原文】,在「AI前線」知乎專欄中查看

作者簡介:

趙翼,畢業於北京理工大學,目前就職於 SouthbankSoftware,從事 NoSQL,MongoDB 方面的開發工作。曾在 GE,ThoughtWorks,元氣兔擔任項目開發,技術總監等職位,接觸過的項目種類繁多,有 Web,Mobile,醫療器械,社交網路,大數據存儲等。


-全文完-

關注人工智慧的落地實踐,與企業一起探尋 AI 的邊界,AICon 全球人工智慧技術大會火熱售票中,6 折倒計時一周搶票,詳情點擊:

aicon.geekbang.org/?

《深入淺出TensorFlow》迷你書現已發布,關注公眾號「AI前線」,ID:ai-front,回復關鍵字:TF,獲取下載鏈接!


推薦閱讀:

解析 TiDB 在線數據同步工具 Syncer
太閣技術秀:一起聊聊cassandra
PingCAP佈道Percona Live 2017 展示TiDB強悍性能
十分鐘成為 TiDB Contributor 系列 | 添加內建函數

TAG:MongoDB | 扩展 | 分布式数据库 |