作者:趙翼
分布式數(shù)據庫的前世今生
當人們一開始使用數(shù)據庫系統(tǒng)的時候,所有數(shù)據都是跑在一臺服務器上,即所謂的單機數(shù)據庫服務器。在企業(yè)級應用中,我們會搭建一臺應用程序服務器,一般它會被運行在一臺服務器或者工作站上,大多數(shù)情況下采用 Linux/Unix/Windows 操作系統(tǒng),也有人把這樣的服務器稱之為應用程序服務器。顧名思義,他的作用是處理復雜的業(yè)務邏輯。但是一點需要注意的是,在這樣的構架中,這臺應用程序服務器不會存儲任何業(yè)務數(shù)據,也就是說,他只負責邏輯運算,處理用戶請求,真正存放數(shù)據的地方是前面提到的那臺數(shù)據庫服務器。應用程序服務器將用戶的請求轉換成數(shù)據庫語言(通常是 SQL),運行在數(shù)據庫中,從而進行數(shù)據的增刪改查。數(shù)據庫服務器不會對外直接開放,管理人員也不允許直接在數(shù)據庫層面操作數(shù)據。所有的操作都會經過應用程序服務器來完成。應用程序層、數(shù)據庫層再加上 UI 層,被稱為傳統(tǒng)的 Web 三層構架。
Replication
隨著數(shù)據量的增大,技術的不斷進步以及需求的增加,安全性、可靠性、容錯性、可恢復性等因素被人們考慮進數(shù)據庫的設計中。于是出現(xiàn)了分布式數(shù)據庫系統(tǒng)。以前在存儲數(shù)據的時候,都是采用單體構架模式,及數(shù)據全部存儲在一臺數(shù)據庫中,一旦數(shù)據庫出現(xiàn)問題,所有的應用請求都會受到影響。數(shù)據庫的恢復也是一個令人頭疼的問題。有時一次數(shù)據庫的全恢復會運行幾個小時甚至幾天的時間。在互聯(lián)網應用不斷普及的今天,業(yè)務需求對構架產生了嚴峻的挑戰(zhàn)。沒有哪個互聯(lián)網應用會允許若干小時的宕機時間。分布式數(shù)據庫的產生,為我們提供了技術上的解決方案。在部署數(shù)據庫的時候,不用于以前的單體應用,分布式下數(shù)據庫部署包括多點部署,一套業(yè)務應用數(shù)據庫被分布在多臺數(shù)據庫服務器上,分主從服務器。主服務器處理日常業(yè)務請求,從服務器在運行時不斷的對主服務器進行備份,當主服務器出現(xiàn)宕機、或者運行不穩(wěn)定的情況時,從服務器會立刻替換成主服務器,繼續(xù)對外提供服務。此時,開發(fā)運維人員會對出現(xiàn)問題的服務器進行搶修、恢復,之后再把它投入到生產環(huán)境中。這樣的構架也被稱作為高可用構架,它支持了災難恢復,為業(yè)務世界提供了可靠的支持,也是很多企業(yè)級應用采用的主流構架之一。需要指出的是,在這樣的主從設計中,從數(shù)據庫常常被設計成只讀,主數(shù)據庫支持讀寫操作。一般會有一臺主數(shù)據庫連接若干臺從數(shù)據庫。在互聯(lián)網產品的應用中,人們大多數(shù)情況下會對應用服務器請求讀操作,這樣應用服務器可以把讀操作請求分發(fā)到若干個從數(shù)據庫中,這樣就避免了主數(shù)據庫的并發(fā)請求次數(shù)過高的問題。至于為什么大多數(shù)應用都是讀操作,你可以想一下在你使用微信或者微博的時候,你是看別人發(fā)布的圖片多還是自己發(fā)布的時候多。當你不斷下滑屏幕,刷新朋友圈,這些都是讀請求。只有當評論、點贊、分享的時候才會進行寫操作。
我們的世界就是這樣,當技術為人們解決了現(xiàn)實問題以后,新的需求會層出不窮。智能手機,互聯(lián)網 +,創(chuàng)業(yè)潮的不斷興起,點燃了這樣一個擁有幾千年文明歷史的民族的激情。各種新點子、新概念不斷的涌現(xiàn),誰的手機里沒有安裝幾十個互聯(lián)網應用,從訂餐,快遞,到住房,旅游,再到教育,養(yǎng)老,那一個環(huán)節(jié)沒有互聯(lián)網的支持,沒有技術的成分。我們就是生存在這樣一個的平凡而又不乏豪情的社會中。許許多多的需求和數(shù)據充斥著我們的構架,挑戰(zhàn)著我們的存儲。
對此,你可能已經想到,前面提到的分布式數(shù)據庫多點部署是不是會存在大量的瓶頸。比如,在主從數(shù)據庫結構中,從數(shù)據庫的內容基本上可以說是主數(shù)據庫的一份全拷貝,這樣的技術稱之為Replication。Replication在實現(xiàn)主從數(shù)據同步時,通常采用Transaction Log的方式,比如,當一條數(shù)據插入到主數(shù)據庫的時候,主數(shù)據庫會像Trasaction Log中插入一條記錄來聲明這次數(shù)據庫寫紀錄的操作。之后,一個Replication Process會被觸發(fā),這個進程會把Transaction Log中的內容同步到從數(shù)據庫中。整個過程如下圖所示:
對于數(shù)據庫的擴展來說,通常有兩種方法,水平擴展和垂直擴展。
垂直擴展:這種擴展方式比較傳統(tǒng),是針對一臺服務器進行硬件升級,比如添加強大的 CPU,內存或者添加磁盤空間等等。這種方式的局限性是僅限于單臺服務器的擴容,盡可能的增加單臺服務器的硬件配置。優(yōu)點是構架簡單,只需要維護單臺服務器。水平擴展:這種方式是目前構架上的主流形式,指的是通過增加服務器數(shù)量來對系統(tǒng)擴容。在這樣的構架下,單臺服務器的配置并不會很高,可能是配置比較低、很廉價的 PC,每臺機器承載著系統(tǒng)的一個子集,所有機器服務器組成的集群會比單體服務器提供更強大、高效的系統(tǒng)容載量。這樣的問題是系統(tǒng)構架會比單體服務器復雜,搭建、維護都要求更高的技術背景。MongoDB 中的 Sharding 正式為了水平擴展而設計的,下面就來擠開 shard 面紗,探討一下 shard 中不同分片的技術區(qū)別以及對數(shù)據庫系統(tǒng)的影響。分片 (Shard)
前面提到的 Replication 結構可以保證數(shù)據庫中的全部數(shù)據都會有多分拷貝,數(shù)據庫的高可用可以保障。但是新的問題是如果要存儲大量的數(shù)據,不論主從服務器,都需要存儲全部數(shù)據,這樣檢索必然會出現(xiàn)性能問題。可以這樣講,Replication只能算是分布式數(shù)據庫的第一階段。主要解決的是數(shù)據庫高可用,讀數(shù)據可以水平擴展,部分解決了主數(shù)據并發(fā)訪問量大的問題。但是它并沒有解決數(shù)據庫寫操作的分布式需求,此外在數(shù)據庫查詢時也只限制在一臺服務器上,并不能支持一次查詢多臺數(shù)據庫服務器。我們假設,如果有一種構架,可以實現(xiàn)數(shù)據庫水平切分,把切分的數(shù)據分布存儲在不同的服務器上,這樣當查詢請求發(fā)送到數(shù)據庫時,可以在多臺數(shù)據庫中異步檢索符合查詢條件的語句,這樣不但可以利用多臺服務器的 CPU,而且還可以充分利用不同服務器上的 IO,顯而易見這樣的構架會大大提高查詢語句的性能。但是這樣的實現(xiàn)卻給數(shù)據庫設計者代碼不少麻煩,首先要解決的就是事務(Transaction),我們知道在進行一次數(shù)據庫寫操作的時候,需要定一個事務操作,這樣在操作失敗的時候可以回滾到原始狀態(tài),那當在分布式數(shù)據庫的情況下,事務需要跨越多個數(shù)據庫節(jié)點以保持數(shù)據的完整性,這給開發(fā)者帶來不少的麻煩。此外,在關系型數(shù)據庫中存在大量表關聯(lián)的情況,分布式的查詢操作就會牽扯到大量的數(shù)據遷移,顯然這必將降低數(shù)據庫性能。但是,在非關系型數(shù)據庫中,我們弱化甚至去除了事務和多表關聯(lián)操作,根據 CAP 理論:在分布式數(shù)據庫環(huán)境中,為了保持構架的擴展性,在分區(qū)容錯性不變的前提下,我們必須從一致性和可用性中取其一,那么,從這一點上來理解“NoSQL 數(shù)據庫是為了保證 A 與 P,而犧牲 C”的說法,也是可以講得通的。同時,根據該理論,業(yè)界有一種非常流行的認識,那就是:關系型數(shù)據庫設計選擇了一致性與可用性,NoSQL 數(shù)據庫設計則不同。其中,HBase選擇了一致性與分區(qū)可容忍性,Cassandra選擇了可用性與分區(qū)可容忍性。
本文關注于非關系型數(shù)據庫中分區(qū)的技巧和性能,以 MongoDB 為例進行說明,在下面的章節(jié)中就圍繞這一點展開討論。
MongoDB 分片原理
MongoDB 中通過 Shard 支持服務器水平擴展,通過 Replication 支持高可用(HA)。這兩種技術可以分開來使用,但是在大數(shù)據庫企業(yè)級應用中通常人們會把他們結合在一起使用。
MongoDB Sharding
首先我們簡要概述一下分片在 MongoDB 中的工作原理。通過分片這個單詞我們可以看出,他的意思是將數(shù)據庫表中的數(shù)據按照一定的邊界分成若干組,每一組放到一臺 MongoDB 服務器上。拿用戶數(shù)據舉例,比如你有一張數(shù)據表存放用戶基本信息,可能由于你的應用很受歡迎,短時間內就積攢了上億個用戶,這樣當你在這張表上進行查詢時通常會耗費比較長的時間,這樣這個用戶表就稱為了你的應用程序的性能瓶頸。很顯然的做法是對這張用戶表進行拆分,假設用戶表中有一個age年齡字段,我們先做一個簡單的拆分操作,按照用戶的年齡段把數(shù)據放到不同的服務器上,以 20 為一個單位,20 歲以下的用戶放到 server1,20 到 40 歲的用戶放到 server2,40-60 歲的用戶放到 server3,60 歲以上放到 server4,后面我們會講這樣的拆分是否合理。在這個例子中,用戶年齡age就是我們進行Sharding(切分)的Shard Key(關于 Shard Key 的選擇后面會詳細介紹),拆分出來的server1,?server2,?server3和server4就是這個集群中的 4 個Shard(分區(qū))服務器。好,Shard 集群已經有了,并且數(shù)據已經拆分完好,當用戶進行一次查詢請求的時候我們如何向這四個 Shard 服務器發(fā)送請求呢?例如:我的查詢條件是用戶年齡在 18 到 35 歲之間,這樣個查詢請求應當發(fā)送到server1和server2,因為他們存儲了用戶年齡在 40 以下的數(shù)據,我們不希望這樣的請求發(fā)送到另外兩臺服務器中因為他們并不會返回任何數(shù)據結果。此時,另外一個成員就要登場了,mongos,它可以被稱為 Shard 集群中的路由器,就像我們網絡環(huán)境中使用的路由器一樣,它的作用就是講請求轉發(fā)到對應的目標服務器中,有了它我們剛才那條查詢語句就會正確的轉發(fā)給server和server2,而不會發(fā)送到server3和server4上。mongos根據用戶年齡(Shard Key)分析查詢語句,并把語句發(fā)送到相關的 shard 服務器中。除了mongos和shard之外,另一個必須的成員是配置服務器,config server,它存儲 Shard 集群中所有其他成員的配置信息,mongos會到這臺config server查看集群中其他服務器的地址,這是一臺不需要太高性能的服務器,因為它不會用來做復雜的查詢計算,值得注意的是,在 MongoDB3.4 以后,config server必須是一個replica set。理解了上面的例子以后,一個 Shard 集群就可以部署成下圖所示的結構:
其中:
shard: 每一個 Shard 服務器存儲數(shù)據的一個子集,例如上面的用戶表,每一個 Shard 存儲一個年齡段的用戶數(shù)據。mongos: 處理來自應用服務器的請求,它是在應用服務器和Shard 集群之間的一個接口。config server: 存儲 shard 集群的配置信息,通常部署在一個 replica set 上。MongoDB Shard 性能分析
環(huán)境準備
這樣的服務器構架是否合理,或者說是否能夠滿足數(shù)據量不斷增長的需求。如果僅僅是通過理論解釋恐怕很難服眾,我已經信奉理論結合實際的工作方式,所以在我的文章中除了闡述理論之外,一定會有一定的示例為大家驗證理論的結果。接下來我們就根據上面的例子做一套本地運行環(huán)境。由于 MongoDB 的便捷性,我們可以在任何一臺 PC 上搭建這樣一個數(shù)據庫集群環(huán)境,并且不限制操作系統(tǒng)類型,任何 Windows/Linux/Mac 的主流版本都可以運行這樣的環(huán)境。在本文中,我才用 MongoDB3.4 版本。
對于如何創(chuàng)建一個 MongoDB Shard 環(huán)境,網上有很多教程和命令供大家選擇,創(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 服務器已經加入到這臺 mongos 中了,這里有 3 個 shard cluster,每個 cluster包含 3 個 shard 服務器。除此之外,我們并沒有看到關于 Shard 更多的信息。這是因為這臺服務器集群還沒有任何數(shù)據,而且也沒有進行數(shù)據切分。
數(shù)據準備
首先是數(shù)據的錄入,為了分析我們服務器集群的性能,需要準備大量的用戶數(shù)據,幸運的是mtools提供了mgenerate方法供我們使用。他可以根據一個數(shù)據模版向 MongoDB 中插入任意條 json 數(shù)據。下面的 json 結構是我們在例子中需要使用的數(shù)據模版:
{ ? ?"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ù)據。隨機數(shù)據的格式就按照上面json文件的定義。你可以通過調整?–num的參數(shù)來插入不同數(shù)量的 Document。(Link to mgenerate wiki)
mgenerate user.json --num 1000000 --database test --collection users --port 38017
上面的命令會像test數(shù)據庫中users?collection 插入一百萬條數(shù)據。在有些機器上,運行上面的語句可能需要等待一段時間,因為生成一百萬條數(shù)據是一個比較耗時的操作,之所以生成如此多的數(shù)據是方便后面我們分析性能時,可以看到性能的顯著差別。當然你也可以只生成十萬條數(shù)據來進行測試,只要能夠在你的機器上看到不同find語句的執(zhí)行時間差異就可以。
插入完數(shù)據之后,我們想看一下剛剛插入的數(shù)據在服務器集群中是如何分配的。通常,可以通過sh.status()?MongoDB shell 命令查看。不過對于一套全新的集群服務器,再沒有切分任何 collection 之前,我們是看不到太多有用的信息。不過,可以通過 explain 一條查詢語句來看一下數(shù)據的分布情況。這里不得不強調一下在進行數(shù)據性能分析時一個好的 IDE 對工作效率有多大的影響,我選擇 dbKoda 作為 MongoDB 的 IDE 主要原因是他是目前唯一一款對 MongoDB Shell 的完美演繹,對于 MongoDB Shell 命令不太熟悉的開發(fā)人員來說尤為重要,幸運的是這款 IDE 還支持 Windows/Mac/Linux 三種平臺,基本上覆蓋了絕大多數(shù)操作系統(tǒng)版本。下面是對剛才建立的一百萬條 collection 的一次 find 的 explain 結果。(對于 Explain 的應用,大家可以參考我的另外一片文章:如何通過 MongoDB 自帶的 Explain 功能提高檢索性能?)
從上圖中可以看到,我們插入的一百萬條數(shù)據全部被分配到了第一個 shard 服務器中,這并不是我們想看到的結果,不要著急,因為我還沒有進行數(shù)據切分,MongoDB 并不會自動的分配這些數(shù)據。下面我們來一點一點分析如何利用 Shard 實現(xiàn)高效的數(shù)據查詢。
配置 Shard 數(shù)據庫
環(huán)境搭建好并且數(shù)據已經準備完畢以后,接下來的事情就是配置數(shù)據庫并切分數(shù)據。方便起見,我們把用戶分為三組,20 歲以下(junior),20 到 40 歲(middle)和 40 歲以上(senior),為了節(jié)省篇幅,我在這里不過多的介紹如何使用 MongoDB 命令,按照下面的幾條命令執(zhí)行以后,我們的數(shù)據會按照用戶年齡段拆分成若干個 chunk,并分發(fā)到不同的 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 創(chuàng)建索引,之后禁止 Balancer 的運行,這么做的原因是不希望在 Shard Collection 的過程中還運行 Balancer。之后將數(shù)據按照年齡分成三組,分別標記為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 會運行一段時間,因為他要對分組的數(shù)據重新分配到指定的 shard 服務器上,你可以通過sh.isBalancerRunning()命令查看 Balancer 是否正在運行?,F(xiàn)在可以稍事休息一下喝杯咖啡或看看窗外的風景。
為了理解數(shù)據如何分布在 3 個 shard 集群中,我們有必要分析一下 chunk 和 zone 的劃分,下圖是在 dbKoda 上顯示 Shard Cluster 統(tǒng)計數(shù)據,可以看到數(shù)據總共被分成 6 個 chunks,每個 shard 集群存儲 2 個 chunk。
對此有些同學會有疑問,為什么我們的數(shù)據會被分為 6 個 chunks,而且每個 shard 集群個分配了 2 個 chunk。是誰來保證數(shù)據的均勻分配?下面我就給大家解釋一下他們的概念以及我們應當如何使用。
Chunk
我們已經知道 MongoDB 是通過 shard key 來對數(shù)據進行切分,被切分出來的數(shù)據被分配到若干個 chunks 中。一個 chunk 可以被認為是一臺 shard 服務器中數(shù)據的子集,根據 shard key,每個 chunk 都有上下邊界,在我們的例子中,邊界值就是用戶年齡。chunk 有自己的大小,數(shù)據不斷插入到 mongos 的過程中,chunk 的大小會發(fā)生變化,chunk 的默認大小是 64M。當然 MongoDB 允許你對 chunk 的大小進行設置,你也可以把一個 chunk 切分成若干個小 chunk,或者合并多個 chunk。一般我不建議大家手動操作 chunk 的大小,或者在 mongos 層面切分或合并 chunk,除非真有合適的原因才去這么做。原因是,在數(shù)據不斷插入到我們的集群中時,mongodb 中的 chunk 大小會發(fā)生很大的變化,當一個 chunk 的大小超過了最大值,mongo 會根據 shard key 對 chunk 進行切分,在必要的時候,一個 chunk 可能會被切分成多個小 chunk,大多數(shù)情況下這種自動行為已經滿足了我們日常的業(yè)務需求,無需進行手動操作,另一點原因是當進行 chunk 切分后,直接的結果會導致數(shù)據分配的不均勻,此時 balancer 會被調用來進行數(shù)據重新分配,很多時候這個操作會運行很長時間,無形中導致了內部結構的負載平衡,因此不建議大家手動拆分。當然,理解 chunk 的分配原理還是有助于大家分析數(shù)據庫性能的必要條件。我在這里不過多的將如何進行這些操作,有興趣的讀者可以參考 MongoDB 官方文檔,上面有比較全面的解釋。這里我只強調在進行 chunk 操作的時候,要注意一下幾個方面,這些都是影響你 MongoDB 性能的關鍵因素。
如果存在大量體積很小的 chunk,他可以保證你的數(shù)據均勻的分布在 shard 集群中但是可能會導致頻繁的數(shù)據遷移。這將加重 mongos 層面上的操作。大的 chunk 會減少數(shù)據遷移,減輕網絡負擔,降低在 mongos 路由層面上的負載,但弊端是有可能導致數(shù)據在 shard 集群中分布的不均勻。Balancer 會在數(shù)據分配不均勻的時候自動運行,那么 Balancer 是如何決定什么情況下需要進行數(shù)據遷移呢?答案是 Migration Thresholds,當 chunk 的數(shù)量在不同 shard replica 之間超過一個定值時,balancer 會自動運行,這個定值根據你的 shard 數(shù)量不同而不同。Zones
可以說 chunk 是 MongoDB 在多個 shard 集群中遷移數(shù)據的最小單元,有時候數(shù)據的分配不會按照我們臆想的方向進行,就拿上面的例子來說,雖然我們選擇了用戶年齡作為 shard key,但是 MongoDB 并不會按照我們設想的那樣來分配數(shù)據,如何進行數(shù)據分配就是通過 Zones 來實現(xiàn)。Zones 解決了 shard 集群與 shard key 之間的關系,我們可以按照 shard key 對數(shù)據進行分組,每一組稱之為一個 Zone,之后把 Zone 在分配給不同的 Shard 服務器。一個 Shard 可以存儲一個或多個 Zone,前提是 Zone 之間沒有數(shù)據沖突。Balancer 在運行的時候會把在 Zone 里的 chunk 遷移到關聯(lián)這個 Zone 的 shard 上。
理解了這些概念以后,我們對數(shù)據的分配就有了更清楚的認識。我們對前面提到的問題就有了充分的解釋。表面上看,數(shù)據的分布貌似均勻,我們執(zhí)行幾個查詢語句看看性能怎樣。這里再次用到 dbKoda 中的 explain 視圖。
上圖中查找年齡在 18 周歲以上的用戶,根據我們的分組定義,三個 shard 上都有對應的紀錄,但是 shard1 對應的年齡組是 20 歲以下,應該包括數(shù)量較少的數(shù)據,所以在圖中 shard 里表里現(xiàn)實的 shard01 返回了 9904 條記錄,遠遠少于其他兩個 shard,這也符合我們的數(shù)據定義。在上面性能描述中也可以看出,這條語句在 shard01 上面運行的時間也是相對較少的。
再看看下面的例子,如果我們查找 25 周歲以上的用戶,得到的結果中并沒有出現(xiàn) shard1 的身影,這也是符合我們的數(shù)據分配,因為 shard1 只存儲了年齡小于 20 周歲的用戶。
你選擇的 Shard Key 合適嗎?
了解了數(shù)據是如何分布的以后,咱們再回過頭來看看我們選擇的 shard key 是否合理。細心的讀者已經發(fā)現(xiàn),上面運行的 explain 結果中存在一個問題,就是 shard3 存儲了大量的數(shù)據,如果我們看一下每個年齡組的紀錄個數(shù),會發(fā)現(xiàn) shard1、shard2、shard3 分別包括 198554, 187975, 593673,顯然年齡大于 40 歲的用戶占了大多數(shù)。這并不是我們希望的結果,因為 shard3 成為了集群中的一個瓶頸,數(shù)據庫操作語句在 shard3 上運行的速度會大大超過另外兩個 shard,這點從上面的 explain 結果中也可以看到,查詢語句在 shard3 上的運行時間是另外兩個 shard 的兩倍以上。更重要的是,隨著用戶數(shù)量的不斷增加,數(shù)據的分布也會出現(xiàn)顯著變化,在系統(tǒng)運行一段時間以后,可能 shard2 的用戶數(shù)超過 shard3,也有可能 shard1 稱為存儲數(shù)據量最多的服務器。這種數(shù)據不平衡是我們不希望看到的。原因在哪里呢?是不是覺得我們選擇的用戶年齡作為分組條件是一個不太理想的 key。那么什么樣的 key 能夠保證數(shù)據的均勻分布呢?接下來我們分析一下 shard key 的種類。
Ranged Shard Key
我們上面選擇的年齡分組就是用的這種 shard key。根據 shard key 的取值,它把數(shù)據切分成連續(xù)的幾個區(qū)間。取值相近的紀錄會放進同一個 shard 服務器。好處是查詢連續(xù)取值紀錄時,查詢效率可以得到保證。當數(shù)據庫查詢語句發(fā)送到 mongos 中時,mongos 會很快的找到目標 shard,而且不需要將語句發(fā)送到所有的 shard 上,一般只需要少量的 shard 就可以完成查詢操作。缺點是不能保證數(shù)據的平均分配,在數(shù)據插入和修改時會產生比較嚴重的性能瓶頸。
Hashed Shard Key
于 Ranged Shard Key 對應的一種被稱之為 Hashed Shard Key,它采用字段的索引哈希值作為 shard key 的取值,這樣做可以保證數(shù)據的均勻分布。在 mongos 和各個 shard 集群之間存在一個哈希值計算方法,所有的數(shù)據在遷移時都是根據這個方法來計算數(shù)據應當被遷移到什么地方。當 mongos 接收到一條語句時,通常他會把這條語句廣播到所有的 shard 上去執(zhí)行。
有了上面的認識,我們如何在 Ranged 和 Shard 之間進行選擇呢?下面兩個屬性是我們選擇 shard key 的關鍵。
Shard Key Cardinality (集)
Cardinality指的是 shard key 可以取到的不同值的個數(shù)。他會影響到 Balancer 的運行,這個值也可以被看做是 Balancer 可以創(chuàng)建的最大 chunk 個數(shù)。以我們年齡字段為例,假如一個人的年齡在 100 歲以下,那么這個字段的 cardinality 可以取 100 個不同的值。對于一個唯一的年齡數(shù)據,不會出現(xiàn)在不同的 chunk 中。如果你選擇的 Shard Key 的 cardinality 很小,比如只有 4 個,那么數(shù)據最多會被分發(fā)到 4 個不同的 shard 中,這樣的結構也不適合服務器的水平擴展,因為不會有數(shù)據被分割到第五個 shard 服務器上。
Shard Key Frequency(頻率)
Frequency指的是 shard key 的重復度,也就是對于一個字段,有多少取值相同的紀錄。如果大部分數(shù)據的 shard key 取值相同,那么存儲他們的 chunk 會成為數(shù)據庫的一個瓶頸。而且,這些 chunk 也變成了不可再切分的 chunk,嚴重影響了數(shù)據庫的水平擴展。在這種情況下應當考慮使用組合索引的方式來創(chuàng)建 shard key。所以,盡量選擇低頻率的字段作為 shard key。
Shard Key Increasing Monotonically (單調增長)
單調增長在這里的意思是在數(shù)據被切分以后,新增加的數(shù)據會按照其 shard key 取值向 shard 中插入,如果新增的數(shù)據的 key 值都是向最大值方向增加,那么這些新的數(shù)據會被插入到同一個 shard 服務器上。例如我們前面的用戶年齡分組字段,如果系統(tǒng)的新增用戶都是年齡大于 40 歲的,那么 shard3 將會存儲所有的新增用戶,shard3 會成為系統(tǒng)的性能瓶頸。在這種情況下,應當考慮使用 Hashed Shard Key。
重新設計 Shard Key
通過上面的分析我們可以得出結論,前面例子中的用戶年齡字段是一個很糟糕的方案。有幾個原因:
用戶的年齡不是固定不變的,由于 shard key 是不可變字段,一旦確定下來以后不能進行修改,所以年齡字段顯然不是很合適,畢竟沒有年齡永遠不增長的用戶。一個系統(tǒng)的用戶在不同年齡階段的分布是不一樣的,對于像游戲、娛樂方面的應用可能會吸引年輕人多一些。而對于醫(yī)療、養(yǎng)生方面也許會有更多老年人關注。從這一點上說,這樣的切分也是不恰當?shù)摹_x擇年齡字段并沒有考慮到未來用戶增長方面帶來的問題,有可能在數(shù)據切分的時候年齡是均勻分布的,但是系統(tǒng)運行一段時間以后有可能出現(xiàn)不平等的數(shù)據分布,這點會給數(shù)據維護帶來很大的困擾。那么我們應當如何進行選擇呢?看一下用戶表的所有屬性可以發(fā)現(xiàn),其中有一個created_at字段,它指的是紀錄創(chuàng)建的時間戳。如果采用 Ranged Key 那么在數(shù)據增長方向上會出現(xiàn)單調增長問題,在分析一下發(fā)現(xiàn)這個字段重復的紀錄不多,他有很高的cardinality和非常低的頻率,這樣 Harded key 就成為了很好的備選方案。
分析完理論以后咱們實踐一下看看效果,不幸的是我們并不能修改 shard key,最好的方法就是備份數(shù)據,重新創(chuàng)建 shard 集群。創(chuàng)建和數(shù)據準備的過程我就不在重復了,你們可以根據前面的例子自己作一遍。
下圖中是我新建的一個userscollection,并以created_at為索引創(chuàng)建了 Hashed Shard Key,注意created_at必須是一個 hash index 才能成為 hashed shard key。下面是針對用戶表的一次查詢結果。
從圖中可以看到,explain 的結果表示了三個 shard 服務器基本上均勻分布了所有的數(shù)據,三個 shard 上執(zhí)行時間也都基本均勻,在 500 到 700 多毫秒以內。還記得上面的幾次查詢結果嗎?在數(shù)據比較多的 shard 上的運行時間在 1 到 2 毫秒??梢钥吹娇偟男阅艿玫搅孙@著提高。
選擇完美的 Shard Key
在 shard key 的選擇方面,我們需要考慮很多因素,有些是技術的,有些是業(yè)務層面的。通常來講應當注意下面幾點:
所有增刪改查語句都可以發(fā)送到集群中所有的 shard 服務器中任何操作只需要發(fā)送到與其相關的 shard 服務器中,例如一次刪除操作不應當發(fā)送到沒有包括要刪除的數(shù)據的 shard 服務器上權衡利弊,實際上沒有完美的 shard key,只有選擇 shard key 時應當注意和考慮的要素。不會出現(xiàn)一種 shard key 可以滿足所有的增刪改查操作。你需要從給你的應用場景中抽象出用來選擇 shard key 的元素,考量這些要素并作出最后選擇,例如:你的應用是處理讀操作多還是寫操作多?最常用的寫操作場景是什么樣子的?
小結
在 shard key 的選擇方面沒有一個統(tǒng)一的方法,要根據具體的需求和數(shù)據增長的方向來設計。在我們日常的開發(fā)過程中,并不是所有技術問題都應當由技術人員來解決,這個世界是一個業(yè)務驅動的時代,而技術主要是為業(yè)務服務,我們要提高對需求變化的相應速度。像本文中如何選擇 Shard Key 的問題,我覺得并不能單純的通過技術來考量,更多的是要和業(yè)務人員討論各個數(shù)據字段的意義,使用的業(yè)務價值以及未來業(yè)務的增長點。如果在一開始 shard key 的選擇出現(xiàn)錯誤,那么在接下來的應用過程中想要改變 shard key 是一件極其繁瑣的過程。可能你需要備份你的 collection,然后重新創(chuàng)建 shard 服務并恢復數(shù)據,這個過程很可能需要運行很長一段時間。在互聯(lián)網應用的今天,服務器的宕機事件都是以秒為單位計算,很可能錯誤的 shard key 選擇會給你的應用帶來災難性的后果。希望此文能給各位一點啟示,在項目初期的設計階段充分考慮到各方面的因素。
- 特朗普宣布200億美元投資計劃,在美國多地建設數(shù)據中心
- 工信部:“點、鏈、網、面”體系化推進算力網絡工作 持續(xù)提升算網綜合供給能力
- 2025年超融合基礎設施的4大趨勢
- 2025年將影響數(shù)據中心的5個云計算趨勢
- 80萬輛大眾汽車因AWS云配置錯誤導致數(shù)據泄露,包含“高精度”位置記錄
- 名創(chuàng)優(yōu)品超4000家門店接入“碰一下”支付,引爆年輕消費熱潮
- 免稅店也能用“碰一下”支付了!中免海南免稅店:碰一下就優(yōu)惠
- 報告:人工智能推動數(shù)據中心系統(tǒng)支出激增25%
- 密態(tài)計算技術助力農村普惠金融 螞蟻密算、網商銀行項目入選大數(shù)據“星河”案例
- 專利糾紛升級!Netflix就虛擬機專利侵權起訴博通及VMware
免責聲明:本網站內容主要來自原創(chuàng)、合作伙伴供稿和第三方自媒體作者投稿,凡在本網站出現(xiàn)的信息,均僅供參考。本網站將盡力確保所提供信息的準確性及可靠性,但不保證有關資料的準確性及可靠性,讀者在使用前請進一步核實,并對任何自主決定的行為負責。本網站對有關資料所引致的錯誤、不確或遺漏,概不負任何法律責任。任何單位或個人認為本網站中的網頁或鏈接內容可能涉嫌侵犯其知識產權或存在不實內容時,應及時向本網站提出書面權利通知或不實情況說明,并提供身份證明、權屬證明及詳細侵權或不實情況證明。本網站在收到上述法律文件后,將會依法盡快聯(lián)系相關文章源頭核實,溝通刪除相關內容或斷開相關鏈接。