TiDB 架構的演進和開發哲學

本文來自 CSDN《程序員》2017 年 2 月的封面報道。

對於一個從零開始的數據庫來說:選擇什麼語言,整體架構怎麼做,要不要開源,如何去測試…太多的問題需要去考量。

在本篇文章中,PingCAP 聯合創始人兼 CTO 黃東旭對 TiDB 的開發歷程進行了詳細簡介,爲大家還原 TiDB 的架構演進全過程。

在大約兩年前,我有一次做MySQL分庫分表和中間件的經歷,那時在中間件裏做 sharding,把 16 個節點的 MySQL 擴到 32 節點,差不多要提前一個月做演練,再用一個禮拜來上線。我就在想,能不能有一個數據庫可以讓我們不再想分庫分表這些東西?當時我們也剛剛做完 Codis,覺得分佈式是個比較合適的解決方案。另外我一直在關注學術圈關於分佈式數據庫的最新進展,有看到谷歌在 2013 年發的 Spanner 和 F1 的論文,所以決定乾脆就重新開始寫一個數據庫,從根本上解決 MySQL 擴展性的問題。

而決定之後發現面對的問題非常複雜:選擇什麼語言,整個架構怎麼做,到底要不要開源……做基礎軟件有一個很重要的事情:寫出來並不難,難的是你怎麼保證這個東西寫對了。尤其是對於業務方,他們所有的業務正確性是構建在基礎軟件的正確性上。所以,對於分佈式系統來說,什麼是寫對了,怎麼去測試,這都是很重要的問題。關於這些我想了很久。

一開始總是要起步的。當時就決定冷靜一下,先確定一個目標:解決 MySQL 的問題。MySQL 是單機型數據庫,它沒有辦法做全擴展,我們選擇 MySQL 兼容,首先選擇在協議和語法層面的兼容,因爲已有的社區裏邊很多的海量的測試。第二點是用戶的遷移成本,能讓用戶遷移得很順暢。第三是因爲萬事開頭難,必須得有一個明確的目標,選定一個目標去做,對開發人員來說心理的壓力最小。確定目標以後,我們 3 個人的創始團隊從原來的公司出來,拿了一筆比較大的風險投資,開始正式做這件事情。

兼容 MySQL 最簡單的方案,就是直接用 MySQL。爲了讓這個東西儘快地做起來,我們一開始做了一個最簡單的版本,複用 MySQL前端 代碼,做一個分佈式的存儲引擎就可以了,這個事情想想還是蠻簡單的,所以非常樂觀,覺得這個戰略很完美。

 

上圖是我在 2015 年 4 月份用六個禮拜完成的第一個版本的框架,但是後來沒好意思開源出來,雖然能跑,但是在性能上完全無法接受。我就想這個東西爲什麼這麼慢?一步一步去看每一層,就想動手改,但是發現工程量巨大,比如 MySQL 的 SQL 優化器,  事務模型等等,完全沒有辦法下手。就像這個架構圖裏看到的,因爲在 MySQL Engine 這一層,我們能做的事情太少了,所以就沒有辦法。

第一版實驗到此宣告失敗,現在看起來寫 SQL parser 和優化器等這些已經是繞不開了,我們索性決定從頭開始寫,唯一給我安慰的就是終於可以使用我們最愛的編程語言了,就是 Go。

我們跟其他做這種軟件的工程師的思路相反,選擇了從上往下寫,先寫最頂層的 SQL 的接口 SQL Layer,我要保證這個東西長得跟 MySQL 一模一樣,包括網絡協議和語法層。從 TiDB 網絡協議、SQL 的語法解析器、到 SQL 的優化器、執行器等基本從上到下寫了一遍。這個階段持續了大概三個月左右。從這個階段開始,我們慢慢摸索出了幾個實踐中深有體會的開發哲學。

 

♣ 第一,所有計算機科學裏面的問題都可以把它不停地抽象,抽象到另外一個層次上去解決。

我們完成了架構 0.2,此時 TiDB 只有一個 SQL 解析器,完全不能存數據,因爲底下的存儲引擎根本沒有實現。我想要保證這個數據庫是對的,要先保證 SQL Layer 是對的,讓它可以完整的跑 MySQL 的 test。至於底下的存儲我可以實現個假的或者內存裏先存着,先保證我的 SQL 正常運轉起來就可以了。

