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

大數(shù)據(jù)

作者:趙翼

分布式數(shù)據(jù)庫的前世今生

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

Replication

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

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

對此,你可能已經(jīng)想到,前面提到的分布式數(shù)據(jù)庫多點部署是不是會存在大量的瓶頸。比如,在主從數(shù)據(jù)庫結(jié)構(gòu)中,從數(shù)據(jù)庫的內(nèi)容基本上可以說是主數(shù)據(jù)庫的一份全拷貝,這樣的技術(shù)稱之為Replication。Replication在實現(xiàn)主從數(shù)據(jù)同步時,通常采用Transaction Log的方式,比如,當一條數(shù)據(jù)插入到主數(shù)據(jù)庫的時候,主數(shù)據(jù)庫會像Trasaction Log中插入一條記錄來聲明這次數(shù)據(jù)庫寫紀錄的操作。之后,一個Replication Process會被觸發(fā),這個進程會把Transaction Log中的內(nèi)容同步到從數(shù)據(jù)庫中。整個過程如下圖所示:

大數(shù)據(jù)

對于數(shù)據(jù)庫的擴展來說,通常有兩種方法,水平擴展和垂直擴展。

垂直擴展:這種擴展方式比較傳統(tǒng),是針對一臺服務器進行硬件升級,比如添加強大的 CPU,內(nèi)存或者添加磁盤空間等等。這種方式的局限性是僅限于單臺服務器的擴容,盡可能的增加單臺服務器的硬件配置。優(yōu)點是構(gòu)架簡單,只需要維護單臺服務器。水平擴展:這種方式是目前構(gòu)架上的主流形式,指的是通過增加服務器數(shù)量來對系統(tǒng)擴容。在這樣的構(gòu)架下,單臺服務器的配置并不會很高,可能是配置比較低、很廉價的 PC,每臺機器承載著系統(tǒng)的一個子集,所有機器服務器組成的集群會比單體服務器提供更強大、高效的系統(tǒng)容載量。這樣的問題是系統(tǒng)構(gòu)架會比單體服務器復雜,搭建、維護都要求更高的技術(shù)背景。MongoDB 中的 Sharding 正式為了水平擴展而設計的,下面就來擠開 shard 面紗,探討一下 shard 中不同分片的技術(shù)區(qū)別以及對數(shù)據(jù)庫系統(tǒng)的影響。

分片 (Shard)

前面提到的 Replication 結(jié)構(gòu)可以保證數(shù)據(jù)庫中的全部數(shù)據(jù)都會有多分拷貝,數(shù)據(jù)庫的高可用可以保障。但是新的問題是如果要存儲大量的數(shù)據(jù),不論主從服務器,都需要存儲全部數(shù)據(jù),這樣檢索必然會出現(xiàn)性能問題??梢赃@樣講,Replication只能算是分布式數(shù)據(jù)庫的第一階段。主要解決的是數(shù)據(jù)庫高可用,讀數(shù)據(jù)可以水平擴展,部分解決了主數(shù)據(jù)并發(fā)訪問量大的問題。但是它并沒有解決數(shù)據(jù)庫寫操作的分布式需求,此外在數(shù)據(jù)庫查詢時也只限制在一臺服務器上,并不能支持一次查詢多臺數(shù)據(jù)庫服務器。我們假設,如果有一種構(gòu)架,可以實現(xiàn)數(shù)據(jù)庫水平切分,把切分的數(shù)據(jù)分布存儲在不同的服務器上,這樣當查詢請求發(fā)送到數(shù)據(jù)庫時,可以在多臺數(shù)據(jù)庫中異步檢索符合查詢條件的語句,這樣不但可以利用多臺服務器的 CPU,而且還可以充分利用不同服務器上的 IO,顯而易見這樣的構(gòu)架會大大提高查詢語句的性能。但是這樣的實現(xiàn)卻給數(shù)據(jù)庫設計者代碼不少麻煩,首先要解決的就是事務(Transaction),我們知道在進行一次數(shù)據(jù)庫寫操作的時候,需要定一個事務操作,這樣在操作失敗的時候可以回滾到原始狀態(tài),那當在分布式數(shù)據(jù)庫的情況下,事務需要跨越多個數(shù)據(jù)庫節(jié)點以保持數(shù)據(jù)的完整性,這給開發(fā)者帶來不少的麻煩。此外,在關(guān)系型數(shù)據(jù)庫中存在大量表關(guān)聯(lián)的情況,分布式的查詢操作就會牽扯到大量的數(shù)據(jù)遷移,顯然這必將降低數(shù)據(jù)庫性能。但是,在非關(guān)系型數(shù)據(jù)庫中,我們?nèi)趸踔寥コ耸聞蘸投啾黻P(guān)聯(lián)操作,根據(jù) CAP 理論:在分布式數(shù)據(jù)庫環(huán)境中,為了保持構(gòu)架的擴展性,在分區(qū)容錯性不變的前提下,我們必須從一致性和可用性中取其一,那么,從這一點上來理解“NoSQL 數(shù)據(jù)庫是為了保證 A 與 P,而犧牲 C”的說法,也是可以講得通的。同時,根據(jù)該理論,業(yè)界有一種非常流行的認識,那就是:關(guān)系型數(shù)據(jù)庫設計選擇了一致性與可用性,NoSQL 數(shù)據(jù)庫設計則不同。其中,HBase選擇了一致性與分區(qū)可容忍性,Cassandra選擇了可用性與分區(qū)可容忍性。

