App下載

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

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

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

幾乎每一個分布式系統(tǒng),都會給用戶提供自定義路由的功能。因為,僅通過rangemod、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ù)具有以下特點:

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

配號系統(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-1mysql-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ī)則使用。

8個庫

通過三次改進,我們的路由滿足:

一、 當我們發(fā)現(xiàn),當商戶 id 增長到100 056400,就達到瓶頸了,那么就可以新增一個新的范圍,只需要改動一下路由表邏輯就ok了

二、 當某個范圍內(nèi)某個商戶成長為 vip ,那我們就可以單獨將其提取出來,增加新的 vip 庫

三、 某個范圍內(nèi)數(shù)據(jù)熱點嚴重,那么就可以 mod 4 進行擴容,并不影響范圍外的數(shù)據(jù)

四、 商戶 id 同時也有時間緯度的概念,可以針對某些舊商戶進行歸檔清理

切分需求四階段

系統(tǒng)想要預(yù)留另外一部分號段,用來提供一些測試賬號,供客戶試用。經(jīng)歷過前三輪的改造,我們可以很容易的對其進行規(guī)劃。

End

為什么覺得redis-clusterslot設(shè)計是個雞肋呢,因為它把路由規(guī)則給定死了,要我去設(shè)計我肯定要放在驅(qū)動層。

某些架構(gòu)師瀟灑的來,瀟灑的走,留下了不可磨滅的痕跡。為了兼容這些遺留系統(tǒng)的路由代碼,分支會更加復雜,每一個公司都有一堆故事,無非是罵娘和被罵。穩(wěn)定性重如山,路由代碼可能是最重要的沒技術(shù)含量的if else。一動,都得死。

就問你怕不怕?

以上就是W3Cschool編程獅關(guān)于現(xiàn)實中的路由規(guī)則,可能比你想象中復雜的多的相關(guān)介紹了,希望對大家有所幫助。

0 人點贊