1. 程式人生 > >[架構師之路] 資料庫拆分 系列(上)

[架構師之路] 資料庫拆分 系列(上)

一分鐘掌握資料庫垂直拆分 一、緣起 當資料庫的資料量非常大時,水平切分和垂直拆分是兩種常見的降低資料庫大小,提升效能的方法。假設有使用者表:
user(     uid bigint,     name varchar(16),     pass varchar(16),     age int,     sex tinyint,     flag tinyint,     sign varchar(64),     intro varchar(256) …);
    水平切分是指,以某個欄位為依據(例如uid),按照一定規則(例如取模),將一個庫(表)上的資料拆分到多個庫(表)上,以降低單庫(表)大小,達到提升效能的目的的方法,
水平切分後,各個庫(表)的特點是:     1)每個庫(表)的結構都一樣     2)每個庫(表)的資料都不一樣,沒有交集     3)所有庫(表)的並集是全量資料 二、什麼是垂直拆分     垂直拆分是指,將一個屬性較多,一行資料較大的表,將不同的屬性拆分到不同的表中,以降低單庫(表)大小,達到提升效能的目的的方法,垂直切分後,各個庫(表)的特點是:     1)每個庫(表)的結構都不一樣     2)一般來說,每個庫(表)的屬性至少有一列交集,一般是主鍵     3)所有庫(表)的並集是全量資料 還是以上文提到的使用者表為例,如果要垂直拆分,可能拆分結果會是這樣的:
user_base(     uid bigint,     name varchar(16),     pass varchar(16),     age int,     sex tinyint,     flag tinyint, …); user_ext(     uid bigint,     sign varchar(64),     intro varchar(256) …);
三、垂直切分的依據是什麼     當一個表屬性很多時,如何來進行垂直拆分呢?如果沒有特殊情況,拆分依據主要有幾點:         1)將長度較短,訪問頻率較高的屬性儘量放在一個表裡,這個表暫且稱為主表         2)將欄位較長,訪問頻率較低的屬性儘量放在一個表裡,這個表暫且稱為擴充套件表     如果1和2都滿足,還可以考慮第三點:         3)經常一起訪問的屬性,也可以放在一個表裡     優先考慮1和2,第3點不是必須。另如果實在屬性過多,主表和擴充套件表都可以有多個。
    一般來說,資料量併發量比較大時,資料庫的上層都會有一個服務層。需要注意的是,當應用方需要同時訪問主表和擴充套件表中的屬性時,服務層不要使用join來連表訪問,而應該分兩次進行查詢:     原因是,大資料高併發網際網路場景下,一般來說,吞吐量和擴充套件性是主要矛盾:         1)join更消損耗資料庫效能         2)join會讓base表和ext表耦合在一起(必須在一個數據庫例項上),不利於資料量大時拆分到不同的資料庫例項上(機器上)。畢竟減少資料量,提升效能才是垂直拆分的初衷。 四、為什麼要這麼這麼拆分     為何要將欄位短,訪問頻率高的屬性放到一個表內?為何這麼垂直拆分可以提升效能?因為:         1)資料庫有自己的記憶體buffer,會將磁碟上的資料load到記憶體buffer裡(暫且理解為程序內快取吧)         2)記憶體buffer快取資料是以row為單位的         3)在記憶體有限的情況下,在資料庫記憶體buffer裡快取短row,就能快取更多的資料         4)在資料庫記憶體buffer裡快取訪問頻率高的row,就能提升快取命中率,減少磁碟的訪問     舉個例子就很好理解了:     假設資料庫記憶體buffer為1G,未拆分的user表1行資料大小為1k,那麼只能快取100w行資料。     如果垂直拆分成user_base和user_ext,其中:         1)user_base訪問頻率高(例如uid, name, passwd, 以及一些flag等),一行大小為0.1k         2)user_ext訪問頻率低(例如簽名, 個人介紹等),一行大小為0.9k     那邊記憶體buffer就就能快取近乎1000w行user_base的記錄,訪問磁碟的概率會大大降低,資料庫訪問的時延會大大降低,吞吐量會大大增加。 五、總結     1)水平拆分和垂直拆分都是降低資料量大小,提升資料庫效能的常見手段     2)流量大,資料量大時,資料訪問要有service層,並且service層不要通過join來獲取主表和擴充套件表的屬性     3)垂直拆分的依據,儘量把長度較短,訪問頻率較高的屬性放在主表裡 希望沒有浪費你這一分鐘,幫轉哈。 單KEY業務,資料庫水平切分架構實踐 本文將以“使用者中心”為例,介紹“單KEY”類業務,隨著資料量的逐步增大,資料庫效能顯著降低,資料庫水平切分相關的架構實踐:     1)如何來實施水平切分     2)水平切分後常見的問題     3)典型問題的優化思路及實踐 一、使用者中心     使用者中心是一個非常常見的業務,主要提供使用者註冊、登入、資訊查詢與修改的服務,其核心元資料為:         User(uid, login_name, passwd, sex, age, nickname, …)     其中:         uid為使用者ID,主鍵         login_name, passwd, sex, age, nickname, …等使用者屬性     資料庫設計上,一般來說在業務初期,單庫單表就能夠搞定這個需求,典型的架構設計為:         user-center:使用者中心服務,對呼叫者提供友好的RPC介面         user-db:對使用者進行資料儲存 二、使用者中心水平切分方法     當資料量越來越大時,需要對資料庫進行水平切分,常見的水平切分演算法有“範圍法”和“雜湊法”。
  • 範圍法
    以使用者中心的業務主鍵uid為劃分依據,將資料水平切分到兩個資料庫例項上去:          
        user-db1:儲存0到1千萬的uid資料         user-db2:儲存1到2千萬的uid資料     範圍法的優點是:         切分策略簡單,根據uid,按照範圍,user- center很快能夠定位到資料在哪個庫上         擴容簡單,如果容量不夠,只要增加user-db3即可     範圍法的不足是:         uid必須要滿足遞增的特性         資料量不均,新增的user-db3,在初期的資料會比較少         請求量不均,一般來說,新註冊的使用者活躍度會比較高,故user-db2往往會比user-db1負載要高,導致伺服器利用率不平衡
  • 雜湊法
    也是以使用者中心的業務主鍵uid為劃分依據,將資料水平切分到兩個資料庫例項上去:      
        user-db1:儲存uid取模得1的uid資料         user-db2:儲存uid取模得0的uid資料     雜湊法的優點是:         切分策略簡單,根據uid,按照hash,user-center很快能夠定位到資料在哪個庫上         資料量均衡,只要uid是均勻的,資料在各個庫上的分佈一定是均衡的         請求量均衡,只要uid是均勻的,負載在各個庫上的分佈一定是均衡的     雜湊法的不足是:         擴容麻煩,如果容量不夠,要增加一個庫,重新hash可能會導致資料遷移,如何平滑的進行資料遷移,是一個需要解決的問題 三、使用者中心水平切分後帶來的問題     使用uid來進行水平切分之後,整個使用者中心的業務訪問會遇到什麼問題呢?     對於uid屬性上的查詢可以直接路由到庫,假設訪問uid=124的資料,取模後能夠直接定位db-user1     
    對於非uid屬性上的查詢,例如login_name屬性上的查詢,就悲劇了,假設訪問login_name=shenjian的資料,由於不知道資料落在哪個庫上,往往需要遍歷所有庫,當分庫數量多起來,效能會顯著降低。     
    如何解決分庫後,非uid屬性上的查詢問題,是後文要重點討論的內容。 四、使用者中心非uid屬性查詢需求分析     任何脫離業務的架構設計都是耍流氓,在進行架構討論之前,先來對業務進行簡要分析,看非uid屬性上有哪些查詢需求。     根據樓主這些年的架構經驗,使用者中心非uid屬性上經常有兩類業務需求:         1) 使用者側,前臺訪問,最典型的有兩類需求             使用者登入:通過login_name/phone/email查詢使用者的實體,1%請求屬於這種型別             使用者資訊查詢:登入之後,通過uid來查詢使用者的例項,99%請求屬這種型別         使用者側的查詢基本上是單條記錄的查詢,訪問量較大,服務需要高可用,並且對一致性的要求較高。         2) 運營側,後臺訪問,根據產品、運營需求,訪問模式各異,按照年齡、性別、頭像、登陸時間、註冊時間來進行查詢。         運營側的查詢基本上是批量分頁的查詢,由於是內部系統,訪問量很低,對可用性的要求不高,對一致性的要求也沒這麼嚴格。 這兩類不同的業務需求,應該使用什麼樣的架構方案來解決呢? 五、使用者中心水平切分架構思路     使用者中心在資料量較大的情況下,使用uid進行水平切分,對於非uid屬性上的查詢需求,架構設計的核心思路為:         1)針對使用者側,應該採用“建立非uid屬性到uid的對映關係”的架構方案         2)針對運營側,應該採用“前臺與後臺分離”的架構方案 六、使用者中心-使用者側最佳實踐 【索引表法】 思路:uid能直接定位到庫,login_name不能直接定位到庫,如果通過login_name能查詢到uid,問題解決 解決方案:     建立一個索引表記錄login_name->uid的對映關係     用login_name來訪問時,先通過索引表查詢到uid,再定位相應的庫     索引表屬性較少,可以容納非常多資料,一般不需要分庫     如果資料量過大,可以通過login_name來分庫 潛在不足:     多一次資料庫查詢,效能下降一倍 【快取對映法】 思路:訪問索引表效能較低,把對映關係放在快取裡效能更佳 解決方案:     login_name查詢先到cache中查詢uid,再根據uid定位資料庫     假設cache miss,採用掃全庫法獲取login_name對應的uid,放入cache     login_name到uid的對映關係不會變化,對映關係一旦放入快取,不會更改,無需淘汰,快取命中率超高     如果資料量過大,可以通過login_name進行cache水平切分 潛在不足: 多一次cache查詢 【login_name生成uid】 思路:不進行遠端查詢,由login_name直接得到uid 解決方案:     在使用者註冊時,設計函式login_name生成uid,uid=f(login_name),按uid分庫插入資料     用login_name來訪問時,先通過函式計算出uid,即uid=f(login_name)再來一遍,由uid路由到對應庫 潛在不足:     該函式設計需要非常講究技巧,有uid生成衝突風險 【login_name基因融入uid】 思路:     不能用login_name生成uid,可以從login_name抽取“基因”,融入uid中     
    假設分8庫,採用uid%8路由,潛臺詞是,uid的最後3個bit決定這條資料落在哪個庫上,這3個bit就是所謂的“基因”。 解決方案:     在使用者註冊時,設計函式login_name生成3bit基因,login_name_gene=f(login_name),如上圖粉色部分     同時,生成61bit的全域性唯一id,作為使用者的標識,如上圖綠色部分     接著把3bit的login_name_gene也作為uid的一部分,如上圖屎黃色部分     生成64bit的uid,由id和login_name_gene拼裝而成,並按照uid分庫插入資料     用login_name來訪問時,先通過函式由login_name再次復原3bit基因,login_name_gene=f(login_name),通過login_name_gene%8直接定位到庫     注:相當於生成一個全域性ID作為第一部分,然後利用login_name生成的uid來做Hash作為第二部分。第一部分保證唯一性,第二部用於快速定位。 七、使用者中心-運營側最佳實踐     前臺使用者側,業務需求基本都是單行記錄的訪問,只要建立非uid屬性 login_name / phone / email 到uid的對映關係,就能解決問題。     後臺運營側,業務需求各異,基本是批量分頁的訪問,這類訪問計算量較大,返回資料量較大,比較消耗資料庫效能。     如果此時前臺業務和後臺業務公用一批服務和一個數據庫,有可能導致,由於後臺的“少數幾個請求”的“批量查詢”的“低效”訪問,導致資料庫的cpu偶爾瞬時100%,影響前臺正常使用者的訪問(例如,登入超時)。     
    而且,為了滿足後臺業務各類“奇形怪狀”的需求,往往會在資料庫上建立各種索引,這些索引佔用大量記憶體,會使得使用者側前臺業務uid/login_name上的查詢效能與寫入效能大幅度降低,處理時間增長。     對於這一類業務,應該採用“前臺與後臺分離”的架構方案:     
    使用者側前臺業務需求架構依然不變,產品運營側後臺業務需求則抽取獨立的web / service / db 來支援,解除系統之間的耦合,對於“業務複雜”“併發量低”“無需高可用”“能接受一定延時”的後臺業務:     1) 可以去掉service層,在運營後臺web層通過dao直接訪問db     2) 不需要反向代理,不需要叢集冗餘     3) 不需要訪問實時庫,可以通過MQ或者線下非同步同步資料     4) 在資料庫非常大的情況下,可以使用更契合大量資料允許接受更高延時的“索引外接”或者“HIVE”的設計方案      