本文關(guān)注于非關(guān)系型數(shù)據(jù)庫中分區(qū)的技巧和性能,以 MongoDB 為例進行說明,在下面的章節(jié)中就圍繞這一點展開討論。

MongoDB 分片原理

MongoDB 中通過 Shard 支持服務器水平擴展,通過 Replication 支持高可用(HA)。這兩種技術(shù)可以分開來使用,但是在大數(shù)據(jù)庫企業(yè)級應用中通常人們會把他們結(jié)合在一起使用。

MongoDB Sharding

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

大數(shù)據(jù)

其中:

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

MongoDB Shard 性能分析

環(huán)境準備

這樣的服務器構(gòu)架是否合理,或者說是否能夠滿足數(shù)據(jù)量不斷增長的需求。如果僅僅是通過理論解釋恐怕很難服眾,我已經(jīng)信奉理論結(jié)合實際的工作方式,所以在我的文章中除了闡述理論之外,一定會有一定的示例為大家驗證理論的結(jié)果。接下來我們就根據(jù)上面的例子做一套本地運行環(huán)境。由于 MongoDB 的便捷性,我們可以在任何一臺 PC 上搭建這樣一個數(shù)據(jù)庫集群環(huán)境,并且不限制操作系統(tǒng)類型,任何 Windows/Linux/Mac 的主流版本都可以運行這樣的環(huán)境。在本文中,我才用 MongoDB3.4 版本。

對于如何創(chuàng)建一個 MongoDB Shard 環(huán)境,網(wǎng)上有很多教程和命令供大家選擇,創(chuàng)建一個有 3 個 Mongos,每個 Mongos 連接若干個 Shards,再加上 3 個 config server cluster,通常需要 20 幾臺 MongoDB 服務器。如果一行命令一行命令的打,即便是在非常熟練的情況下,沒有半個小時恐怕搭建不出來。不過幸運的是有第三方庫幫我們做這個事情,大家可以查看一下mtools。他是用來創(chuàng)建各種 MongoDB 環(huán)境的命令行工具,代碼使用python寫的,可以通過pip install安裝到你的環(huán)境上。具體的使用方法可以參考https://github.com/rueckstiess/mtools/wiki/mlaunch。也可以通過https://github.com/zhaoyi0113/mongo-cluster-docker上面的腳本把環(huán)境搭載 Docker 上面。

下面的命令用來在本地創(chuàng)建一個 MongoDB Shard 集群,包含 1 個mongos路由,3 個shardreplica,每個 replica 有 3 個shard服務器,3 個config服務器。這樣一共創(chuàng)建 13 個進程。

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

服務器創(chuàng)建好以后我們可以連接到mongos上看一下 shard 狀態(tài),端口是上面制定的 38017。

mongos> sh.status()--- Sharding Status ---  ?... ?shards: ? ?{ ?"_id" : "shard01", ?"host" : "shard01/localhost:38018,localhost:38019,localhost:38020", ?"state" : 1 } ? ?{ ?"_id" : "shard02", ?"host" : "shard02/localhost:38021,localhost:38022,localhost:38023", ?"state" : 1 } ? ?{ ?"_id" : "shard03", ?"host" : "shard03/localhost:38024,localhost:38025,localhost:38026", ?"state" : 1 } ?active mongoses: ? ?"3.4.0" : 1 ?...