其實在苦哈哈的寫 TiDB 的 SQL Parser 的時候我們還做了很多事情,不管是 MySQL 的 unittests,SQL logic tests,ORM tests 等,把它的測試全都收集下來,到現在大概有一千萬個集成測試用例。我們還做了一個事情,就是把存儲引擎這個概念抽象成很薄的幾個接口,使得它去接入一個 KV engine。絕大多數的 KV engine 非常多,比如 LevelDB,RocksDB,接口的語義都是非常明確的。

幾個月過去了,團隊也大約有了十幾個人,因爲在每一層我都非常嚴格地要求我們團隊用接口來劃分,使得每一個層次上的工作都是可以並行,這對於整個項目的推進是非常有利的事情。

大概去年九月份,歷史上第一個不能用來存數據的數據庫——沒有存儲引擎的第一版 TiDB 開源了,放在 HackerNews 非常受歡迎,還被推薦到了首頁。

♣ 第二,Talk is cheap,show me the tests。

做基礎軟件 test 是比 code 更重要的事情。比如你提了一個 Feature,我到底是合併還是不合,不能直接判斷,需要看到你的 test。我們現在在 GitHub 上運營 TiDB,一個新的提交如果讓整個項目的代碼測試的覆蓋率下降了,我是不允許你的代碼合併到主幹分支的,非常嚴格。構建一個數據庫最難的並不是把它寫出來,而是證明它是對的,尤其是分佈式系統的測試要比單機的測試要更加困難。因爲在分佈式系統裏面每一個節點都可能 crash,每一個網絡的延遲可能是飄忽不定的,各種各樣的異常情況都會發生。我們在做整個數據庫的時候,第一步是完成 SQL Layer,第二步是把每個 IO,每個集羣的節點交互行爲全都抽象成爲一個接口,使得我們可以回放整個包括 TCP/IP 包的接收順序。一旦發現 bug,就把它重放到單元測試裏面重現。不管是新的開發者或者新的模塊加入,是無法相信“人”的,只相信機器。我只相信 strong test 才能不斷的保證項目在可以控制的範圍之內。

後來做了一個架構 0.5,因爲已經有了 SQL 層,SQL 層跟存儲層基本上做了完全分離,終於可以像最初的 0.1 那樣,我可以接一個分佈式引擎上去,當時我們接了 HBase。 HBase 是階段性的戰略選型,因爲我們想既然我的 SQL Layer 寫的足夠穩定,那麼我們先接一個分佈式的引擎上去,但是我又不能在架構中引入太多不確定的變量,於是就挑選了一個在市面上能找到的,我認爲最穩定的分佈式引擎,先接上去看整個系統到底能不能跑起來。結果還可以,能夠跑起來,但是我們的要求會更高,所以之後我們就把 HBase 扔掉了。接 HBase 這個事情標誌着我們上層 SQL Layer 跟我們接口的抽象已經足夠穩定,我們的 test 已經足夠健壯,能讓我們往下一步步去做分佈式的東西。

這個架構大概是這樣:

 

上層是 MySQL 業務層 Client,你可以用任意的 MySQL 的客戶端去連接它,如果數據量大的話,你不需要再去分庫分表,就把它當成無窮大的 MySQL 用就好,這個用戶體驗很好。因爲 TiDB 是一個無狀態的設計,它並不存儲數據,所以你可以部署無數多個 TiDB 負載均衡。底層一開始是個 HBase,那時候是 11 月,至此距離創業半年過去了。

因爲有海量的 Test 保證,讓整個設計的過程沒有太過困難。不過這裏涉及一個問題:我們在做技術選型的時候,如果在有很大自由度的前提下,怎麼去控制發揮慾望和膨脹的野心?你的敵人並不是預算,而是複雜度。你怎麼控制每一層的複雜度是非常重要的,特別是對於一個架構師來說,所有的工作都是在去規避複雜度,提升開發效率和穩定性。

