iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 26
2
Software Development

30天之從 0 至 1 盡可能的建立一個好的系統 (性能基礎篇)系列 第 26

30-26之資料庫層的擴展 - 分庫分表架構

黑色好看版 - 傳送門


https://ithelp.ithome.com.tw/upload/images/20191011/20089358wfhe9Vz08O.png

正文開始

上一篇文章中,咱們介紹了資料庫層的分散的第一個起手式『 讀寫分離 』,這個方案是將寫與讀分散在不同的機器上,正常情況下,大部份的系統使用這種方案就已經可以處理很好了。

但 !

如果你已經將資料庫層與緩存層的架構都已經建立好,但還是發現有性能貧頸,那接下來才會建議使用幾個方案,因為這些方案沒用好,會衍生出非常多的問題。

本篇文章分為以下幾個章節,這些就是接下來咱們要來學的擴展法。

  • 分庫
  • 分表
  • 分庫與分表的問題

重要 : 使用前注意事項

要使用以下的擴展方法時,先確認你的資料庫是否以下的問題是否有發生。

  • 問題 1 : 單庫太大,導致硬碟空間不夠囉。
  • 問題 2 : 單庫寫入量太大,導致每一次新增或更新性能非常的吃緊,感覺隨時都會上天堂。
  • 問題 3 : 單表資料量太太,導致每一次操作時都非常的慢。

有以上事情發生才開始往接下來的擴展走。

沒事真的別用它們

分庫


它可以解決
問題 1 : 單庫太大,導致硬碟空間不夠囉

問題 2 : 單庫寫入量太大,導致每一次新增或更新性能非常的吃緊

首先第一個要介紹的就是分庫,它的基本定義如下 :

將一個大大的資料庫,依據『 規則 』拆分成小的資料庫

其中上述說的規則,在傳統上可以分為以下幾種 :

  • 垂直切分 : 根據『 業務 』來拆分成多個小資料庫 ( 圖 1 所示 )。
  • 水平切分 : 根據『 特性 』來拆分成多個小資料庫,例如地區 ( 圖 2 所示 )。

https://ithelp.ithome.com.tw/upload/images/20191011/20089358NiVxczyhHQ.png
圖 1 : 垂直切分範例

https://ithelp.ithome.com.tw/upload/images/20191011/20089358jdmFWBMEIZ.png
圖 2 : 水平切分範例

這個方案基本上在業界應該算是非常常見的方案,尤其是以業務的垂直切分這個方案,整體而言還是有不少的優點,如下幾個 :

  • 減少單庫的性能壓力
  • 減少備份復原的時間
  • 增加維護性,例如下修某個資料庫,不會影響到所有的功能。

業界的確很常使用這個方案,它們會根據業務需求來分成多個庫,可是上面不是建議沒必要,不要這樣搞嗎 ? 嗯對沒錯,但是如果是下面這種情況下,那的確可以切分 :

假設在業務擴展初出,所有資料都寫到同一個資料庫中,但後其長大後,就將不同『 單位業務 』的資料分不同資料庫,它們彼此間很獨立,非常少會一起使用

這種情況下,我覺得切分合理,而且這很常見。就算是咱們某個資料庫中還是有一堆奇奇怪怪不知道是幹啥的表,或是其它業務的表,這些都是一間公司從 0 至 1+ 時都會有的過程。

但如果是一堆需要共要在一起的業務,硬是要把他切分成不同資料庫,那我想打爆那個提出這樣切的人的頭。

分表


這種方法可以解決以下的問題 :

它可以解決問題 3 : 單表資料量太太,導致每一次操作時都非常的慢。

簡單它們的概念都 :

將一張很大的表,根據『 key 』來分成小張的表

它本質上是應用層自已手動實現,根據規則將一個表,分成多個表,這表可能在同一台機器上,或是不同機器上,應用層 sql 代碼需修改。

開始前的小提醒

分表這種擴展法是用在單表太大的情境。

假設你們的系統是聊天室這種類型的系統,然後你們會將用戶說的每一句話儲存在某一張表,那你們遲早會碰到表太大,影響查詢這種問題的。

不過先說一下,並不是一定要用這種方法來處理此情境,像以咱們公司是這樣處理的。

  1. 根據業務規則,來定義資料表中要儲多久的資料,例如 3 個月左右。
  2. 將 3 個月之前的資料全部打包壓縮丟到 aws s3 或啥的。
  3. 寫一個每天的排程,來定時的砍資料與移資料。

如果真的有需要去 s3 找資料,你也可以用 aws athena 去查詢資料的,雖然有點貴,但是久久才用一次,沒毛病。

上述是題外話,假設咱們真的都需要儲放在資料表中,那咱們就只能選擇『 分表與分區表 』這個方案囉。

不過說句真心話,沒必要真的不要用它,因此之後會很多問題。

