簡單瞭解 TiDB 架構

一、前言

大家如果看過我之前發過的文章就知道,我寫過很多篇關於 MySQL 的文章,從我的 Github 彙總倉庫 中可以看出來:

可能還不是很全,算是對 MySQL 有一個淺顯但較爲全面的理解。之前跟朋友聊天也會聊到,基於現有的微服務架構,絕大多數的性能瓶頸都不在服務,因爲我們的服務是可以橫向擴展的。

在很多的 case 下,這個瓶頸就是「數據庫」。例如,我們爲了減輕 MySQL 的負擔,會引入消息隊列來對流量進行削峯;再例如會引入 Redis 來緩存一些不太常變的數據,來減少對 MySQL 的請求。

另一方面,如果業務往 MySQL 中灌入了海量的數據,不做優化的話,會影響 MySQL 的性能。而對於這種情況,就需要進行分庫分表,落地起來還是較爲麻煩的。

聊着聊着,就聊到了分佈式數據庫。其對數據的存儲方式就類似於 Redis Cluster 這種,不管你給我灌多少的數據,理論上我都能夠吞下去。這樣一來也不用擔心後期數據量大了需要進行分庫分表。

剛好,之前閒逛的時候看到了 PingCAP 的 TiDB,正好就來聊一聊。

二、正文

由於是簡單瞭解,所以更多的側重點在存儲

1.TiDB Server

還是從一個黑盒子講起,在沒有了解之前,我們對 TiDB 的認識就是,我們往裏面丟數據,TiDB 負責存儲數據。並且由於是分佈式的,所以理論上只要存儲資源夠,多大的數據都能夠存下。

我們知道,TiDB 支持 MySQL,或者說兼容大多數 MySQL 的語法。那我們就還是拿一個 Insert 語句來當作切入點,探索數據在 TiDB 中到底是如何存儲的。

首先要執行語句,必然要先建立連接。

在 MySQL 中,負責處理客戶端連接的是 MySQL Server,在 TiDB 中也有同樣的角色 —— TiDB Server,雖角色類似,但兩者有着很多的不同

TiDB Server 對外暴露 MySQL 協議,負責 SQL 的解析、優化,並最終生成分佈式執行計劃,MySQL 的 Server 層也會涉及到 SQL 的解析、優化,但與 MySQL 最大的不同在於,TiDB Server 是無狀態的。

而 MySQL Server 由於和底層存儲引擎的耦合部署在同一個節點,並且在內存中緩存了頁的數據,是有狀態的。

這裏其實可以簡單的把兩者理解爲,TiDB 是無狀態的可橫向擴展的服務。而 MySQL 則是在內存中緩存了業務數據、無法橫向擴展的單體服務

而由於 TiDB Server 的無狀態特性,在生產中可以啓動多個實例,並通過負載均衡的策略來對外提供統一服務。

實際情況下,TiDB 的存儲節點是單獨、分佈式部署的,這裏只是爲了方便理解 TiDB Server 的橫向擴展特性,不用糾結,後面會聊到存儲

總結下來,TiDB Server 只幹一件事:負責解析 SQL,將實際的數據操作轉發給存儲節點

2.TiKV

我們知道,對於 MySQL,其存儲引擎(絕大多數情況)是 InnoDB,其存儲採用的數據結構是 B+ 樹,最終以 .ibd 文件的形式存儲在磁盤上。那 TiDB 呢?

TiDB 的存儲是由 TiKV 來負責的,這是一個分佈式、支持事務的 KV 存儲引擎。說到底,它就是個 KV 存儲引擎。

用大白話說,這就是個巨大的、有序的 Map。但說到 KV 存儲,很多人可能會聯想到 Redis,數據大多數時候是放在內存,就算是 Redis,也會有像 RDB 和 AOF 這樣的持久化方式。那 TiKV 作爲一個分佈式的數據庫,也不例外。它採用 RocksDB 引擎來實現持久化,具體的數據落地由其全權負責。

RocksDB 是由 Facebook 開源的、用 C++ 實現的單機 KV 存儲引擎。

3.索引數據

直接拿官網的例子給大家看看,話說 TiDB 中有這樣的一張表:

然後表裏有三行數據:

這三行數據,每一行都會被映射成爲一個鍵值對:

其中,Key 中的 t10 代表 ID 爲 10 的表,r1 代表 RowID 爲 1 的行,由於我們建表時制定了主鍵,所以 RowID 就爲主鍵的值。Value 就是該行除了主鍵之外的其他字段的值,上圖表示的就是主鍵索引