八、總結 將以“使用者中心”為典型的“單KEY”類業務,水平切分的架構點,本文做了這樣一些介紹。 水平切分方式:     水平切分後碰到的問題:         通過uid屬性查詢能直接定位到庫,通過非uid屬性查詢不能定位到庫         非uid屬性查詢的典型業務:             使用者側,前臺訪問,單條記錄的查詢,訪問量較大,服務需要高可用,並且對一致性的要求較高             運營側,後臺訪問,根據產品、運營需求,訪問模式各異,基本上是批量分頁的查詢,由於是內部系統,訪問量很低,對可用性的要求不高,對一致性的要求也沒這麼嚴格     這兩類業務的架構設計思路:         使用者前臺側,“建立非uid屬性到uid的對映關係”最佳實踐:             索引表法:資料庫中記錄login_name->uid的對映關係             快取對映法:快取中記錄login_name->uid的對映關係             login_name生成uid             login_name基因融入uid         運營後臺側,“前臺與後臺分離”最佳實踐:             前臺、後臺系統web/service/db分離解耦,避免後臺低效查詢引發前臺查詢抖動             可以採用資料冗餘的設計方式             可以採用“外接索引”(例如ES搜尋系統)或者“大資料處理”(例如HIVE)來滿足後臺變態的查詢需求 1對多業務,資料庫水平切分架構一次搞定 本文將以“帖子中心”為例,介紹“1對多”類業務,隨著資料量的逐步增大,資料庫效能顯著降低,資料庫水平切分相關的架構實踐:     1) 如何來實施水平切分     2) 水平切分後常見的問題     3) 典型問題的優化思路及實踐 一、什麼是1對多關係     所謂的“1對1”,“1對多”,“多對多”,來自資料庫設計中的“實體-關係”ER模型,用來描述實體之間的對映關係。     1對1         一個使用者只有一個登入名,一個登入名只對應一個使用者         一個uid對應一個login_name,一個login_name只對應一個uid     這是一個1對1的關係。     1對多         一個使用者可以發多條微博,一條微博只有一個傳送者         一個uid對應多個msg_id,一個msg_id只對應一個uid     這是一個1對多的關係。     多對多         一個使用者可以關注多個使用者         一個使用者也可以被多個粉絲關注     這是一個多對多的關係。 二、帖子中心業務分析     帖子中心是一個典型的1對多業務。一個使用者可以釋出多個帖子,一個帖子只對應一個釋出者。任何脫離業務的架構設計都是耍流氓,先來看看帖子中心對應的業務需求。     帖子中心,是一個提供帖子釋出/修改/刪除/檢視/搜尋的服務。     寫操作:         1) 釋出(insert)帖子         2) 修改(update)帖子         3) 刪除(delete)帖子     讀操作:         1) 通過tid查詢(select)帖子實體,單行查詢         2) 通過uid查詢(select)使用者釋出過的帖子,列表查詢         3) 帖子檢索(search),例如通過時間、標題、內容搜尋符合條件的帖子     在資料量較大,併發量較大的時候,通常通過元資料與索引資料分離的架構來滿足不同型別的需求:      
    架構中的幾個關鍵點:         tiezi-center:帖子服務         tiezi-db:提供元資料儲存         tiezi-search:帖子搜尋服務         tiezi-index:提供索引資料儲存         MQ:tiezi-center與tiezi-search通訊媒介,一般不直接使用RPC呼叫,而是通過MQ對兩個子系統解耦(為何這麼解耦,請參見《到底什麼時候該使用MQ?》)     其中,tiezi-center和tiezi-search分別滿足兩類不同的讀需求:      
    如上圖所示:         tid和uid上的查詢需求,可以由tiezi-center從元資料讀取並返回         其他類檢索需求,可以由tiezi-search從索引資料檢索並返回     對於寫需求:     
    如上圖所示:         增加,修改,刪除的操作都會從tiezi-center發起         tiezi-center修改元資料         tiezi-center將資訊修改通知傳送給MQ         tiezi-search從MQ接受修改資訊         tiezi-search修改索引資料     tiezi-search,搜尋架構不是本文的重點(外接索引架構設計,請參見《100億資料1萬屬性資料架構設計》),後文將重點描述帖子中心元資料這一塊的水平切分設計。 三、帖子中心元資料設計     通過帖子中心業務分析,很容易瞭解到,其核心元資料為:         Tiezi(tid, uid, time, title, content, …);     其中:         tid為帖子ID,主鍵         uid為使用者ID,發帖人         time, title, content …等為帖子屬性     資料庫設計上,在業務初期,單庫就能滿足元資料儲存要求,其典型的架構設計為:      
        tiezi-center:帖子中心服務,對呼叫者提供友好的RPC介面         tiezi-db:對帖子資料進行儲存     在相關欄位上建立索引,就能滿足相關業務需求:         帖子記錄查詢,通過tid查詢,約佔讀請求量90%             select * from t_tiezi where tid=$tid         帖子列表查詢,通過uid查詢其釋出的所有帖子,約佔讀請求量10%             select * from t_tiezi where uid=$uid 四、帖子中心水平切分-tid切分法     當資料量越來越大時,需要對帖子資料的儲存進行線性擴充套件。     既然是帖子中心,並且帖子記錄查詢量佔了總請求的90%,很容易想到通過tid欄位取模來進行水平切分:     
    這個方法簡單直接,優點:         100%請求可以直接定位到庫         90%的讀請求可以直接定位到庫     缺點:         一個使用者釋出的所有帖子可能會落到不同的庫上,10%的請求通過uid來查詢會比較麻煩          
    如上圖,一個uid訪問需要遍歷所有庫。 五、帖子中心水平切分-uid切分法     有沒有一種切分方法,確保同一個使用者釋出的所有帖子都落在同一個庫上,而在查詢一個使用者釋出的所有帖子時,不需要去遍歷所有的庫呢?         答:使用uid來分庫可以解決這個問題。     新出現的問題:如果使用uid來分庫,確保了一個使用者的帖子資料落在同一個庫上,那通過tid來查詢,就不知道這個帖子落在哪個庫上了,豈不是還需要遍歷全庫,需要怎麼優化呢?         答:tid的查詢是單行記錄查詢,只要在資料庫(或者快取)記錄tid到uid的對映關係,就能解決這個問題。     新增一個索引庫:             t_mapping(tid, uid);         這個庫只有兩列,可以承載很多資料         即使資料量過大,索引庫可以利用tid水平切分         這類kv形式的索引結構,可以很好的利用cache優化查詢效能         一旦帖子釋出,tid和uid的對映關係就不會發生變化,cache的命中率會非常高     使用uid分庫,並增加索引庫記錄tid到uid的對映關係之後,每當有uid上的查詢:      
    可以通過uid直接定位到庫。     每當有tid上的查詢:         
        先查詢索引表,通過tid查詢到對應的uid,再通過uid定位到庫     這個方法的優點:         一個使用者釋出的所以帖子落在同一個庫上         10%的請求過過uid來查詢列表,可以直接定位到庫         索引表cache命中率非常高,因為tid與uid的對映關係不會變     缺點:        90%的tid請求,以及100%的修改請求,不能直接定位到庫,需要先進行一次索引表的查詢,當然這個查詢非常塊,通常在5ms內可以返回         資料插入時需要操作元資料與索引表,可能引發潛在的一致性問題。 六、帖子中心水平切分-基因法     有沒有一種方法,既能夠通過uid定位到庫,又不需要建立索引表來進行二次查詢呢,這就是本文要敘述的“1對多”業務分庫最佳實踐,基因法。     什麼是分庫基因?     通過uid分庫,假設分為16個庫,採用uid%16的方式來進行資料庫路由,這裡的uid%16,其本質是uid的最後4個bit決定這行資料落在哪個庫上,這4個bit,就是分庫基因    什麼是基因法分庫?     在“1對多”的業務場景,使用“1”分庫,在“多”的資料id生成時,id末端加入分庫基因,就能同時滿足“1”和“多”的分庫查詢需求。     
    如上圖所示,uid=666的使用者釋出了一條帖子(666的二進位制表示為:1010011010):         使用uid%16分庫,決定這行資料要插入到哪個庫中         分庫基因是uid的最後4個bit,即1010         在生成tid時,先使用一種分散式ID生成演算法生成前60bit(上圖中綠色部分)         將分庫基因加入到tid的最後4個bit(上圖中粉色部分)         拼裝成最終的64bit帖子tid(上圖中藍色部分) (怎麼生成60bit分散式唯一ID,請參見《分散式ID生成演算法》)     這般,保證了同一個使用者釋出的所有帖子的tid,都落在同一個庫上,tid的最後4個bit都相同,於是:         通過uid%16能夠定位到庫         通過tid%16也能定位到庫     注:這個本質上還是使用uid進行分庫的,但是由於tid中攜帶了uid的資訊,所以tid單獨也可以定位到庫。本質上《單KEY業務,資料庫水平切分架構實踐》是這一篇的特殊情況。log_name對應一個使用者,而一個uid則對應多個帖子。但是,最後都是用“額外”那個資訊來作為分庫依據,讓主鍵生成過程中包含這個依據。     潛在問題一:同一個uid釋出的tid落在同一個庫上,會不會出現資料不均衡?         答:只要uid是均衡的,每個使用者釋出的平均帖子數是均衡的,每個庫的資料就是均衡的。     潛在問題二:最開始分16庫,分庫基因是4bit,未來要擴充成32庫,分庫基因變成了5bit,那怎麼辦?         答:需要提前做好容量預估,例如事先規劃好5年內資料增長256庫足夠,就提前預留8bit基因。 七、總結     將以“帖子中心”為典型的“1對多”類業務,在架構上,採用元資料與索引資料分離的架構設計方法:         帖子服務,元資料滿足uid和tid的查詢需求         搜尋服務,索引資料滿足複雜搜尋尋求     對於元資料的儲存,在資料量較大的情況下,有三種常見的切分方法:         tid切分法,按照tid分庫,同一個使用者釋出的帖子落在不同的庫上,通過uid來查詢要遍歷所有庫         uid切分法,按照uid分庫,同一個使用者釋出的帖子落在同一個庫上,需要通過索引表或者快取來記錄tid與uid的對映關係,通過tid來查詢時,先查到uid,再通過uid定位庫         基因法,按照uid分庫,在生成tid里加入uid上的分庫基因,保證通過uid和tid都能直接定位到庫 對於1對多的業務場景,分庫架構不再是瓶頸。