拉回分表

分表的基本概念就是如下,咱們假設以 id 為 key,將這張表拆成如下圖 3 所示。分區表下篇文章會說明。

https://ithelp.ithome.com.tw/upload/images/20191011/20089358R5Urg1zKv1.png
圖 3 : 分表範例

問個問題,分表有需要分到不同的機器嗎 ? 例如下圖 4 所示呢 ?

除非,你的單表真的大到,就算進行分表後,單機仍然無法負荷它的資料量,那這樣才可以考慮,不然雷會很多,因為這就代表分庫 + 分表的問題你都需要一同考慮。

https://ithelp.ithome.com.tw/upload/images/20191011/20089358yWS6t38SZ7.png
圖 4 : 分表不同機器圖

要使用什麼當切分鍵 ?

分片( Sharding )就是咱們要用什麼規則來去分割資料的意思。以我們這裡來看就是咱們要用什麼欄位,來去分割資料。

先說一下,我們會很難到完美的分片,一個完美的分片可以讓我們做到以下的事情 :

  • 可以平均將查詢操作,分配到每一張表。
  • 可以平均的將新增、更新等操作,分配到每一張表上。

咱們很難找到一個完美的選擇,但是可以先定一個最低的標準選項 :

高基數欄位,這是最低標準

為啥 ? 因為這樣才好分割,你想想如果選擇性別男與女來當分割,那這樣你不是只能分成兩份嗎 ? 如果又滿了,你要如何處理 ?

然後這裡列一下一些常見的分片欄位選項 :

選擇 1 : 升序特性欄位

『 自動產生編號 』 或是 『 日期 』

根據 id 或日期來分區的這種都算是升序特性欄位。以 id 為例,它們的分割方式為 :

id_1_1000 : 儲放 id 編號 1 至 1000 的資料
id_1001_2000 : 儲放 id 編號 1001 至 2000 的資料

但是這事實上不能說是好選項,主要的原因在於 :

操作會壓在一張表上

因為通常新的資料者是會放在同一張表,所以大部份的操作都會壓往他身上。

選擇 2 : Hash 編號 Id

根據 id + hash 來進行切分,假設咱們有個 hash 函數如下 :

hashId = id % 10

這種做法比上述的直接編號來分割還優質點,因為它可以平均的分散到不同的表上。先說一下上述只是 hash 的簡單範例,實際上不一定是用上述方式分割。

選擇 3 : 隨機特性的欄位

像是一些沒有規則的欄位,例如信箱或是一些亂數編號這種,這種欄位分片可以平均的分散到每一張表,但是比較大的問題在於 :

查詢

由於分配是隨機的,所以查詢時不知道要去那張表找,所以只能一個一個慢慢找。

選擇 4 : 組合型的欄位

假設咱們有個資料表是專門存放聊天訊息的,有以下欄位 :

id, type , text, user_id, created_at

type : 它有 5 種類型 ( 可能是表示不同的服務 )

那這時如果我們只選擇 type,會發現它的基數太小,怎麼分就可能只有五張表,這樣還是會碰到貧頸的。

但這時咱們可以轉個想,如果一次用兩個欄位呢 ?

{ type, created_at }

例如咱們可以根據 type 與 created_at 來當分片欄位組合,這樣的話資料表會分為如下圖 5 所示。

https://ithelp.ithome.com.tw/upload/images/20191011/200893584euxx7QrV6.png
圖 5 : 組合型範例

這樣就有幾種好處 :

  • 查詢方便,如果要查某種類型的,只要去那種類型的表查就好,如果要查時間,就去該段時間查詢。
  • 有將壓力分散,雖然 type 基數小,但是我們可以同過細分時間段,來減少壓力。

上面這個只是組合型的範例,不代表一定要這樣使用,這裡只是要點醒一下 :

分片欄位不一定是要一個欄位組成

分表與分庫可能會碰到的問題


問題 1 : 事務如何處理呢 ?

咱們之前的這兩篇文章中,有談到單機一致性的問題。

30-15 之資料庫層的難題 - 單機『 故障 』一致性難題
30-16 之資料庫層的難題 - 單機『 並行 』一致性難題 ( 1 )
30-17 之資料庫層的難題 - 單機『 並行 』一致性難題 ( 2 )

而它們都是依賴 mysql 所提供的 redo log 與 undo log 還有鎖之類的功能,來達成事務的 acid 特性。

那如果這個換成『 多台 』資料庫的話,你要如何處理呢 ?

這個問題咱們會在之後的文章『 分散式事務 』的主是中來慢慢的談談。

問題 2 : Join 如何處理呢 ?

