文章轉(zhuǎn)載自公眾號:小姐姐味道
幾乎每一個分布式系統(tǒng),都會給用戶提供自定義路由的功能。因為,僅通過range
、mod
、hash
等方法,很大概率已經(jīng)滿足不了用戶的需求。下面以一個實際場景為例,說一下數(shù)據(jù)路由的思路。
文中聊的是數(shù)據(jù)路由,不是nginx之類的。
場景
某個大型toB
的應(yīng)用,使用 MySQL 存儲,單表數(shù)據(jù)量已達數(shù)億,在結(jié)構(gòu)變更、數(shù)據(jù)查詢方面,已表現(xiàn)出明顯的瓶頸,需要進行分庫分表。
實施步驟
找到切分鍵
第一步就是找到切分的緯度。比如業(yè)務(wù)是按照時間緯度進行查詢的,那么就把創(chuàng)建時間作為切分鍵。
此業(yè)務(wù)的切分鍵,是商戶 id (類似于你在美團開店了,美團給你分配的唯一 id )。由于歷史原因,這個 id 是用的數(shù)據(jù)庫主鍵 id ,而且是自增的。業(yè)務(wù)具有以下特點:
- 業(yè)務(wù)操作是由某個商戶發(fā)起的,每張表都有商戶 id 字段
- 商戶的數(shù)據(jù)不均衡,有的商戶有幾千萬,有的可能只有十幾條
- 存在部分 vip 商家,其數(shù)據(jù)量非常龐大
- 存儲大量統(tǒng)計需求,所以無法分表,只能分庫
- 存在遍歷數(shù)據(jù)的可能,比如部分定時
切分需求一階段
分庫迫在眉睫。通過分析,部分 vip 商戶,數(shù)據(jù)量巨大,把它單獨轉(zhuǎn)移到一個數(shù)據(jù)庫中也不為過。
通過維護一個映射文件,來控制 vip 商戶到數(shù)據(jù)存儲流向。這時候,就需要自定義路由。
偽代碼如下:
function viptable(id){
10 => "mysql-002"
101 => "mysql-003"
}
function router4vip(id){
aimDb = viptable(id)
if(aimDb) return aimDb
return "mysql-001"
}
商戶為 10,數(shù)據(jù)將落向mysql-002
;商戶為 101,將落向mysql-003
;數(shù)據(jù)默認使用mysql-001
存儲。
另外,由于 id 是自動生成的自增字段,與路由存在一個先有雞還是先有蛋的問題,所以將 id 字段修改為人工設(shè)值,延伸出另外一個配號系統(tǒng),在此不多提。
切分需求二階段
解決了 vip 商戶的問題,接下來就需要解決mysql-001
的問題。隨著業(yè)務(wù)的發(fā)展,落在默認庫上的數(shù)據(jù)越來越多,很快又遇到了瓶頸。
想到的方法是,對其一分為二。mysql-001
的數(shù)據(jù)打散到兩個庫中。這個打散的規(guī)則,我們直接采用mod。
為什么不是一拆為三呢?主要是基于以下考慮,假設(shè)拆分后的 db 為:
mysql-001-1
mysql-001-2
這種情況下mysql-001
就變成了邏輯集群。當mysql-001-1
和mysql-001-2
也達到了瓶頸,那我們就可以對其繼續(xù)進行拆分,依然是一拆為二,這時候,mod 4
就可以了,不會涉及復雜的數(shù)據(jù)遷移。
拆分后的db為:
mysql-001-1-1
mysql-001-1-2
mysql-001-2-1
mysql-001-2-2
到現(xiàn)在為止,我們采用了 vip 分庫,mod 4
分庫,偽代碼如下:
...
function routertable(pivot){
0 => "mysql-001-1-1"
1 => "mysql-001-1-2"
2 => "mysql-001-2-1"
3 => "mysql-001-2-2"
}
function router4mod(id){
aimDb = router4vip(id)
if(aimDb) return aimDb
pivot = mod4(id)
return routertable(pivot)
}
到現(xiàn)在,我們已經(jīng)分了六個庫了。通過裂變的模式,有著較好的擴展性。
這樣就可以高枕無憂了么?
切分需求三階段
可惜的是,我們每次擴容,都是指數(shù)級別的。下一次,就是 mod 8
;而下下次,就是mod 16
。每次擴容,都會動一半的數(shù)據(jù),wtf。
最后,決定在商戶 id 的范圍上做文章。
首先,做一個定長的商戶 id ,比現(xiàn)有系統(tǒng)中的任何一個都長,主要考慮新的規(guī)則不會影響舊的路由規(guī)則。
然后,首先根據(jù)商戶 id 的范圍劃分第一層虛擬集群,然后再根據(jù) mod
劃分第二層虛擬集群。我們的路由,現(xiàn)在是雙層路由。
比如,我們把商戶號定9位(java中int是10位),并做如下路由表:
100 000000 - 100 100000=> 虛擬集群1
100 100000 - 100 200000=> 虛擬集群2
...
前三位,用來分第一層虛擬集群,支持899個;后6位,代表范圍,最大10萬。每個范圍下面,都會有自己的路由規(guī)則,有的可能 mod 2
,有的可能 mod 3
,有的可能再次 range
。
好,我們加入新的集群:
mysql-range0-0 代表號段在范圍1中的偶數(shù)id
mysql-range0-1
偽代碼如下:
...
function router4range(id){
if(id < 100000000){
return router4mod(id)
}else if
(id in [100000000-100100000]){
return
"mysql-range0-"+mod2(id)
}
}
到此為止,我們一共有8個庫,其中兩個是給 vip 用的,四個是遺留的路由算法,還有兩個是給新的分庫規(guī)則使用。
通過三次改進,我們的路由滿足:
一、 當我們發(fā)現(xiàn),當商戶 id 增長到100 056400
,就達到瓶頸了,那么就可以新增一個新的范圍,只需要改動一下路由表邏輯就ok了
二、 當某個范圍內(nèi)某個商戶成長為 vip ,那我們就可以單獨將其提取出來,增加新的 vip 庫
三、 某個范圍內(nèi)數(shù)據(jù)熱點嚴重,那么就可以 mod 4
進行擴容,并不影響范圍外的數(shù)據(jù)
四、 商戶 id 同時也有時間緯度的概念,可以針對某些舊商戶進行歸檔清理
切分需求四階段
系統(tǒng)想要預留另外一部分號段,用來提供一些測試賬號,供客戶試用。經(jīng)歷過前三輪的改造,我們可以很容易的對其進行規(guī)劃。
End
為什么覺得redis-cluster
的slot
設(shè)計是個雞肋呢,因為它把路由規(guī)則給定死了,要我去設(shè)計我肯定要放在驅(qū)動層。
某些架構(gòu)師瀟灑的來,瀟灑的走,留下了不可磨滅的痕跡。為了兼容這些遺留系統(tǒng)的路由代碼,分支會更加復雜,每一個公司都有一堆故事,無非是罵娘和被罵。穩(wěn)定性重如山,路由代碼可能是最重要的沒技術(shù)含量的if else
。一動,都得死。
就問你怕不怕?
以上就是W3Cschool編程獅
關(guān)于現(xiàn)實中的路由規(guī)則,可能比你想象中復雜的多的相關(guān)介紹了,希望對大家有所幫助。