天天看點

高性能資料庫叢集:分庫分表

讀寫分離分散了資料庫讀寫操作的壓力,但沒有分散存儲壓力,當資料量達到千萬甚至上億條的時候,單台資料庫伺服器的存儲能力會成為系統的瓶頸,主要展現在這幾個方面:

  • 資料量太大,讀寫的性能會下降,即使有索引,索引也會變得很大,性能同樣會下降。
  • 資料檔案會變得很大,資料庫備份和恢複需要耗費很長時間。
  • 資料檔案越大,極端情況下丢失資料的風險越高(例如,機房火災導緻資料庫主備機都發生故障)。

基于上述原因,單個資料庫伺服器存儲的資料量不能太大,需要控制在一定的範圍内。為了滿足業務資料存儲的需求,就需要将存儲分散到多台資料庫伺服器上。

常見的分散存儲的方法“分庫分表”,其中包括“分庫”和“分表”兩大類。

業務分庫

業務分庫指的是按照業務子產品将資料分散到不同的資料庫伺服器。例如,一個簡單的電商網站,包括使用者、商品、訂單三個業務子產品,我們可以将使用者資料、商品資料、訂單資料分開放到三台不同的資料庫伺服器上,而不是将所有資料都放在一台資料庫伺服器上。

這樣做能夠分散存儲和通路壓力,但同時也帶來了新的問題。

1.join操作

業務分庫後,原本在同一個資料庫中的表分散到不同資料庫中,導緻無法使用 SQL 的 join 查詢。

例如:“查詢購買了化妝品的使用者中女性使用者的清單”這個功能,雖然訂單資料中有使用者的 ID 資訊,但是使用者的性别資料在使用者資料庫中,如果在同一個庫中,簡單的 join 查詢就能完成;但現在資料分散在兩個不同的資料庫中,無法做 join 查詢,隻能采取先從訂單資料庫中查詢購買了化妝品的使用者 ID 清單,然後再到使用者資料庫中查詢這批使用者 ID 中的女性使用者清單,這樣實作就比簡單的 join 查詢要複雜一些。

2. 事務問題

原本在同一個資料庫中不同的表可以在同一個事務中修改,業務分庫後,表分散到不同的資料庫中,無法通過事務統一修改。雖然資料庫廠商提供了一些分布式事務的解決方案(例如,MySQL 的 XA),但性能實在太低,與高性能存儲的目标是相違背的。

例如,使用者下訂單的時候需要扣商品庫存,如果訂單資料和商品資料在同一個資料庫中,我們可以使用事務來保證扣減商品庫存和生成訂單的操作要麼都成功要麼都失敗,但分庫後就無法使用資料庫事務了,需要業務程式自己來模拟實作事務的功能。例如,先扣商品庫存,扣成功後生成訂單,如果因為訂單資料庫異常導緻生成訂單失敗,業務程式又需要将商品庫存加上;而如果因為業務程式自己異常導緻生成訂單失敗,則商品庫存就無法恢複了,需要人工通過日志等方式來手工修複庫存異常。

3. 成本問題

業務分庫同時也帶來了成本的代價,本來 1 台伺服器搞定的事情,現在要 3 台,如果考慮備份,那就是 2 台變成了 6 台。

基于上述原因,對于小公司初創業務,并不建議一開始就這樣拆分,主要有幾個原因:

  • 初創業務存在很大的不确定性,業務不一定能發展起來,業務開始的時候并沒有真正的存儲和通路壓力,業務分庫并不能為業務帶來價值。
  • 業務分庫後,表之間的 join 查詢、資料庫事務無法簡單實作了。
  • 業務分庫後,因為不同的資料要讀寫不同的資料庫,代碼中需要增加根據資料類型映射到不同資料庫的邏輯,增加了工作量。而業務初創期間最重要的是快速實作、快速驗證,業務分庫會拖慢業務節奏。

如果業務真的發展很快,豈不是很快就又要進行業務分庫了?那為何不一開始就設計好呢?

其實這個問題很好回答,按照我前面提到的“架構設計三原則”,簡單分析一下。

首先,這裡的“如果”事實上發生的機率比較低,做 10 個業務有 1 個業務能活下去就很不錯了,更何況快速發展,和中彩票的機率差不多。如果我們每個業務上來就按照淘寶、微信的規模去做架構設計,不但會累死自己,還會害死業務。其次,如果業務真的發展很快,後面進行業務分庫也不遲。因為業務發展好,相應的資源投入就會加大,可以投入更多的人和更多的錢,那業務分庫帶來的代碼和業務複雜的問題就可以通過增加人來解決,成本問題也可以通過增加資金來解決。