那如果是非聚簇索引(二級索引)呢?就比如索引 idxAge,建表語句裏對 Age 這一列建立了二級索引:

i1 代表 ID 爲 1 的索引,即當前這個二級索引,10、20、30 則是索引列 Age 的值,最後的 1、2、3 則是對應的行的主鍵 ID。從建索引的語句部分可以看出來,idxAge 是個普通的二級索引,不是唯一索引。所以索引中允許存在多個 Age 爲 30 的列。

但如果我們是唯一索引呢?

只拿表 ID、索引 ID 和索引值來組成 Key,這樣一來如果再次插入 Age 爲 30 的數據,TiKV 就會發現該 Key 已經存在了,就能做到唯一鍵檢測。

4.存儲細節

知道了列數據是如何映射成 Map 的,我們就可以繼續瞭解存儲相關的細節了。

從圖中,我們可以看出個問題:如果某個 TiKV 節點掛了,那麼該節點上的所有數據是不是都沒了?

當然不是的,TiDB 可以是一款金融級高可用的分佈式關係型數據庫,怎麼可能會讓這種事發生。

TiKV 在存儲數據時,會將同一份數據想辦法存儲到多個 TiKV 節點上,並且使用 Raft 協議來保證同一份數據在多個 TiKV 節點上的數據一致性。

上圖爲了方便理解,進行了簡化。實際上一個 TiKV 中有存在 2 個 RocksDB。一個用於存儲 Raft Log,通常叫 RaftDB,而另一個用於存儲用戶數據,通常叫 KVDB。

簡單來說,就是會選擇其中一份數據作爲 Leader 對外提供讀、寫服務,其餘的作爲 Follower 僅僅只同步 Leader 的數據。當 Leader 掛掉之後,可以自動的進行故障轉移,從 Follower 中重新選舉新的 Leader 出來。

看到這,是不是覺得跟 Kafka 有那麼點神似了。Kafka 中一個 Topic 是邏輯概念,實際上會分成多個 Partition,分散到多個 Broker 上,並且會選舉一個 Leader Partition 對外提供服務,當 Leader Partition 出現故障時,會從 Follower Partiiton 中重新再選舉一個 Leader 出來。

那麼,Kafka 中選舉、提供服務的單位是 Partition,TiDB 中的是什麼呢?

5.Region

答案是 Region。剛剛講過,TiKV 可以理解爲一個巨大的 Map,而 Map 中某一段連續的 Key 就是一個 Region。不同的 Region 會保存在不同的 TiKV 上。

一個 Region 有多個副本,每個副本也叫 Replica,多個 Replica 組成了一個 Raft Group。按照上面介紹的邏輯,某個 Replica 會被選作 Leader,其餘 Replica 作爲 Follower。

並且,在數據寫入時,TiDB 會儘量保證 Region 不會超過一定的大小,目前這個值是 96M。當然,還是可能會超過這個大小限制。

每個 Region 都可以用 [startKey, endKey) 這樣一個左閉右開的區間來表示。

但不可能讓它無限增長是吧?所以 TiDB 做了一個最大值的限制,當 Region 的大小超過144M(默認) 後,TiKV 會將其分裂成兩個或更多個 Region,以保證數據在各個 Region 中的均勻分佈;同理,當某個 Region 由於短時間刪除了大量的數據之後,會變的比其他 Region 小很多,TiKV 會將比較小的兩個相鄰的 Region 合併

大致的存儲機制、高可用機制上面已經簡單介紹了。

但其實上面還遺留一了比較大的問題。大家可以結合上面的圖思考,一條查詢語句過來,TiDB Server 解析了之後,它是怎麼知道自己要找的數據在哪個 Region 裏?這個 Region 又在哪個 TiKV 上?

難道要遍歷所有的 TiKV 節點?用腳想想都不可能這麼完。剛剛講到多副本,除了要知道提供讀、寫服務的 Leader Replica 所在的 TiKV,還需要知道其餘的 Follower Replica 都分別在哪個實例等等。

6.PD

這就需要引入 PD 了,有了 PD 「存儲相關的細節」那幅圖就會變成這樣:

PD 是個啥?其全名叫 Placement Driver,用於管理整個集羣的元數據,你可以把它當成是整個集羣的控制節點也行。PD 集羣本身也支持高可用,至少由 3 個節點組成。舉個對等的例子應該就好理解了,你可以把 PD 大概理解成 Zookeeper,或者 RocketMQ 裏的 NameServer。Zookeeper 不必多說,NameServer 是負責管理整個 RocketMQ 集羣的元數據的組件。

