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前端 代碼,作一個分佈式的存儲引擎就能夠了,這個事情想一想仍是蠻簡單的,因此很是樂觀,以爲這個戰略很完美。
1.png-36.5kB
上圖是我在 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 已經足夠健壯,能讓咱們往下一步步去作分佈式的東西。

這個架構大概是這樣:

架構 0.5.png-65.1kB

上層是 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 終於開源了。

TIkv 開源了.png-45.7kB

從上圖能看到,最底層是 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 作基礎架構,這是一個須要去思考的問題。

相關文章
相關標籤/搜索