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