第三,單台資料庫伺服器的性能其實也沒有想象的那麼弱,一般來說,單台資料庫伺服器能夠支撐 10 萬使用者量量級的業務,初創業務從 0 發展到 10 萬級使用者,并不是想象得那麼快。

而對于業界成熟的大公司來說,由于已經有了業務分庫的成熟解決方案,并且即使是嘗試性的新業務,使用者規模也是海量的,這與前面提到的初創業務的小公司有本質差別,是以最好在業務開始設計時就考慮業務分庫。例如,在淘寶上做一個新的業務,由于已經有成熟的資料庫解決方案,使用者量也很大,需要在一開始就設計業務分庫甚至接下來介紹的分表方案。

分表

将不同業務資料分散存儲到不同的資料庫伺服器,能夠支撐百萬甚至千萬使用者規模的業務,但如果業務繼續發展,同一業務的單表資料也會達到單台資料庫伺服器的處理瓶頸。例如,淘寶的幾億使用者資料,如果全部存放在一台資料庫伺服器的一張表中,肯定是無法滿足性能要求的,此時就需要對單表資料進行拆分。

單表資料拆分有兩種方式:垂直分表和水準分表。示意圖如下:

高性能資料庫叢集:分庫分表

單表進行切分後,是否要将切分後的多個表分散在不同的資料庫伺服器中,可以根據實際的切分效果來确定,并不強制要求單表切分為多表後一定要分散到不同資料庫中。原因在于單表切分為多表後,新的表即使在同一個資料庫伺服器中,也可能帶來可觀的性能提升,如果性能能夠滿足業務要求,是可以不拆分到多台資料庫伺服器的,畢竟我們在上面業務分庫的内容看到業務分庫也會引入很多複雜性的問題;如果單表拆分為多表後,單台伺服器依然無法滿足性能要求,那就不得不再次進行業務分庫的設計了。

分表能夠有效地分散存儲壓力和帶來性能提升,但和分庫一樣,也會引入各種複雜性。

1. 垂直分表

垂直分表适合将表中某些不常用且占了大量空間的列拆分出去。

假設我們是一個婚戀網站,使用者在篩選其他使用者的時候,主要是用 age 和 sex 兩個字段進行查詢,而 nickname 和 description 兩個字段主要用于展示,一般不會在業務查詢中用到。description 本身又比較長,是以我們可以将這兩個字段獨立到另外一張表中,這樣在查詢 age 和 sex 時,就能帶來一定的性能提升。

垂直分表引入的複雜性主要展現在表操作的數量要增加。例如,原來隻要一次查詢就可以擷取 name、age、sex、nickname、description,現在需要兩次查詢,一次查詢擷取 name、age、sex,另外一次查詢擷取 nickname、description。

2.水準分表

這個複雜性遠比垂直分表要大。

水準分表适合表行數特别大的表,有的公司要求單表行數超過 5000 萬就必須進行分表,這個數字可以作為參考,但并不是絕對标準,關鍵還是要看表的通路性能。對于一些比較複雜的表,可能超過 1000 萬就要分表了;而對于一些簡單的表,即使存儲資料超過 1 億行,也可以不分表。但不管怎樣,當看到表的資料量達到千萬級别時,作為架構師就要警覺起來,因為這很可能是架構的性能瓶頸或者隐患。

水準分表相比垂直分表,會引入更多的複雜性,主要表現在下面幾個方面:

  • 路由

水準分表後,某條資料具體屬于哪個切分後的子表,需要增加路由算法進行計算,這個算法會引入一定的複雜性。

常見的路由算法有:

範圍路由:選取有序的資料列(例如,整形、時間戳等)作為路由的條件,不同分段分散到不同的資料庫表中。以最常見的使用者 ID 為例,路由算法可以按照 1000000 的範圍大小進行分段,1 ~ 999999 放到資料庫 1 的表中,1000000 ~ 1999999 放到資料庫 2 的表中,以此類推。

範圍路由設計的複雜點主要展現在分段大小的選取上,分段太小會導緻切分後子表數量過多,增加維護複雜度;分段太大可能會導緻單表依然存在性能問題,一般建議分段大小在 100 萬至 2000 萬之間,具體需要根據業務選取合适的分段大小。

範圍路由的優點是可以随着資料的增加平滑地擴充新的表。例如,現在的使用者是 100 萬,如果增加到 1000 萬,隻需要增加新的表就可以了,原有的資料不需要動。

