資深架構(gòu)師技術(shù)分享:一文詳解分布式系統(tǒng)的分區(qū)
數(shù)據(jù)的復(fù)制是冗余的過程,冗余會增加可用性,并且可以有效均衡讀取負(fù)載。而數(shù)據(jù)的分區(qū)是一個整體轉(zhuǎn)換為局部的過程,這種拆解就像你擁有大量圖書,但你的書架放不下,所以需要再加幾個書架存儲是一個道理。
將整體拆分,局部存儲在多個較小空間內(nèi)。這種思想映射到計算機(jī)上也是一樣的,當(dāng)數(shù)據(jù)量過大,單個存儲節(jié)點(diǎn)不足與存儲這些數(shù)據(jù)(更大容量的磁盤沒有或者太貴)時,人們想要繼續(xù)存儲就需要將數(shù)據(jù)集拆解并規(guī)整。這就是數(shù)據(jù)分區(qū)的意義,它是用來提高數(shù)據(jù)系統(tǒng)的可擴(kuò)展性而引入的技術(shù)方法。

如何分區(qū)?
分區(qū)的關(guān)鍵在于采用一種統(tǒng)一的規(guī)則,這種規(guī)則可以計算出將數(shù)據(jù)放在哪個節(jié)點(diǎn),并且在讀取時也能計算出去哪個節(jié)點(diǎn)讀取數(shù)據(jù)。
要做到這幾點(diǎn)目前有三種分區(qū)方式:
- 按key的范圍進(jìn)行分區(qū) 當(dāng)要存儲數(shù)據(jù)時,我們?nèi)?shù)據(jù)中的某一個字段作為分區(qū)key,按這個字段的范圍進(jìn)行分區(qū)例如自增的id值,0-10000存儲在A節(jié)點(diǎn)上,10001-20000存儲在B節(jié)點(diǎn)上,那么基于這樣的規(guī)則我們可以高效的存取分區(qū)中的數(shù)據(jù),并且自然的支持按區(qū)間查找(key的存儲是有序的),只要區(qū)間的范圍僅在一個分區(qū)時,那么區(qū)間查找就只會訪問一個分區(qū),除非查找范圍跨越多個分區(qū)。但是問題在于當(dāng)數(shù)據(jù)的寫入在某段時間內(nèi)存在熱點(diǎn)時,例如0-100000的key被大量的寫入,而10001-20000的key很少的時候,就會造成 數(shù)據(jù)傾斜 (數(shù)據(jù)分區(qū)大小不均衡)
- 按key的散列進(jìn)行分區(qū) 對于數(shù)據(jù)傾斜,很自然的方式想到一個高效的散列函數(shù)來將數(shù)據(jù)存放在不同的分區(qū),只要散列函數(shù)一致,相同的key一定會被映射到同一個分區(qū)。所以也是能夠解決分區(qū)的關(guān)鍵問題,但是由于散列的問題,自然的進(jìn)行區(qū)間范圍查找會非常的困難,有些數(shù)據(jù)庫會將區(qū)間查找的請求發(fā)送給所有分區(qū),并行處理后,再全部聚合返回結(jié)果,但無疑會頻繁的產(chǎn)生大量的請求,雖然可以有效的解決數(shù)據(jù)傾斜問題,但是這種熱點(diǎn)數(shù)據(jù)是沒有辦法完全避免的,比如一個大V用戶總有非常多的粉絲,每天要產(chǎn)生非常多的數(shù)據(jù),通過key散列這些數(shù)據(jù)還是會存儲在相同的分區(qū)內(nèi),造成數(shù)據(jù)傾斜的同時,還會導(dǎo)致熱點(diǎn)數(shù)據(jù)的頻繁訪問,讀與寫負(fù)載都會分布不均勻。
- range+hash 模式 上述兩種分區(qū)的優(yōu)缺點(diǎn)恰好是互補(bǔ)的,那么可以考慮將二者結(jié)合例如用數(shù)據(jù)記錄的兩個關(guān)鍵字段作為key,比如是id與時間戳,先用id 散列存儲在不同的分區(qū)上,然后在使用時間戳按范圍進(jìn)行分區(qū),這樣做在一定程度上彌補(bǔ)了二者的優(yōu)缺點(diǎn)。 但依舊沒有完全解決熱點(diǎn)數(shù)據(jù)問題,這時熱點(diǎn)數(shù)據(jù)問題可以考慮其他方面來解決,比如建立熱點(diǎn)數(shù)據(jù)的緩存架構(gòu)。
分區(qū)方法看似完美的對數(shù)據(jù)的存儲進(jìn)行了擴(kuò)展,但也引入了另外的復(fù)雜度,那就是在查詢數(shù)據(jù)的時候,如果數(shù)據(jù)恰巧不在同一個分區(qū)內(nèi),就需要訪問多次不同的分區(qū)這樣就會加大請求的延遲,或者當(dāng)我們需要對關(guān)系模型中的數(shù)據(jù)進(jìn)行join操作時,由于數(shù)據(jù)在不同的分區(qū)中的不同表內(nèi),進(jìn)行join的難度就會非常大,增加了多表查詢的復(fù)雜程度,一種折中的解決方案是,從業(yè)務(wù)上來看,將會被join或者同時讀取的數(shù)據(jù)盡量放在同一個分區(qū)上,來減少跨分區(qū)調(diào)用的性能損耗,這就相當(dāng)于降低磁盤尋址尋道的次數(shù)是一樣的道理,都是在降低最耗時操作的發(fā)生次數(shù)。
二級索引的分區(qū)如何設(shè)計?
上述的三種分區(qū)方案,僅僅是對主鍵的分區(qū),也就是一條記錄的唯一標(biāo)識進(jìn)行分區(qū),但從數(shù)據(jù)庫功能的角度來看,我們還需要可以根據(jù)一條記錄的任意字段建立索引,以便靈活高效的查詢.這樣的索引,就稱之為二級索引。那么二級索引在分區(qū)數(shù)據(jù)庫的設(shè)計上應(yīng)該如何實現(xiàn)呢?通常有兩種設(shè)計,本地索引與全局索引。
本地索引
當(dāng)寫入與讀取二級索引時都在本分區(qū)上進(jìn)行時,我們就說這樣的二級索引為本地索引,也就是說每個分區(qū)上的二級索引文件僅存儲本分區(qū)上的索引數(shù)據(jù)。這樣做的好處是,在寫入數(shù)據(jù)時更新一條記錄的二級索引會很方便,因為關(guān)于本記錄的所有二級索引都在這個節(jié)點(diǎn)上.但是以二級索引讀取某條記錄時,我們沒辦法知道記錄在哪個分區(qū),因此我們需要進(jìn)行并行查詢?nèi)缓髮⒉樵兘Y(jié)果進(jìn)行合并,這樣做無疑放大了讀取的延遲。
全局索引
與之相對的是全局索引,即對于某個二級索引,其全部的字段都在同一個分區(qū)之中(不同的全局索引在不同的分區(qū)上),當(dāng)我們查詢某個二級索引時,我們可以只去唯一的一個節(jié)點(diǎn)上進(jìn)行讀取數(shù)據(jù)即可,不需要并行查詢,這樣讀取的效率會很高,但是在寫入數(shù)據(jù)的時候,由于一條記錄涉及的二級索引可能在多個分區(qū)上,所以需要操作多個分區(qū)這就涉及到分布式的事務(wù)一致性等問題,復(fù)雜度大大增加并影響性能。全局索引在讀取數(shù)據(jù)時,如何找到索引所在分區(qū)呢?答案是,對于全局的二級索引我們可以對其采用相同的分區(qū)策略,范圍分區(qū),散列分區(qū)或者散列范圍分區(qū)等. 不同的分區(qū)策略同樣會影響其對區(qū)間查詢的效率。
分區(qū)再平衡
多個節(jié)點(diǎn)上擁有多個分區(qū),當(dāng)隨著數(shù)據(jù)負(fù)載的增加,每個分區(qū)的大小就會不斷的增加,這樣就造成了隱患,一旦一個節(jié)點(diǎn)失效,其上分區(qū)都將失效,占比很大的一部分?jǐn)?shù)據(jù)都將失效,再比如現(xiàn)在向集群中加入或剔除一個新的節(jié)點(diǎn),那么數(shù)據(jù)需要可以被均勻的轉(zhuǎn)移到新的節(jié)點(diǎn)上(新節(jié)點(diǎn)不轉(zhuǎn)移數(shù)據(jù),而是接受新的寫請求是否可行?這樣做會使在一段時間內(nèi),寫入請求不能均衡的請求不同的節(jié)點(diǎn),大量的請求新節(jié)點(diǎn)會使其負(fù)載不平衡),上述問題都概括起來就是引入分區(qū)再平衡特性的原因,即為了可用性與擴(kuò)展性,分區(qū)再平衡都是必不可少的特性。
固定數(shù)量的分區(qū)
分區(qū)數(shù)與節(jié)點(diǎn)數(shù)應(yīng)當(dāng)不同,這樣做是為了方便其擴(kuò)展。理由是:假設(shè)分區(qū)數(shù)與節(jié)點(diǎn)數(shù)相同,那么通過對節(jié)點(diǎn)數(shù)取模來決定數(shù)據(jù)被分配到哪個分區(qū)上,這種取模會造成隱患.當(dāng)我們添加或者刪除一個節(jié)點(diǎn)時,取模的數(shù)發(fā)生變化,之前的數(shù)據(jù)不能被路由到正確的分區(qū),所以必須進(jìn)行再平衡對,所有數(shù)據(jù)重新分區(qū)(類似,hashmap 的再哈希),這會導(dǎo)致所有數(shù)據(jù)都處于遷移的狀態(tài),整個集群將不可用。
因此,我們必須將節(jié)點(diǎn)數(shù)與分區(qū)數(shù)進(jìn)行解耦合,在一個節(jié)點(diǎn)上分配固定數(shù)量的分區(qū)數(shù),例如在集群初始化時指定一個有1024個分區(qū),現(xiàn)在有三個節(jié)點(diǎn),那么每個節(jié)點(diǎn)應(yīng)該擁有341個分區(qū),最后一個節(jié)點(diǎn)可能擁有342個分區(qū),這時添加一個節(jié)點(diǎn),集群擁有4個節(jié)點(diǎn)后,我們需要對其進(jìn)行分區(qū)再平衡,僅需要將原來的三個節(jié)點(diǎn)上的分區(qū)各取一半即可,這樣就僅僅有一半的數(shù)據(jù)在遷移的過程中(比例經(jīng)過復(fù)雜的算法可以動態(tài)調(diào)整),就可以降低分區(qū)再平衡過程中的復(fù)雜度了。節(jié)點(diǎn)刪除也是同樣的道理,將該節(jié)點(diǎn)上的分區(qū)平均的分散在其他節(jié)點(diǎn)上即可,固定數(shù)量的分區(qū)方案解決了節(jié)點(diǎn)數(shù)與分區(qū)數(shù)的耦合,我們對分區(qū)數(shù)進(jìn)行取模即可很快的確定數(shù)據(jù)所在分區(qū),并且在遷移前后相對分區(qū)保持不變,redis的集群模式就是采用這種方式進(jìn)行的分區(qū)再平衡。
動態(tài)數(shù)量分區(qū)
固定數(shù)量分區(qū)不能很好的應(yīng)對熱點(diǎn)問題,當(dāng)一個分區(qū)存儲的數(shù)據(jù)量遠(yuǎn)多于其他節(jié)點(diǎn)時,這是不合理的。由于節(jié)點(diǎn)數(shù)量固定,這些數(shù)據(jù)無法遷移,因此引入類似B+樹節(jié)點(diǎn)分裂與合并的概念,我們對分區(qū)也可以根據(jù)其數(shù)據(jù)量的多少進(jìn)行分裂與合并,當(dāng)某個分區(qū)負(fù)載高于一個指定的閾值時,我們就會對其進(jìn)行分裂,變成兩個分區(qū),這樣分區(qū)的數(shù)量就發(fā)生了變化,此種方案就不能使用分區(qū)數(shù)取模的方式進(jìn)行數(shù)據(jù)的散列了,僅能根據(jù)關(guān)鍵字區(qū)間或者哈希進(jìn)行分區(qū)。但這是值得的,他可以有效的平衡各個分區(qū)的數(shù)據(jù)負(fù)載。
按節(jié)點(diǎn)比例進(jìn)行分區(qū)
動態(tài)分區(qū)同樣擁有一個弊端,那就是其分區(qū)的數(shù)量與數(shù)據(jù)量成正比,數(shù)據(jù)量的增加會不斷的增加新的分區(qū),分區(qū)數(shù)量的變多將會成為新的性能瓶頸。
因此,引入一種新的方案,結(jié)合上述兩種方案,當(dāng)節(jié)點(diǎn)數(shù)固定不變時,分區(qū)數(shù)也是固定不變的,每個節(jié)點(diǎn)上的分區(qū)數(shù)永遠(yuǎn)是固定數(shù)量的,這樣當(dāng)節(jié)點(diǎn)數(shù)不變時,隨著數(shù)據(jù)負(fù)載的增加,其分區(qū)的大小也會不斷變大,當(dāng)有新的節(jié)點(diǎn)加入或者剔除時,會隨機(jī)(可以有某些復(fù)雜的策略)選擇一些節(jié)點(diǎn)上的分區(qū)進(jìn)行分裂,一分為二的分區(qū),一半被移動到新的節(jié)點(diǎn),另一半留在原地,這樣做的好處是分區(qū)的數(shù)量被節(jié)點(diǎn)數(shù)所限定了,不會無限的增長成為瓶頸。
手動與自動的再平衡
是否應(yīng)該有數(shù)據(jù)系統(tǒng)自動的完成分區(qū)再平衡?這樣做無疑是方便的,也有很多數(shù)據(jù)庫采用,但其復(fù)雜度確是非常之高的,例如,當(dāng)發(fā)生節(jié)點(diǎn)分區(qū)平衡時,被系統(tǒng)檢測到節(jié)點(diǎn)不可用時,那么就會造成級聯(lián)崩潰的情況,觸發(fā)剔除節(jié)點(diǎn)邏輯,又會觸發(fā)新的分區(qū)平衡致使整個集群崩潰。
基于簡單性的原則,有管理員手動的分區(qū)再平衡是一種折中的選擇。
請求路由
引入復(fù)雜的分區(qū)方案后,客戶端如何知道請求的數(shù)據(jù)在哪個分區(qū)上?一般有三種方式:
- 隨機(jī)的請求一個節(jié)點(diǎn),該分區(qū)會判斷數(shù)據(jù)是否在自身上,是則直接返回結(jié)果,不是則轉(zhuǎn)發(fā)請求到擁有數(shù)據(jù)的節(jié)點(diǎn),并返回結(jié)果。
- 所有的請求都訪問一個路由層,路由層擁有足夠的元數(shù)據(jù)進(jìn)行決策,將請求轉(zhuǎn)發(fā)到合適的節(jié)點(diǎn)上。
- 客戶端本身可以感知到分區(qū)節(jié)點(diǎn)的分配關(guān)系,直接請求相應(yīng)分區(qū)。
無論哪種方式,只不過是路由決策的邏輯交由誰來完成的問題。
對于客戶端的請求路由,需要讓客戶端感知到分區(qū)與節(jié)點(diǎn)的映射關(guān)系的變化,通常基于分布式的一致性共識組件完成,例如zk,etcd等。當(dāng)分區(qū)節(jié)點(diǎn)啟動時向zk注冊自己的元數(shù)據(jù),然后zk會將信息傳播到訂閱了此變化的客戶端上,客戶端更新分區(qū)與節(jié)點(diǎn)的映射關(guān)系,當(dāng)有請求時直接訪問對應(yīng)的分區(qū)即可。其優(yōu)點(diǎn)在于這樣網(wǎng)絡(luò)調(diào)用次數(shù)最少最高效,但依賴第三方一致性共識組件。
另一種不同的做法是,讓客戶端請求任意節(jié)點(diǎn),分區(qū)節(jié)點(diǎn)根據(jù)自身持有的元數(shù)據(jù)信息判斷請求的數(shù)據(jù)是否在自己的分區(qū),在則直接處理并返回,不在則將請求轉(zhuǎn)發(fā)給擁有分區(qū)的請求進(jìn)行處理并返回給客戶端。這樣做的優(yōu)點(diǎn)在于不依賴共識組件,但最壞情況下網(wǎng)絡(luò)的調(diào)用次數(shù)翻倍,影響性能。
并行查詢處理
當(dāng)需要對多個字段進(jìn)行聯(lián)合查詢時,分區(qū)數(shù)據(jù)庫應(yīng)該怎么做,我們一直探討如何通過單獨(dú)的key進(jìn)行查詢,但是對于大量的針對數(shù)據(jù)倉庫的查詢,更多的是復(fù)雜的join等多表操作.分區(qū)數(shù)據(jù)庫能表現(xiàn)的很好嗎?這就涉及到分布式數(shù)據(jù)庫的分區(qū)并行查詢的問題,理論上當(dāng)請求到達(dá)路由層后,由路由層中并行查詢優(yōu)化器等組件制定并行查詢計劃,并委派給對應(yīng)的分區(qū),并把結(jié)果做最后的合并。這個過程中的細(xì)節(jié)非常之多,我將在之后的文章中詳細(xì)介紹。