可以看到剛才創(chuàng)建的 shard 服務器已經(jīng)加入到這臺 mongos 中了,這里有 3 個 shard cluster,每個 cluster包含 3 個 shard 服務器。除此之外,我們并沒有看到關(guān)于 Shard 更多的信息。這是因為這臺服務器集群還沒有任何數(shù)據(jù),而且也沒有進行數(shù)據(jù)切分。

數(shù)據(jù)準備

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

{ ? ?"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插入一百條隨機數(shù)據(jù)。隨機數(shù)據(jù)的格式就按照上面json文件的定義。你可以通過調(diào)整?–num的參數(shù)來插入不同數(shù)量的 Document。(Link to mgenerate wiki)

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

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

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

大數(shù)據(jù)

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

配置 Shard 數(shù)據(jù)庫

環(huán)境搭建好并且數(shù)據(jù)已經(jīng)準備完畢以后,接下來的事情就是配置數(shù)據(jù)庫并切分數(shù)據(jù)。方便起見,我們把用戶分為三組,20 歲以下(junior),20 到 40 歲(middle)和 40 歲以上(senior),為了節(jié)省篇幅,我在這里不過多的介紹如何使用 MongoDB 命令,按照下面的幾條命令執(zhí)行以后,我們的數(shù)據(jù)會按照用戶年齡段拆分成若干個 chunk,并分發(fā)到不同的 shard cluster 中。如果對下面的命令不熟悉,可以查看 MongoDB 官方文檔關(guān)于 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 創(chuàng)建索引,之后禁止 Balancer 的運行,這么做的原因是不希望在 Shard Collection 的過程中還運行 Balancer。之后將數(shù)據(jù)按照年齡分成三組,分別標記為junior,?middle,senior并把這三組分別分配到三個 Shard 集群中。 之后對 test 庫中的 users collection 進行按用戶年齡字段的切分操作,如果 Shard collection 成功返回,你會得到下面的輸出結(jié)果:{ “collectionsharded” : “test.users”, “ok” : 1 }。

關(guān)于 Shard 需要注意的幾點

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

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

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

大數(shù)據(jù)

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

Chunk

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

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

Zones

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

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

大數(shù)據(jù)

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

再看看下面的例子,如果我們查找 25 周歲以上的用戶,得到的結(jié)果中并沒有出現(xiàn) shard1 的身影,這也是符合我們的數(shù)據(jù)分配,因為 shard1 只存儲了年齡小于 20 周歲的用戶。

大數(shù)據(jù)

你選擇的 Shard Key 合適嗎?

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

Ranged Shard Key

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

Hashed Shard Key

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

有了上面的認識,我們?nèi)绾卧?Ranged 和 Shard 之間進行選擇呢?下面兩個屬性是我們選擇 shard key 的關(guān)鍵。

Shard Key Cardinality (集)

Cardinality指的是 shard key 可以取到的不同值的個數(shù)。他會影響到 Balancer 的運行,這個值也可以被看做是 Balancer 可以創(chuàng)建的最大 chunk 個數(shù)。以我們年齡字段為例,假如一個人的年齡在 100 歲以下,那么這個字段的 cardinality 可以取 100 個不同的值。對于一個唯一的年齡數(shù)據(jù),不會出現(xiàn)在不同的 chunk 中。如果你選擇的 Shard Key 的 cardinality 很小,比如只有 4 個,那么數(shù)據(jù)最多會被分發(fā)到 4 個不同的 shard 中,這樣的結(jié)構(gòu)也不適合服務器的水平擴展,因為不會有數(shù)據(jù)被分割到第五個 shard 服務器上。

Shard Key Frequency(頻率)

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

Shard Key Increasing Monotonically (單調(diào)增長)

單調(diào)增長在這里的意思是在數(shù)據(jù)被切分以后,新增加的數(shù)據(jù)會按照其 shard key 取值向 shard 中插入,如果新增的數(shù)據(jù)的 key 值都是向最大值方向增加,那么這些新的數(shù)據(jù)會被插入到同一個 shard 服務器上。例如我們前面的用戶年齡分組字段,如果系統(tǒng)的新增用戶都是年齡大于 40 歲的,那么 shard3 將會存儲所有的新增用戶,shard3 會成為系統(tǒng)的性能瓶頸。在這種情況下,應當考慮使用 Hashed Shard Key。

