App下載

現(xiàn)實(shí)中的路由規(guī)則,可能比你想象中復(fù)雜的多

猿友 2020-08-31 14:11:50 瀏覽數(shù) (3883)
反饋

文章轉(zhuǎn)載自公眾號(hào):小姐姐味道

幾乎每一個(gè)分布式系統(tǒng),都會(huì)給用戶提供自定義路由的功能。因?yàn)椋瑑H通過range、modhash等方法,很大概率已經(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):

  1. 業(yè)務(wù)操作是由某個(gè)商戶發(fā)起的,每張表都有商戶 id 字段
  2. 商戶的數(shù)據(jù)不均衡,有的商戶有幾千萬,有的可能只有十幾條
  3. 存在部分 vip 商家,其數(shù)據(jù)量非常龐大
  4. 存儲(chǔ)大量統(tǒng)計(jì)需求,所以無法分表,只能分庫
  5. 存在遍歷數(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),在此不多提。

配號(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-1mysql-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ò)展性。

這樣就可以高枕無憂了么?

六個(gè)庫

切分需求三階段

可惜的是,我們每次擴(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ī)則使用。

8個(gè)庫

通過三次改進(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-clusterslot設(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ì)大家有所幫助。

0 人點(diǎn)贊