範圍路由的一個比較隐含的缺點是分布不均勻,假如按照 1000 萬來進行分表,有可能某個分段實際存儲的資料量隻有 1000 條,而另外一個分段實際存儲的資料量有 900 萬條。

Hash 路由:選取某個列(或者某幾個列組合也可以)的值進行 Hash 運算,然後根據 Hash 結果分散到不同的資料庫表中。同樣以使用者 ID 為例,假如我們一開始就規劃了 10 個資料庫表,路由算法可以簡單地用 user_id % 10 的值來表示資料所屬的資料庫表編号,ID 為 985 的使用者放到編号為 5 的子表中,ID 為 10086 的使用者放到編号為 6 的字表中。Hash 路由設計的複雜點主要展現在初始表數量的選取上,表數量太多元護比較麻煩,表數量太少又可能導緻單表性能存在問題。而用了 Hash 路由後,增加子表數量是非常麻煩的,所有資料都要重分布。Hash 路由的優缺點和範圍路由基本相反,Hash 路由的優點是表分布比較均勻,缺點是擴充新的表很麻煩,所有資料都要重分布。

配置路由:配置路由就是路由表,用一張獨立的表來記錄路由資訊。同樣以使用者 ID 為例,我們新增一張 user_router 表,這個表包含 user_id 和 table_id 兩列,根據 user_id 就可以查詢對應的 table_id。配置路由設計簡單,使用起來非常靈活,尤其是在擴充表的時候,隻需要遷移指定的資料,然後修改路由表就可以了。配置路由的缺點就是必須多查詢一次,會影響整體性能;而且路由表本身如果太大(例如,幾億條資料),性能同樣可能成為瓶頸,如果我們再次将路由表分庫分表,則又面臨一個死循環式的路由算法選擇問題。

  • join 操作

水準分表後,資料分散在多個表中,如果需要與其他表進行 join 查詢,需要在業務代碼或者資料庫中間件中進行多次 join 查詢,然後将結果合并。

  • count() 操作

水準分表後,雖然實體上資料分散到多個表中,但某些業務邏輯上還是會将這些表當作一個表來處理。例如,擷取記錄總數用于分頁或者展示,水準分表前用一個 count() 就能完成的操作,在分表後就沒那麼簡單了。常見的處理方式有下面兩種:

count() 相加:具體做法是在業務代碼或者資料庫中間件中對每個表進行 count() 操作,然後将結果相加。這種方式實作簡單,缺點就是性能比較低。例如,水準分表後切分為 20 張表,則要進行 20 次 count(*) 操作,如果串行的話,可能需要幾秒鐘才能得到結果。

記錄數表:具體做法是建立一張表,假如表名為“記錄數表”,包含 table_name、row_count 兩個字段,每次插入或者删除子表資料成功後,都更新“記錄數表”。

這種方式擷取表記錄數的性能要大大優于 count() 相加的方式,因為隻需要一次簡單查詢就可以擷取資料。缺點是複雜度增加不少,對子表的操作要同步操作“記錄數表”,如果有一個業務邏輯遺漏了,資料就會不一緻;且針對“記錄數表”的操作和針對子表的操作無法放在同一事務中進行處理,異常的情況下會出現操作子表成功了而操作記錄數表失敗,同樣會導緻資料不一緻。

此外,記錄數表的方式也增加了資料庫的寫壓力,因為每次針對子表的 insert 和 delete 操作都要 update 記錄數表,是以對于一些不要求記錄數實時保持精确的業務,也可以通過背景定時更新記錄數表。定時更新實際上就是“count() 相加”和“記錄數表”的結合,即定時通過 count() 相加計算表的記錄數,然後更新記錄數表中的資料。

  • order by 操作

水準分表後,資料分散到多個子表中,排序操作無法在資料庫中完成,隻能由業務代碼或者資料庫中間件分别查詢每個子表中的資料,然後彙總進行排序。

實作方法

和資料庫讀寫分離類似,分庫分表具體的實作方式也是“程式代碼封裝”和“中間件封裝”,但實作會更複雜。讀寫分離實作時隻要識别 SQL 操作是讀操作還是寫操作,通過簡單的判斷 SELECT、UPDATE、INSERT、DELETE 幾個關鍵字就可以做到,而分庫分表的實作除了要判斷操作類型外,還要判斷 SQL 中具體需要操作的表、操作函數(例如 count 函數)、order by、group by 操作等,然後再根據不同的操作進行不同的處理。例如 order by 操作,需要先從多個庫查詢到各個庫的資料,然後再重新 order by 才能得到最終的結果。

參考:李運華老師《從0開始學架構》(極客時間)

繼續閱讀