當時我們選擇了一個非常小衆的編程語言就是 Rust。首先它是一個 high performance 的編程語言,它沒有 GC ,也沒有 runtime,很多的創新是做在了編譯器這一層,最大的特點就是安全、安全和安全,我認爲它是更現代的 C++,不過 C++ 最大的問題是如果用不熟容易把自己的手腳砍斷。當時我選 Rust 以後,很多朋友問我你爲什麼去選擇它。說實話最開始我是很怕的,因爲這個語言畢竟是一個新的比較小衆的語言,community 也沒那麼大,但這是當時對於我們團隊的狀況來說是最優選擇。我們就用 Rust 寫起來,結果一不小心 TiKV 成爲了 Rust 社區最大的開源項目之一。因爲我們在 Rust 很早期的時候就開始用了,Rust 官方也一直在找我們來分享 Rust 的使用經驗,我們也很熱心的去擁抱 Rust community。 Rust 社區每週的 weekly 裏面有一個固定的專欄叫做 “This week in TiKV” ,就是爲我們打造的 :)

2015 年的冬天我們是在糾結中度過的。一是用最新的編程語言 Rust, 大家之前都沒有接觸過;第二就是,我們想要的“彈性擴展、真正的高可用、高性能、強一致”這四點要求,每一個都非常困難。

怎麼辦?只能去擁抱社區,不要自己去做所有的事情,一是人數有限,第二是複用是個很好的習慣,既然別人都已經幹過這些事情,就不要再去重複性的工作。我們要做一個真正高可用的數據庫,把高可用的分佈式存儲找了一圈發現 Etcd,Etcd 背後算法叫 Raft這是個一致性算法等價於 Paxos。這個算法目前來說最穩定地實現就是 Etcd 裏的 Raft。而且 Etcd 是真正在生產環境中被大量認證過的 Raft 的實現。我仔細看過 Etcd 的源碼,每個狀態的切換都抽象成接口,我們測試是可以脫離整個網絡、脫離整個 IO、脫離整個硬件的環境去構建的。我覺得這個思路非常贊,這也是爲什麼 CoreOS 的 Etcd  包括像 k8s 背後的元信息存儲也用的是它,質量非常高,性能非常好。但是 Etcd 有一個問題是它是 GO 寫的,我們已經決定去用 Rust 開發底層存儲的數據庫。如果用類似 paxos 這種算法,我不相信除了 Google Chubby 以外的公司有能力把它寫對。但是 Raft 不一樣,雖然它也很難,但是畢竟它是可以實現的東西,所以我們爲了它的質量,加速我們開發的進度,我們做了一件比較瘋狂的事情,就是我們把 Etcd 的 Raft 狀態機的每一行代碼,line by line 的翻譯成了 Rust。而我們第一個轉的就是所有 Etcd 本身的測試用例。我們寫一模一樣的 test ,保證這個東西我們 port 的過程是沒有問題的。

TiDB 底層的存儲引擎一開始是不能存數據的,那現在是時候要選一個真正的 Storage engine,我們覺得這個事情是一個巨坑。本地存儲引擎讓一個小團隊去寫的話基本不現實,我們就從最底層選擇了 RocksDB。RocksDB 大家可以認爲是一個單機的 key-value engine,前身其實是 LevelDB,是 Google 在 2011 年左右開源的 key-value 的存儲引擎。 RocksDB 的背後結構是 LSM Tree,是一個對寫非常友好、同時在你的機器內存比較大的時候它的讀性能會非常好的數據結構 。存儲引擎還有一個很重要的工作就是,需要根據你機器的性能去做針對性的調優,大家會看到像 MySQL 調優都快變成黑魔法一樣的東西,RocksDB 也是一個調優能寫本書的存在。大家可以看到,新一代的分佈式數據庫存儲引擎大家都會選擇 RocksDB,我覺得這是大勢所趨。

從 15 年的冬天開始,我們苦逼哈哈的寫了 5 個月的代碼,用 Rust 去寫,到 2016 年 4 月 1 日 TiKV 終於開源了。

 

 

從上圖能看到,最底層是 RocksDB ,上面的分佈式這一層是用 Raft,這兩層雖然是我們寫的,但是質量上是我們社區的盟友幫我們保證的。在 Raft 之上是 MVCC ,從這裏往上,就都是我們自己來寫的了。所以 TiKV 終於是一個可以實現彈性擴展、支持 ACID 事務、全局一致性,跨數據中心高可用的存儲引擎了,而且性能還非常的棒。因爲我沒有在底下去接一個像 HDFS 的文件系統。

其實從開源到現在,我們一直在做 TiKV 的性能調優、穩定性等很多的工作,但是從架構上來看,這個架構我覺得至少在未來的五年之內,不會再有很大的變化。我一直在強調的點就是:複雜性纔是你最大的敵人,我寧可是以不變應萬變的姿勢去應對未來突變的需求。還好,數據庫這個東西的需求變化也沒太多。