重新設計 Shard Key

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

用戶的年齡不是固定不變的,由于 shard key 是不可變字段,一旦確定下來以后不能進行修改,所以年齡字段顯然不是很合適,畢竟沒有年齡永遠不增長的用戶。一個系統(tǒng)的用戶在不同年齡階段的分布是不一樣的,對于像游戲、娛樂方面的應用可能會吸引年輕人多一些。而對于醫(yī)療、養(yǎng)生方面也許會有更多老年人關(guān)注。從這一點上說,這樣的切分也是不恰當?shù)?。選擇年齡字段并沒有考慮到未來用戶增長方面帶來的問題,有可能在數(shù)據(jù)切分的時候年齡是均勻分布的,但是系統(tǒng)運行一段時間以后有可能出現(xiàn)不平等的數(shù)據(jù)分布,這點會給數(shù)據(jù)維護帶來很大的困擾。

那么我們應當如何進行選擇呢?看一下用戶表的所有屬性可以發(fā)現(xiàn),其中有一個created_at字段,它指的是紀錄創(chuàng)建的時間戳。如果采用 Ranged Key 那么在數(shù)據(jù)增長方向上會出現(xiàn)單調(diào)增長問題,在分析一下發(fā)現(xiàn)這個字段重復的紀錄不多,他有很高的cardinality和非常低的頻率,這樣 Harded key 就成為了很好的備選方案。

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

下圖中是我新建的一個userscollection,并以created_at為索引創(chuàng)建了 Hashed Shard Key,注意created_at必須是一個 hash index 才能成為 hashed shard key。下面是針對用戶表的一次查詢結(jié)果。

大數(shù)據(jù)

從圖中可以看到,explain 的結(jié)果表示了三個 shard 服務器基本上均勻分布了所有的數(shù)據(jù),三個 shard 上執(zhí)行時間也都基本均勻,在 500 到 700 多毫秒以內(nèi)。還記得上面的幾次查詢結(jié)果嗎?在數(shù)據(jù)比較多的 shard 上的運行時間在 1 到 2 毫秒??梢钥吹娇偟男阅艿玫搅孙@著提高。

選擇完美的 Shard Key

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

所有增刪改查語句都可以發(fā)送到集群中所有的 shard 服務器中任何操作只需要發(fā)送到與其相關(guān)的 shard 服務器中,例如一次刪除操作不應當發(fā)送到?jīng)]有包括要刪除的數(shù)據(jù)的 shard 服務器上

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

小結(jié)

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

極客網(wǎng)企業(yè)會員

免責聲明:本網(wǎng)站內(nèi)容主要來自原創(chuàng)、合作伙伴供稿和第三方自媒體作者投稿,凡在本網(wǎng)站出現(xiàn)的信息,均僅供參考。本網(wǎng)站將盡力確保所提供信息的準確性及可靠性,但不保證有關(guān)資料的準確性及可靠性,讀者在使用前請進一步核實,并對任何自主決定的行為負責。本網(wǎng)站對有關(guān)資料所引致的錯誤、不確或遺漏,概不負任何法律責任。任何單位或個人認為本網(wǎng)站中的網(wǎng)頁或鏈接內(nèi)容可能涉嫌侵犯其知識產(chǎn)權(quán)或存在不實內(nèi)容時,應及時向本網(wǎng)站提出書面權(quán)利通知或不實情況說明,并提供身份證明、權(quán)屬證明及詳細侵權(quán)或不實情況證明。本網(wǎng)站在收到上述法律文件后,將會依法盡快聯(lián)系相關(guān)文章源頭核實,溝通刪除相關(guān)內(nèi)容或斷開相關(guān)鏈接。

2017-11-09
MongoDB的水平擴展,你做對了嗎?
作者:趙翼 分布式數(shù)據(jù)庫的前世今生 當人們一開始使用數(shù)據(jù)庫系統(tǒng)的時候,所有數(shù)據(jù)都是跑在一臺服務器上,即所謂的單機數(shù)據(jù)庫服務器。在企業(yè)級應用中,我們會搭建一臺應用

長按掃碼 閱讀全文