擔心大家槓,所以特意把大概兩個字加粗了。因爲 PD 不僅僅負責元數據管理,還擔任根據數據分佈狀態進行合理調度的工作。

這個根據數據狀態進行調度,具體是指啥呢?

7.調度

舉個例子,假設每個 Raft Group 需要始終保持 3 個副本,那麼當某個 Raft Group 的 Replica 由於網絡、機器實例等原因不可用了,Replica 數量下降到了 1 個,此時 PD 檢測到了就會進行調度,選擇適當的機器補充 Replica;Replica 補充完後,掉線的又恢復了就會導致 Raft Group 數量多於預期,此時 PD 會合理的刪除掉多餘的副本。

一句話概括上面描述的特性:PD 會讓任何時候集羣內的 Raft Group 副本數量保持預期值

這個可以參考 Kubernetes 裏的 Replica Set 概念,我理解是很類似的。

或者,當 TiDB 集羣進行存儲擴容,向存儲集羣新增 TiKV 節點時,PD 會將其他 TiKV 節點上的 Region 遷移到新增的節點上來。

或者,Leader Replica 掛了,PD 會從 Raft Group 的 Replica 中選舉出一個新的 Leader。

再比如,熱點 Region 的情況,並不是所有的 Region 都會被頻繁的訪問到,PD 就需要對這些熱點 Region 進行負載均衡的調度。

總結一下 PD 的調度行爲會發現,就 3 個操作:

  1. 增加一個 Replica
  2. 刪除一個 Replica
  3. 將 Leader 角色在一個 Raft Group 的不同副本之間遷移

瞭解完了調度的操作,我們再整體的理解一下調度的需求,這點 TiDB 的官網有很好的總結,我把它們整理成腦圖供大家參考:

大多數點都還好,只是可能會對「控制負載均衡的速度」有點問題。因爲 TiDB 集羣在進行負載均衡時,會進行 Region 的遷移,可以理解爲跟 Redis 的 Rehash 比較耗時是類似的問題,可能會影響線上的服務

8.心跳

PD 而要做到調度這些決策,必然需要掌控整個集羣的相關數據,比如現在有多少個 TiKV?多少個 Raft Group?每個 Raft Group 的 Leader 在哪裏等等,這些其實都是通過心跳機制來收集的。

在 NameServer 中,所有的 RocketMQ Broker 都會將自己註冊到 NameServer 中,並且定時發送心跳,Broker 中保存的相關數據也會隨心跳一起發送到 NameServer 中,以此來更新集羣的元數據。

PD 和 TiKV 也是類似的操作,TiKV 中有兩個組件會和 PD 交互,分別是:

  1. Raft Group 的 Leader Replica
  2. TiKV 節點本身

PD 通過心跳來收集數據,更新維護整個集羣的元數據,並且在心跳返回時,將對應的「調度指令」返回。

值得注意的是,上圖中每個 TiKV 中 Raft 只連了一條線,實際上一個 TiKV 節點上可能會有多個 Raft Group 的 Leader

Store(即 TiKV 節點本身)心跳會帶上當前節點存儲的相關數據,例如磁盤的使用狀況、Region 的數量等等。通過上報的數據,PD 會維護、更新 TiKV 的狀態,PD 用 5 種狀態來標識 TiKV 的存儲,分別是:

  1. Up:這個懂的都懂,不懂的解釋了也不懂(手動 doge)
  2. Disconnect:超過 20 秒沒有心跳,就會變成該狀態
  3. Down:Disconnect 了 max-store-down-time 的值之後,就會變成 Down,默認 30 分鐘。此時 PD 會在其他 Up 的 TiKV 上補足 Down 掉的節點上的 Region
  4. Offline:通過 PD Control 進行手動下線操作,該 Store 會變爲 Offline 狀態。PD 會將該節點上所有的 Region 搬遷到其他 Store 上去。當所有的 Region 遷移完成後,就會變成 Tomstone 狀態
  5. Tombstone:表示已經涼透了,可以安全的清理掉了。

其官網的圖已經畫的很好了,就不再重新畫了,以下狀態機來源於 TiDB 官網:

image-20220420210115414

Raft Leader 則更多的是上報當前某個 Region 的狀態,比如當前 Leader 的位置、Followers Region 的數量、掉線 Follower 的個數、讀寫速度等,這樣 TiDB Server 層在解析的時候才知道對應的 Leader Region 的位置。

歡迎微信搜索關注【SH的全棧筆記】,如果你覺得這篇文章對你有幫助,還麻煩點個贊關個注分個享留個言

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章