在分庫以後,基本上有以下幾種方法解決 :

  • 不要用 : 自行在應用層抓一抓你要的資料,然後拼裝超來。
  • 一點點的反正規化 : 也就是說在設計表時,會多加一些欄位,例如訂單與用戶這樣的組合,有可能會在訂單這個分庫的資訊上,多儲放一些用戶的資訊,這樣就不需要使用到 join。
  • 統一透過中間件處理 : 有一些中間件軟體會提供多庫 join 的操作,以 mycat 來看它有提供,不過只提供兩層的 join,這個地方在之後 mycat 的文章中會提。

這裡是比較建議分庫後儘量不要用 join,因為坑會有點大,而且你儘然分庫了,你還很常需要 join,那是不是要想想,分庫的規則有問題了呢 ?

問題 3 : 用戶端要如何處理呢 ?

通常咱們在分表以後,都需要自已去指定要去那一張表來處理,但是這樣非常的麻煩,所以我們通常會使用 mysql 中間件來處理,變成如下圖 6 所示,這之後會開一篇文章來介紹中間件。

正常分表後,SQL 指定修改

SELECT * FROM table_2019_01 

https://ithelp.ithome.com.tw/upload/images/20191011/20089358cHX1YGhxcW.png
圖 6 : 中間件。

問題 4 : 唯一編號如何處理呢 ?

在分表或水平分庫以後,你可能還要考慮每一張表的唯一編號要如何產生,正常只有一張表時,咱們可以使用自動產生編號來建立順序的編號,但多張時就不能這樣用,因為會衝突。

基本上有以下幾種方案 :

方案 1 : UUID

簡單,但是缺點在於沒順序、空間大,這兩個都會影響性能。

在順序的編號,在查詢時可能增加不少效率。而 uuid 所佔空間比較大,會影響到所建立的 b+ 樹每個索引節點的大小,進而影響到操作性能。

方案 2 : 自動編號 + 偏移

假設你有 4 張表,那他們的生成編號為 :

A 表 : 1,5,9,13
B 表 : 2,6,10,14
C 表 : 3,7,11,15
D 表 : 4,8,12,16

但缺點就在於不好擴展。

方案 3 : redis 生成

redis 有提供一個方法 :

incr

可以使用它來產生全局的順序編號,但缺點就在於需要連 redis 處理,還要考慮它是否活這。

問題 5 : 分表分頁如何處理呢 ?

像咱們在單一表進行分頁時,通常是會下如下的指令,說要拿第 6 至 15 的資料 :

SELECT * FROM table ORDER BY time LIMIT 5,10;

但是現在一個大表變成如下 :

table = tableA + tableB + tableC

在分表的情境中,要如何處理呢 ?

首先假設如果咱們的分表是使用『 區間 』來拆分例如日期,那這種情況下,應該還是到指定的表去尋找就夠了,沒啥毛病。

但如果是用 hash id 這種來分的表呢 ?

有人會想說,那就每個表的資料都抓出來,在排序然後再抓取前 n 個值不就好呢 ? 服務會炸裂的,分表的原因就是單表太大,你還要進行排序 ( nlogn ) ?

說實話,我到還沒碰到分表然後又要分頁的情況,這時只能給點參考文件,目前我覺得寫的最完整的是這一篇文章,這個作者也是我之前有提過的『 架構師之路 』這一系列優質文的筆者,他的文章真的只能說優質上等來評價。

而至於分表分頁這問題,等未來我在這個問題有更深入的理解後,在開篇文章來談談。

58沈剑 架构师之路-业界难题-“跨库分页”的四种方案

結論與心得


本篇文章中,咱們討論了另外兩種資料庫層的擴展 :

  • 分庫
  • 分表

基本上我覺得這兩種都是沒必要,不要用的東西,因為如果你在還沒到性能貧頸時就用這幾招,你會面臨到的問題是一個大大的坑,請注意。

分庫這種場境,可以解決以下幾個問題 :

  • 問題 1 : 單庫太大,導致硬碟空間不夠囉。
  • 問題 2 : 單庫寫入量太大,導致每一次新增或更新性能非常的吃緊,感覺隨時都會上天堂。

而分表方面可以解決以下幾個問題 :

  • 問題 3 : 單表資料量太太,導致每一次操作時都非常的慢。

雖然可以解決,但是同時也會帶來以下這些鬼問題,請注意。

  • 事務如何處理 ?
  • Join 如何處理呢 ?
  • 用戶端要如何處理呢 ?
  • 唯一編號如何處理呢 ?
  • 分表分頁如何處理呢 ?

這些問題 :

都是在分庫或分表『 前 』你就要思考好

分了後,才思考,你一定死。

參考資料



上一篇
30-25之資料庫層的擴展 - 讀寫分離架構
下一篇
30-27之資料庫層的擴展 - 分區表
系列文
30天之從 0 至 1 盡可能的建立一個好的系統 (性能基礎篇)30

尚未有邦友留言

立即登入留言