我們知道分布式數(shù)據(jù)庫是將數(shù)據(jù)打散到不同節(jié)點上存儲,從而提升性能與可靠性。那么今天我們來看看在分布式數(shù)據(jù)庫中,一個非常重要的設(shè)計:正確地把數(shù)據(jù)分片,充分發(fā)揮分布式數(shù)據(jù)庫架構(gòu)的優(yōu)勢。
選出分片鍵
在對表中的數(shù)據(jù)進行分片時,首先要選出一個分片鍵(Shard Key),即用戶可以通過這個字段進行數(shù)據(jù)的水平拆分。
對于我們之前使用的電商業(yè)務的訂單表orders,其表結(jié)構(gòu)如下所示:
CREATE TABLE `orders` ( `O_ORDERKEY` int NOT NULL, `O_CUSTKEY` int NOT NULL, `O_ORDERSTATUS` char(1) NOT NULL, `O_TOTALPRICE` decimal(15,2) NOT NULL, `O_ORDERDATE` date NOT NULL, `O_ORDERPRIORITY` char(15) NOT NULL, `O_CLERK` char(15) NOT NULL, `O_SHIPPRIORITY` int NOT NULL, `O_COMMENT` varchar(79) NOT NULL, PRIMARY KEY (`O_ORDERKEY`), KEY `idx_custkey_orderdate` (`O_CUSTKEY`,`O_ORDERDATE`), KEY `ORDERS_FK1` (`O_CUSTKEY`), KEY `idx_custkey_orderdate_totalprice` (`O_CUSTKEY`,`O_ORDERDATE`,`O_TOTALPRICE`), KEY `idx_orderdate` (`O_ORDERDATE`), KEY `idx_orderstatus` (`O_ORDERSTATUS`), CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`O_CUSTKEY`) REFERENCES `customer` (`C_CUSTKEY`)) ENGINE=InnoDB
對于類似淘寶、京東、拼多多這樣業(yè)務體量的應用來說,單實例 MySQL 數(shù)據(jù)庫在性能和存儲容量上肯定無法滿足“雙 11、618 ”大促的要求,所以要改造成分布式數(shù)據(jù)庫架構(gòu)。
而第一步就是要對表選出一個分片鍵,然后進行分布式架構(gòu)的設(shè)計。
對于上面的表orders,可以選擇的分片鍵有:o_orderkey、o_orderdate、也可以是o_custkey。在選出分片鍵后,就要選擇分片的算法,比較常見的有 RANGE 和 HASH 算法。
JAVA日知錄寫代碼的架構(gòu)師,做架構(gòu)的程序員! 實戰(zhàn)、源碼、數(shù)據(jù)庫、架構(gòu)…只要你來,你想了解的這里都有!203篇原創(chuàng)內(nèi)容
公眾號
比如,表 orders,選擇分片鍵 o_orderdate,根據(jù)函數(shù) YEAR 求出訂單年份,然后根據(jù)RANGE 算法進行分片,這樣就能設(shè)計出基于 RANGE 分片算法的分布式數(shù)據(jù)庫架構(gòu):
從圖中我們可以看到,采用 RANGE 算法進行分片后,表 orders 中,1992 年的訂單數(shù)據(jù)存放在分片 1 中、1993 年的訂單數(shù)據(jù)存放在分片 2 中、1994 年的訂單數(shù)據(jù)存放在分片 3中,依次類推,如果要存放新年份的訂單數(shù)據(jù),追加新的分片即可。
不過,RANGE 分片算法在分布式數(shù)據(jù)庫架構(gòu)中,是一種非常糟糕的算法,因為對于分布式架構(gòu),通常希望能解決傳統(tǒng)單實例數(shù)據(jù)庫兩個痛點:
- 性能可擴展,通過增加分片節(jié)點,性能可以線性提升;
- 存儲容量可擴展,通過增加分片節(jié)點,解決單點存儲容量的數(shù)據(jù)瓶頸。
那么對于訂單表 orders 的 RANGE 分片算法來說,你會發(fā)現(xiàn)以上兩點都無法實現(xiàn),因為當年的數(shù)據(jù)依然存儲在一個分片上(即熱點還是存在于一個數(shù)據(jù)節(jié)點上)。
如果繼續(xù)拆細呢?比如根據(jù)每天進行 RANGE 分片?這樣的確會好一些,但是對“雙 11、618”這樣的大促來說,依然是單分片在工作,熱點依然異常集中。
所以在分布式架構(gòu)中,RANGE 分區(qū)算法是一種比較糟糕的算法。但它也有好處:可以方便數(shù)據(jù)在不同機器間進行遷移(migrate),比如要把分片 2 中 1992 年的數(shù)據(jù)遷移到分片 1,直接將表進行遷移就行。
而對海量并發(fā)的 OLTP 業(yè)務來說,一般推薦用 HASH 的分區(qū)算法。這樣分片的每個節(jié)點都可以有實時的訪問,每個節(jié)點負載都能相對平衡,從而實現(xiàn)性能和存儲層的線性可擴展。
我們來看表 orders 根據(jù) o_orderkey 進行 HASH 分片,分片算法如下:
在上述分片算法中,分片鍵是 o_orderkey,總的分片數(shù)量是 4(即把原來 1 份數(shù)據(jù)打散到 4 張表中),具體來講,分片算法是將 o_orderkey 除以 4 進行取模操作。
最終,將表orders 根據(jù) HASH 算法進行分布式設(shè)計后的結(jié)果如下圖所示:
可以看到,對于訂單號除以 4,余數(shù)為 0 的數(shù)據(jù)存放在分片 1 中,余數(shù)為 1 的數(shù)據(jù)存放在分片 2 中,余數(shù)為 2 的數(shù)據(jù)存放在分片 3 中,以此類推。
這種基于 HASH 算法的分片設(shè)計才能較好地應用于大型互聯(lián)網(wǎng)業(yè)務,真正做到分布式數(shù)據(jù)庫架構(gòu)彈性可擴展的設(shè)計要求。
但是,表 orders 分區(qū)鍵選擇 o_orderkey 是最好地選擇嗎?并不是。
我們看一下庫中的其他表,如表 customer、lineitem,這三張表應該是經(jīng)常一起使用的,比如查詢用戶最近的訂單明細。
如果用 o_orderkey 作分區(qū)鍵,那么 lineitem 可以用 l_orderkey 作為分區(qū)鍵,但這時會發(fā)現(xiàn)表customer 并沒有訂單的相關(guān)信息,即無法使用訂單作為分片鍵。
如果表 customer 選擇另一個字段作為分片鍵,那么業(yè)務數(shù)據(jù)無法做到單元化,也就是對于表customer、orders、lineitem,分片數(shù)據(jù)在同一數(shù)據(jù)庫實例上。
所以,如果要實現(xiàn)分片數(shù)據(jù)的單元化,最好的選擇是把用戶字段作為分區(qū)鍵,在表 customer 中就是將 c_custkey 作為分片鍵,表orders 中將 o_custkey 作為分片鍵,表 lineitem 中將 l_custkey 作為分片鍵:
這樣做的好處是:根據(jù)用戶維度進行查詢時,可以在單個分片上完成所有的操作,不用涉及跨分片的訪問,如下面的 SQL:
SELECT * FROM ordersINNER JOIN lineitem ON o_orderkey = l_orderkeyINNER JOIN customer ON o_custkey = c_custkeyWHERE o_custkey = 1ORDER BY o_orderdate DESC LIMIT 10
所以,分布式數(shù)據(jù)庫架構(gòu)設(shè)計的原則是:選擇一個適合的分片鍵和分片算法,把數(shù)據(jù)打散,并且業(yè)務的絕大部分查詢都是根據(jù)分片鍵進行訪問。
那為什么互聯(lián)網(wǎng)業(yè)務這么適合進行分布式架構(gòu)的設(shè)計呢?因為互聯(lián)網(wǎng)業(yè)務大部分是 To C 業(yè)務,分片鍵就是用戶的 ID,業(yè)務的大部分訪問都是根據(jù)用戶 ID 進行查詢,比如:
- 查看某個用戶下的微博/短視頻;
- 查看某個用戶的商品信息/購買記錄;
- 查看某個用戶自己的余額信息。
學完分片鍵的選擇后,接著就是規(guī)劃分片,也就我們經(jīng)常提到的分庫分表。
分庫分表
說了這么久分片,分片到底是什么呢?其實,分片本質(zhì)是一張張表,而不是數(shù)據(jù)庫實例,只是每個分片是在 MySQL 數(shù)據(jù)庫實例中,嚴格來說:
分片 = 實例 + 庫 + 表 = ip@port:db_name:table_name
對于前面的表orders,假設(shè)根據(jù) HASH 算法進行分片,那么可以進行如下的分庫分表設(shè)計:
在這 4 種分庫分表規(guī)則中,最推薦的是第 4 種,也是我們通常意義說的分庫分表,這樣做的好處有以下幾點:
- 不同分片的數(shù)據(jù)可以在同一 MySQL 數(shù)據(jù)庫實例上,便于做容量的規(guī)劃和后期的擴展;
- 同一分片鍵的表都在同一庫下,方便做整體數(shù)據(jù)的遷移和擴容。
如果根據(jù)第 4 種標準的分庫分表規(guī)范,那么分布式 MySQL 數(shù)據(jù)庫的架構(gòu)可以是這樣:
有沒有發(fā)現(xiàn),按上面這樣的分布式設(shè)計,數(shù)據(jù)分片完成后,所有的庫表依然是在同一個 MySQL實例上!??!
牢記,分布式數(shù)據(jù)庫并不一定要求有很多個實例,最基本的要求是將數(shù)據(jù)進行打散分片。接著,用戶可以根據(jù)自己的需要,進行擴縮容,以此實現(xiàn)數(shù)據(jù)庫性能和容量的伸縮性。這才是分布式數(shù)據(jù)庫真正的魅力所在。
對于上述的分布式數(shù)據(jù)庫架構(gòu),一開始我們將 4 個分片數(shù)據(jù)存儲在一個 MySQL 實例上,但是如果遇到一些大促活動,可以對其進行擴容,比如把 4 個分片擴容到 4 個MySQL實例上:
如果完成了大促活動,又可以對資源進行回收,將分片又都放到一臺 MySQL 實例上,這就是對資源進行縮容。
總的來說,對分布式數(shù)據(jù)庫進行擴縮容在互聯(lián)網(wǎng)公司是一件常見的操作,比如對阿里來說,每年下半年 7 月開始,他們就要進行雙 11 活動的容量評估,然后根據(jù)評估結(jié)果規(guī)劃數(shù)據(jù)庫的擴容。
一般來說,電商的雙 11 活動后,還有雙 12、新年、春節(jié),所以一般會持續(xù)到過完年再對數(shù)據(jù)庫進行縮容。接下來,我們來看看如何進行擴縮容。
擴縮容
在 HASH 分片的例子中,我們把數(shù)據(jù)分片到了 4 個節(jié)點,然而在生產(chǎn)環(huán)境中,為了方便之后的擴縮容操作,推薦一開始就把分片的數(shù)量設(shè)置為不少于 1000 個。
不用擔心分片數(shù)量太多,因為分片 1 個還是 1000 個,管理方式都是一樣的,但是 1000 個,意味著可以擴容到 1000 個實例上,對于一般業(yè)務來說,1000 個實例足夠滿足業(yè)務的需求了(BTW,網(wǎng)傳阿里某核心業(yè)務的分布式數(shù)據(jù)庫分片數(shù)量為 10000個)。
如果到了 1000 個分片依然無法滿足業(yè)務的需求,這時能不能拆成 2000 個分片呢?從理論上來說是可以的,但是這意味著需要對一張表中的數(shù)據(jù)進行邏輯拆分,這個工作非常復雜,通常不推薦。
所以,一開始一定要設(shè)計足夠多的分片。在實際工作中,我遇到很多次業(yè)務將分片數(shù)量從 32、64 拆成 256、512。每次這樣的工作,都是扒一層皮,太不值得。所以,做好分布式數(shù)據(jù)庫設(shè)計的工作有多重要!
那么擴容在 MySQL 數(shù)據(jù)庫中如何操作呢?其實,本質(zhì)是搭建一個復制架構(gòu),然后通過設(shè)置過濾復制,僅回放分片所在的數(shù)據(jù)庫就行,這個數(shù)據(jù)庫配置在從服務器上大致進行如下配置:
# 分片1從服務器配置replicate_do_db =”tpch01″
所以在進行擴容時,首先根據(jù)下圖的方式對擴容的分片進行過濾復制的配置:
然后再找一個業(yè)務低峰期,將業(yè)務的請求轉(zhuǎn)向新的分片,完成最終的擴容操作:
至于縮容操作,本質(zhì)就是擴容操作的逆操作,這里就不再多說了。
總結(jié)
今天這一講,我們學習了分布式數(shù)據(jù)庫架構(gòu)設(shè)計中的分片設(shè)計,也就是我們經(jīng)常聽說的分庫分表設(shè)計。希望通過本講,你能牢牢掌握以下內(nèi)容:
- 分布式數(shù)據(jù)庫數(shù)據(jù)分片要先選擇一個或多個字段作為分片鍵;
- 分片鍵的要求是業(yè)務經(jīng)常訪問的字段,且業(yè)務之間的表大多能根據(jù)這個分片鍵進行單元化;
- 如果選不出分片鍵,業(yè)務就無法進行分布式數(shù)據(jù)庫的改造;
- 選擇完分片鍵后,就要選擇分片算法,通常是 RANGE 或 HASH 算法;
- 海量 OLTP 業(yè)務推薦使用 HASH 算法,強烈不推薦使用 RANGE 算法;
- 分片鍵和分片算法選擇完后,就要進行分庫分表設(shè)計,推薦不同庫名表名的設(shè)計,這樣能方便后續(xù)對分片數(shù)據(jù)進行擴縮容;
- 實際進行擴容時,可以使用過濾復制,僅復制需要的分片數(shù)據(jù)。