♣ 第三,Where there’s a metric there’s a way。

再說說 Metrices。對架構師而言一個很重要的工作就是查看系統中有哪些 block 的點,挨個解決掉這些問題。我們發現在數據庫領域,有很多很多點如果能予以解決的話,性能會上去十倍。我有一個觀點,所有的東西,只要有 Metrices,能被監控,這個東西就能被解決。 也就是:“Where there’s a metric there’s a way”。 一旦能重複觀察性能的平衡點,性能問題是最好解決的問題,但是寫對是最難的問題。

一般來說大家都在公司內部自己去審 Metrics,還有監控工具。對於我們小團隊來說,或者說是一個擁抱社區的團隊來說,這基本上是一個得不償失的事情。因爲你費好多勁去寫一個,還不如社區裏面寫得好,這很麻煩。所以,我們在數據庫裏面內嵌了 Prometheus 和 Grafana。Prometheus 現在在硅谷太火了,它其實是一個分佈式的時序數據庫,但是它很適用於日誌蒐集和性能調優,它做得更完美的地方是它提供 DSL 去用於查詢提供監控的報警,包括你可以寫報警的規則。它沒有一個好看的 Dashboard,這時社區裏面另外一個哥們就出來,說我要去給你做一個很好看的界面,這個項目就是 Grafana。Grafana 是一個可視化的 dashboard ,它能讓每一次 dashboard 排布的位置、類型、樣式、大小、寬度都是可以自定義的。而且整個 Metrics  收集 Prometheus 提供了兩種模式,一種是 push 的模式,一種是 pull 的模式。 對於收集監控的代碼性能影響很小。

最後想補充的問題,第一就是工具。對於一個互聯網出身的團隊來說,其實工具是我們非常重視的一個點,可以最小化業務的遷移成本。我覺得很多在大公司做重構,或者做基礎軟件的工程師,最好的方式就是潤物細無聲。你完全不知道我在做底層的重構就莫名地重構完了,這是最完美的狀態。比如我之前做 Codis,我的要求是如果用戶現在在用 Twemproxy ,他遷到新的方案上必須要一行代碼都沒有改,你甚至完全不知道我在做遷移,這纔是最好的,我認爲這應該是所有做基礎設施團隊的自我修養。

第二就是不要意外。比如說,你在做一個數據庫,你號稱跟 MySQL一模一樣,那麼你展現出來任何跟 MySQL 不一樣的東西都會讓用戶嚇一跳,而且這是很重要的一個開發原則。

第三就是悲觀預設。永遠都會有各種各樣的噁心事情和異常的狀況發生,其實這是作爲分佈式系統開發工程師每天都要對自己說的話。業務的數據是重於泰山的,但是任何的基礎設施都是會掛的,你的網線可能會斷,整個數據中心可能會 shut down……你要預設你的數據庫是一定會丟數據的,這個時候你的數據庫設計纔會更好。如何保護自己如何保護業務,我們做了一些神奇的工具,比如像 syncer,這個東西就是把 TiDB cluster 作爲一個 假的 slave 接到 MySQL 上,業務在上面跑 MySQL ,後面存起來其實是個集羣。另外還做了一個比較變態的事情,就是反向。我們頂在業務上面,下面可以接到 MySQL。MySQL 可以做 TiDB 的 slave,TiDB 可以做 MySQL 的 slave。這個功能對於業務來說是非常驚喜的。有的客戶一開始想用 TiDB但是有點害怕,我說沒關係,你在後面接個從,然後你在從上面去查詢。比如你原來一個 SQL 跑了 20 分鐘,現在我能讓你跑到 10 秒以內;或者你跑了大概半年都一點數據沒丟,系統非常穩定,再切成主的業務代碼中更改。

一個架構師總是要去想一些未來十年會發生什麼。有一個名詞 Cloud-Native,我是認爲一切的東西在未來都會跑在雲端,如何針對雲上這個環境去設計基礎軟件?數據庫設計一個很重要的原則是,數據一旦發生了宕機它能夠自動修復和均衡數據,人在裏邊給這些集羣加機器就行了,整個集羣一定要能夠有自己的思考。未來怎麼針對 Cloud 做基礎架構,這是一個需要去思考的問題。

 

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