簡單瞭解 TiDB 架構

detectiveHLH發表於2022-04-25

一、前言

大家如果看過我之前發過的文章就知道,我寫過很多篇關於 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的全棧筆記】,如果你覺得這篇文章對你有幫助,還麻煩點個贊關個注分個享留個言

相關文章