在技術(shù)方面,我自己熱衷于 Open Source,寫(xiě)了很多 Open Source 的東西,擅長(zhǎng)的是 Infrastructure 領(lǐng)域。Infrastructure 領(lǐng)域現(xiàn)在范圍很廣,比如說(shuō)很典型的分布式 Scheduler、Mesos、Kubernetes,另外它和 Microservices 所結(jié)合的東西也特別多。Infrastructure 領(lǐng)域還有比如 Database 有分 AP(分析型)和 TP(事務(wù)型),比如說(shuō)很典型的大家知道的 Spark、Greenplum、Apache Phoenix 等等,這些都屬于在 AP 的,它們也會(huì)去嘗試支持有限的 TP。另外,還有一個(gè)比較有意思的就是 Kudu——Cloudera Open Source 的那個(gè)項(xiàng)目,它的目標(biāo)很有意思:我不做最強(qiáng)的 AP 系統(tǒng),也不做最強(qiáng)的 TP 系統(tǒng),我選擇一個(gè)相對(duì)折中的方案。從文化哲學(xué)上看,它比較符合中國(guó)的中庸思想。

另外,我先后創(chuàng)建了 Codis、TiDB。去年12月份創(chuàng)建了 TiKV 這個(gè) project,TiKV 在所有的 rust 項(xiàng)目里目前排名前三。

首先我們聊聊 Database 的歷史,在已經(jīng)有這么多種數(shù)據(jù)庫(kù)的背景下我們?yōu)槭裁匆獎(jiǎng)?chuàng)建另外一個(gè)數(shù)據(jù)庫(kù);以及說(shuō)一下現(xiàn)在方案遇到的困境,說(shuō)一下 Google Spanner 和 F1、TiKV 和 TiDB,說(shuō)一下架構(gòu)的事情,在這里我們會(huì)重點(diǎn)聊一下 TiKV。因?yàn)槲覀儺a(chǎn)品的很多特性是 TiKV 提供的,比如說(shuō)跨數(shù)據(jù)中心的復(fù)制、Transaction、auto-scale。

接下來(lái)聊一下為什么 TiKV 用 Raft 能實(shí)現(xiàn)所有這些重要的特性,以及 scale、MVCC 和事務(wù)模型。東西非常多,我今天不太可能把里面的技術(shù)細(xì)節(jié)都描述得特別細(xì),因?yàn)閹缀趺恳粋€(gè)話題都可以找到一篇或者是多篇論文,所以詳細(xì)的技術(shù)問(wèn)題大家可以單獨(dú)來(lái)找我聊。

后面再說(shuō)一下我們現(xiàn)在遇到的窘境,就是大家常規(guī)遇到的分布式方案有哪些問(wèn)題,比如 MySQL Sharding。我們創(chuàng)建了無(wú)數(shù) MySQL Proxy,比如官方的 MySQL proxy、Youtube 的 Vitess、淘寶的 Cobar、TDDL以及基于 Cobar 的 MyCAT、金山的 Kingshard、360 的 Atlas、京東的 JProxy,我在豌豆莢也寫(xiě)了一個(gè)??梢哉f(shuō),隨便一個(gè)大公司都會(huì)造一個(gè) MySQL Sharding 的方案。

為什么我們要?jiǎng)?chuàng)建另外一個(gè)數(shù)據(jù)庫(kù)?

昨天晚上我還跟一個(gè)同學(xué)聊到,基于 MySQL 的方案它的天花板在哪里,它的天花板特別明顯。有一個(gè)思路是能不能通過(guò) MySQL 的 server 把 InnoDB 變成一個(gè)分布式數(shù)據(jù)庫(kù),聽(tīng)起來(lái)這個(gè)方案很完美,但是很快就會(huì)遇到天花板。因?yàn)?MySQL 生成的執(zhí)行計(jì)劃是個(gè)單機(jī)的,它認(rèn)為整個(gè)計(jì)劃的 cost 也是單機(jī)的,我讀取一行和讀取下一行之間的開(kāi)銷(xiāo)是很小的,比如迭代 next row 可以立刻拿到下一行。實(shí)際上在一個(gè)分布式系統(tǒng)里面,這是不一定的。

另外,你把數(shù)據(jù)都拿回來(lái)計(jì)算這個(gè)太慢了,很多時(shí)候我們需要把我們的 expression 或者計(jì)算過(guò)程等等運(yùn)算推下去,向上返回一個(gè)最終的計(jì)算結(jié)果,這個(gè)一定要用分布式的 plan,前面控制執(zhí)行計(jì)劃的節(jié)點(diǎn),它必須要理解下面是分布式的東西,才能生成最好的 plan,這樣才能實(shí)現(xiàn)最高的執(zhí)行效率。

比如說(shuō)你做一個(gè) sum,你是一條條拿回來(lái)加,還是讓一堆機(jī)器一起算,最后給我一個(gè)結(jié)果。 例如我有 100 億條數(shù)據(jù)分布在 10 臺(tái)機(jī)器上,并行在這 10臺(tái)機(jī)器我可能只拿到 10 個(gè)結(jié)果,如果把所有的數(shù)據(jù)每一條都拿回來(lái),這就太慢了,完全喪失了分布式的價(jià)值。聊到 MySQL 想實(shí)現(xiàn)分布式,另外一個(gè)實(shí)現(xiàn)分布式的方案就是 Proxy。但是 Proxy 本身的天花板在那里,就是它不支持分布式的 transaction,它不支持跨節(jié)點(diǎn)的 join,它無(wú)法理解復(fù)雜的 plan,一個(gè)復(fù)雜的 plan 打到 Proxy 上面,Proxy 就傻了,我到底應(yīng)該往哪一個(gè)節(jié)點(diǎn)上轉(zhuǎn)發(fā)呢,如果我涉及到 subquery sql 怎么辦?所以這個(gè)天花板是瞬間會(huì)到,在傳統(tǒng)模型下面的修改,很快會(huì)達(dá)不到我們的要求。

另外一個(gè)很重要的是,MySQL 支持的復(fù)制方式是半同步或者是異步,但是半同步可以降級(jí)成異步,也就是說(shuō)任何時(shí)候數(shù)據(jù)出了問(wèn)題你不敢切換,因?yàn)橛锌赡苁钱惒綇?fù)制,有一部分?jǐn)?shù)據(jù)還沒(méi)有同步過(guò)來(lái),這時(shí)候切換數(shù)據(jù)就不一致了。前一陣子出現(xiàn)過(guò)某公司突然不能支付了這種事件,今年有很多這種類(lèi)似的 case,所以微博上大家都在說(shuō)“說(shuō)好的異地多活呢?”……

為什么傳統(tǒng)的方案在這上面解決起來(lái)特別的困難,天花板馬上到了,基本上不可能解決這個(gè)問(wèn)題。另外是多數(shù)據(jù)中心的復(fù)制和數(shù)據(jù)中心的容災(zāi),MySQL 在這上面是做不好的。

在前面三十年基本上是關(guān)系數(shù)據(jù)庫(kù)的時(shí)代,那個(gè)時(shí)代創(chuàng)建了很多偉大的公司,比如說(shuō) IBM、Oracle、微軟也有自己的數(shù)據(jù)庫(kù),早期還有一個(gè)公司叫 Sybase,有一部分特別老的程序員同學(xué)在當(dāng)年的教程里面還可以找到這些東西,但是現(xiàn)在基本上看不到了。

另外是 NoSQL。NoSQL 也是一度非?;?,像 Cassandra、MongoDB 等等,這些都屬于在互聯(lián)網(wǎng)快速發(fā)展的時(shí)候創(chuàng)建這些能夠 scale 的方案,但 Redis scale 出來(lái)比較晚,所以很多時(shí)候大家把 Redis 當(dāng)成一個(gè) Cache,現(xiàn)在慢慢大家把它當(dāng)成存儲(chǔ)不那么重要的數(shù)據(jù)的數(shù)據(jù)庫(kù)。因?yàn)樗辛?scale 支持以后,大家會(huì)把更多的數(shù)據(jù)放在里面。

然后到了 2015,嚴(yán)格來(lái)講是到 2014 年到 2015 年之間,Raft 論文發(fā)表以后,真正的 NewSQL 的理論基礎(chǔ)終于完成了。我覺(jué)得 NewSQL 這個(gè)理論基礎(chǔ),最重要的劃時(shí)代的幾篇論文,一個(gè)是谷歌的 Spanner,是在 2013 年初發(fā)布的;再就是 Raft 是在 2014 年上半年發(fā)布的。這幾篇相當(dāng)于打下了分布式數(shù)據(jù)庫(kù) NewSQL 的理論基礎(chǔ),這個(gè)模型是非常重要的,如果沒(méi)有模型在上面是堆不起來(lái)東西的。說(shuō)到現(xiàn)在,大家可能對(duì)于模型還是可以理解的,但是對(duì)于它的實(shí)現(xiàn)難度很難想象。

前面我大概提到了我們?yōu)槭裁葱枰硗庖粋€(gè)數(shù)據(jù)庫(kù),說(shuō)到 Scalability 數(shù)據(jù)的伸縮,然后我們講到需要 SQL,比如你給我一個(gè)純粹的 key-velue 系統(tǒng)的 API,比如我要查找年齡在 10 歲到 20 歲之間的 email 要滿足一個(gè)什么要求的。如果只有 KV 的 API 這是會(huì)寫(xiě)死人的,要寫(xiě)很多代碼,但是實(shí)際上用 SQL 寫(xiě)一句話就可以了,而且 SQL 的優(yōu)化器對(duì)整個(gè)數(shù)據(jù)的分布是知道的,它可以很快理解你這個(gè) SQL,然后會(huì)得到一個(gè)最優(yōu)的 plan,他得到這個(gè)最優(yōu)的 plan 基本上等價(jià)于一個(gè)真正理解 KV 每一步操作的人寫(xiě)出來(lái)的程序。通常情況下,SQL 的優(yōu)化器是為了更加了解或者做出更好的選擇。

另外一個(gè)就是 ACID 的事務(wù),這是傳統(tǒng)數(shù)據(jù)庫(kù)必須要提供的基礎(chǔ)。以前你不提供 ACID 就不能叫數(shù)據(jù)庫(kù),但是近些年大家寫(xiě)一個(gè)內(nèi)存的 map 也可以叫自己是數(shù)據(jù)庫(kù)。大家寫(xiě)一個(gè) append-only 文件,我們也可以叫只讀數(shù)據(jù)庫(kù),數(shù)據(jù)庫(kù)的概念比以前極大的泛化了。

另外就是高可用和自動(dòng)恢復(fù),他們的概念是什么呢?有些人會(huì)有一些誤解,因?yàn)榻裉爝€有朋友在現(xiàn)場(chǎng)問(wèn)到,出了故障,比如說(shuō)一個(gè)機(jī)房掛掉以后我應(yīng)該怎么做切換,怎么操作。這個(gè)實(shí)際上相當(dāng)于還是上一代的概念,還需要人去干預(yù),這種不算是高可用。

未來(lái)的高可用一定是系統(tǒng)出了問(wèn)題馬上可以自動(dòng)恢復(fù),馬上可以變成可用。比如說(shuō)一個(gè)機(jī)房掛掉了,十秒鐘不能支付,十秒鐘之后系統(tǒng)自動(dòng)恢復(fù)了變得可以支付,即使這個(gè)數(shù)據(jù)中心再也不起來(lái)我整個(gè)系統(tǒng)仍然是可以支付的。Auto-Failover 的重要性就在這里。大家不希望在睡覺(jué)的時(shí)候被一個(gè)報(bào)警給拉起來(lái),我相信大家以后具備這樣一個(gè)能力,5 分鐘以內(nèi)的報(bào)警不用理會(huì),掛掉一個(gè)機(jī)房,又掛掉一個(gè)機(jī)房,這種連續(xù)報(bào)警才會(huì)理。我們內(nèi)部開(kāi)玩笑說(shuō),希望大家都能睡個(gè)好覺(jué),很重要的事情就是這個(gè)。

說(shuō)完應(yīng)用層的事情,現(xiàn)在很有很多業(yè)務(wù),在應(yīng)用層自己去分片,比如說(shuō)我按照 user ID在代碼里面分片,還有一部分是更高級(jí)一點(diǎn)我會(huì)用到一致性哈希。問(wèn)題在于它的復(fù)雜度,到一定程度之后我自動(dòng)的分庫(kù),自動(dòng)的分表,我覺(jué)得下一代數(shù)據(jù)庫(kù)是不需要理解這些東西的,不需要了解什么叫做分庫(kù),不需要了解什么叫做分表,因?yàn)橄到y(tǒng)是全部自動(dòng)搞定的。同時(shí)復(fù)雜度,如果一個(gè)應(yīng)用不支持事務(wù),那么在應(yīng)用層去做,通常的做法是引入一個(gè)外部隊(duì)列,引入大量的程序機(jī)制和狀態(tài)轉(zhuǎn)換,A 狀態(tài)的時(shí)候允許轉(zhuǎn)換到 B 狀態(tài),B 狀態(tài)允許轉(zhuǎn)換到 C 狀態(tài)。

舉一個(gè)簡(jiǎn)單的例子,比如說(shuō)在京東上買(mǎi)東西,先下訂單,支付狀態(tài)之后這個(gè)商品才能出庫(kù),如果不是支付狀態(tài)一定不能出庫(kù),每一步都有嚴(yán)格的流程。

Google Spanner / F1

說(shuō)一下 Google 的 Spanner 和 F1,這是我非常喜歡的論文,也是我最近幾年看過(guò)很多遍的論文。 Google Spanner 已經(jīng)強(qiáng)大到什么程度呢?Google Spanner 是全球分布的數(shù)據(jù)庫(kù),在國(guó)內(nèi)目前普遍做法叫做同城兩地三中心,它們的差別是什么呢?以 Google 的數(shù)據(jù)來(lái)講,谷歌比較高的級(jí)別是他們有 7 個(gè)副本,通常是美國(guó)保存 3 個(gè)副本,再在另外 2 個(gè)國(guó)家可以保存 2 個(gè)副本,這樣的好處是萬(wàn)一美國(guó)兩個(gè)數(shù)據(jù)中心出了問(wèn)題,那整個(gè)系統(tǒng)還能繼續(xù)可用,這個(gè)概念就是比如美國(guó) 3 個(gè)副本全掛了,整個(gè)數(shù)據(jù)都還在,這個(gè)數(shù)據(jù)安全級(jí)別比很多國(guó)家的安全級(jí)別還要高,這是 Google 目前做到的,這是全球分布的好處。

現(xiàn)在國(guó)內(nèi)主流的做法是兩地三中心,但現(xiàn)在基本上都不能自動(dòng)切換。大家可以看到很多號(hào)稱(chēng)實(shí)現(xiàn)了兩地三中心或者異地多活,但是一出現(xiàn)問(wèn)題都說(shuō)不好意思這段時(shí)間我不能提供服務(wù)了。大家無(wú)數(shù)次的見(jiàn)到這種 case, 我就不列舉了。

Spanner 現(xiàn)在也提供一部分 SQL 特性。在以前,大部分 SQL 特性是在 F1 里面提供的,現(xiàn)在 Spanner 也在逐步豐富它的功能,Google 是全球第一個(gè)做到這個(gè)規(guī)?;蛘呤亲龅竭@個(gè)級(jí)別的數(shù)據(jù)庫(kù)。事務(wù)支持里面 Google 有點(diǎn)黑科技(其實(shí)也沒(méi)有那么黑),就是它有GPS 時(shí)鐘和原子鐘。大家知道在分布式系統(tǒng)里面,比如說(shuō)數(shù)千臺(tái)機(jī)器,兩個(gè)事務(wù)啟動(dòng)先后順序,這個(gè)順序怎么界定(事務(wù)外部一致性)。這個(gè)時(shí)候 Google 內(nèi)部使用了 GPS 時(shí)鐘和原子鐘,正常情況下它會(huì)使用一個(gè)GPS 時(shí)鐘的一個(gè)集群,就是說(shuō)我拿的一個(gè)時(shí)間戳,并不是從一個(gè) GPS 上來(lái)拿的時(shí)間戳,因?yàn)榇蠹抑浪械挠布紩?huì)有誤差。如果這時(shí)候我從一個(gè)上拿到的 GPS 本身有點(diǎn)問(wèn)題,那么你拿到的這個(gè)時(shí)鐘是不精確的。而 Google 它實(shí)際上是在一批 GPS 時(shí)鐘上去拿了能夠滿足 majority 的精度,再用時(shí)間的算法,得到一個(gè)比較精確的時(shí)間。大家知道 GPS 也不太安全,因?yàn)樗敲绹?guó)軍方的,對(duì)于 Google 來(lái)講要實(shí)現(xiàn)比國(guó)家安全級(jí)別更高的數(shù)據(jù)庫(kù),而 GPS 是可能受到干擾的,因?yàn)?GPS 信號(hào)是可以調(diào)整的,這在軍事用途上面很典型的,大家知道導(dǎo)彈的制導(dǎo)需要依賴 GPS,如果調(diào)整了 GPS 精度,那么導(dǎo)彈精度就廢了。所以他們還用原子鐘去校正 GPS,如果 GPS 突然跳躍了,原子鐘上是可以檢測(cè)到 GPS 跳躍的,這部分相對(duì)有一點(diǎn)黑科技,但是從原理上來(lái)講還是比較簡(jiǎn)單,比較好理解的。

最開(kāi)始它 Spanner 最大的用戶就是 Google 的 Adwords,這是 Google 最賺錢(qián)的業(yè)務(wù),Google 就是靠廣告生存的,我們一直覺(jué)得 Google 是科技公司,但是他的錢(qián)是從廣告那來(lái)的,所以一定程度來(lái)講 Google 是一個(gè)廣告公司。Google 內(nèi)部的方向先有了 Big table ,然后有了 MegaStore ,MegaStore 的下一代是 Spanner ,F(xiàn)1 是在 Spanner 上面構(gòu)建的。

TiDB and TiKV

TiKV 和 TiDB 基本上對(duì)應(yīng) Google Spanner 和 Google F1,用 Open Source 方式重建。目前這兩個(gè)項(xiàng)目都開(kāi)放在 GitHub 上面,兩個(gè)項(xiàng)目都比較火爆,TiDB 是更早一點(diǎn)開(kāi)源的, 目前 TiDB 在 GitHub 上 有 4300 多個(gè) Star,每天都在增長(zhǎng)。

另外,對(duì)于現(xiàn)在的社會(huì)來(lái)講,我們覺(jué)得 Infrastructure 領(lǐng)域閉源的東西是沒(méi)有任何生存機(jī)會(huì)的。沒(méi)有任何一家公司,愿意把自己的身家性命壓在一個(gè)閉源的項(xiàng)目上。舉一個(gè)很典型的例子,在美國(guó)有一個(gè)數(shù)據(jù)庫(kù)叫 FoundationDB,去年被蘋(píng)果收購(gòu)了。FoundationDB 之前和用戶簽的合約都是一年的合約。比如說(shuō),我給你服務(wù)周期是一年,現(xiàn)在我被另外一個(gè)公司收購(gòu)了,我今年服務(wù)到期之后,我是滿足合約的。但是其他公司再也不能找它服務(wù)了,因?yàn)樗F(xiàn)在不叫 FoundationDB 了,它叫 Apple了,你不能找 Apple 給你提供一個(gè) Enterprise service。

TiDB 和 TiKV 為什么是兩個(gè)項(xiàng)目,因?yàn)樗?Google 的內(nèi)部架構(gòu)對(duì)比差不多是這樣的:TiKV 對(duì)應(yīng)的是 Spanner,TiDB 對(duì)應(yīng)的是 F1 。F1 里面更強(qiáng)調(diào)上層的分布式的 SQL 層到底怎么做,分布式的 Plan 應(yīng)該怎么做,分布式的 Plan 應(yīng)該怎么去做優(yōu)化。同時(shí) TiDB 有一點(diǎn)做的比較好的是,它兼容了 MySQL 協(xié)議,當(dāng)你出現(xiàn)了一個(gè)新型的數(shù)據(jù)庫(kù)的時(shí)候,用戶使用它是有成本的。大家都知道作為開(kāi)發(fā)很討厭的一個(gè)事情就是,我要每個(gè)語(yǔ)言都寫(xiě)一個(gè) Driver,比如說(shuō)你要支持 C++、你要支持 Java、你要支持 Go 等等,這個(gè)太累了,而且用戶還得改他的程序,所以我們選擇了一個(gè)更加好的東西兼容 MySQL 協(xié)議,讓用戶可以不用改。一會(huì)我會(huì)用一個(gè)視頻來(lái)演示一下,為什么一行代碼不改就可以用,用戶就能體會(huì)到 TiDB 帶來(lái)的所有的好處。

這個(gè)圖實(shí)際上是整個(gè)協(xié)議?;蛘呤钦麄€(gè)軟件棧的實(shí)現(xiàn)。大家可以看到整個(gè)系統(tǒng)是高度分層的,從最底下開(kāi)始是 RocksDB ,然后再上面用 Raft 構(gòu)建一層可以被復(fù)制的 RocksDB ,在這一層的時(shí)候它還沒(méi)有 Transaction,但是整個(gè)系統(tǒng)現(xiàn)在的狀態(tài)是所有寫(xiě)入的數(shù)據(jù)一定要保證它復(fù)制到了足夠多的副本。也就是說(shuō)只要我寫(xiě)進(jìn)來(lái)的數(shù)據(jù)一定有足夠多的副本去 cover 它,這樣才比較安全,在一個(gè)比較安全的 Key-value store 上面, 再去構(gòu)建它的多版本,再去構(gòu)建它的分布式事務(wù),然后在分布式事務(wù)構(gòu)建完成之后,就可以輕松的加上 SQL 層,再輕松的加上MySQL 協(xié)議的支持。然后,這兩天我比較好奇,自己寫(xiě)了 MongoDB 協(xié)議的支持,然后我們可以用 MongoDB 的客戶端來(lái)玩,就是說(shuō)協(xié)議這一層是高度可插拔的。TiDB 上可以在上面構(gòu)建一個(gè) MongoDB 的協(xié)議,相當(dāng)于這個(gè)是構(gòu)建一個(gè) SQL 的協(xié)議,可以構(gòu)建一個(gè) NoSQL 的協(xié)議。這一點(diǎn)主要是用來(lái)驗(yàn)證 TiKV 在模型上面的支持能力。

這是整個(gè) TiKV 的架構(gòu)圖,從這個(gè)看來(lái),整個(gè)集群里面有很多 Node,比如這里畫(huà)了四個(gè) Node ,分別對(duì)應(yīng)了四個(gè)機(jī)器。每一個(gè) Node 上可以有多個(gè) Store,每個(gè) Store 里面又會(huì)有很多小的 Region,就是說(shuō)一小片數(shù)據(jù),就是一個(gè) Region 。從全局來(lái)看所有的數(shù)據(jù)被劃分成很多小片,每個(gè)小片默認(rèn)配置是 64M,它已經(jīng)足夠小,可以很輕松的從一個(gè)節(jié)點(diǎn)移到另外一個(gè)節(jié)點(diǎn),Region 1 有三個(gè)副本,它分別在 Node1、Node 2 和 Node4 上面, 類(lèi)似的Region 2,Region 3 也是有三個(gè)副本。每個(gè) Region 的所有副本組成一個(gè) Raft Group,整個(gè)系統(tǒng)可以看到很多這樣的 Raft groups。

Raft 細(xì)節(jié)我不展開(kāi)了,大家有興趣可以找我私聊或者看一下相應(yīng)的資料。

因?yàn)檎麄€(gè)系統(tǒng)里面我們可以看到上一張圖里面有很多 Raft group 給我們,不同 Raft group 之間的通訊都是有開(kāi)銷(xiāo)的。所以我們有一個(gè)類(lèi)似于 MySQL 的 group commit 機(jī)制 ,你發(fā)消息的時(shí)候?qū)嶋H上可以 share 同一個(gè) connection , 然后 pipeline + batch 發(fā)送,很大程度上可以省掉大量 syscall 的開(kāi)銷(xiāo)。

另外,其實(shí)在一定程度上后面我們?cè)谥С謮嚎s的時(shí)候,也有非常大的幫助,就是可以減少數(shù)據(jù)的傳輸。對(duì)于整個(gè)系統(tǒng)而言,可能有數(shù)百萬(wàn)的 Region,它的大小可以調(diào)整,比如說(shuō) 64M、128M、256M,這個(gè)實(shí)際上依賴于整個(gè)系統(tǒng)里面當(dāng)前的狀況。

比如說(shuō)我們?cè)?jīng)在有一個(gè)用戶的機(jī)房里面做過(guò)測(cè)試,這個(gè)測(cè)試有一個(gè)香港機(jī)房和新加坡的機(jī)房。結(jié)果我們?cè)谧鰪?fù)制的時(shí)候,新加坡的機(jī)房大于 256M 就復(fù)制不過(guò)去,因?yàn)闄C(jī)房很不穩(wěn)定,必須要保證數(shù)據(jù)切的足夠小,這樣才能復(fù)制過(guò)去。

如果一個(gè) Region 太大以后我們會(huì)自動(dòng)做 SPLIT,這是非常好玩的過(guò)程,有點(diǎn)像細(xì)胞的分裂。

然后 TiKV 的 Raft 實(shí)現(xiàn),是從 etcd 里面 port 過(guò)來(lái)的,為什么要從 etcd 里面 port 過(guò)來(lái)呢?首先 TiKV 的 Raft 實(shí)現(xiàn)是用 Rust 寫(xiě)的。作為第一個(gè)做到生產(chǎn)級(jí)別的 Raft 實(shí)現(xiàn),所以我們從 etcd 里面把它用 Go 語(yǔ)言寫(xiě)的 port 到這邊。

這個(gè)是 Raft 官網(wǎng)上面列出來(lái)的 TiKV在里面的狀態(tài),大家可以看到 TiKV 把所有 Raft 的 feature 都實(shí)現(xiàn)了。 比如說(shuō) Leader Election、Membership Changes,這個(gè)是非常重要的,整個(gè)系統(tǒng)的 scale 過(guò)程高度依賴 Membership Changes,后面我用一個(gè)圖來(lái)講這個(gè)過(guò)程。后面這個(gè)是 Log Compaction,這個(gè)用戶不太關(guān)心。

這是很典型的細(xì)胞分裂的圖,實(shí)際上 Region 的分裂過(guò)程和這個(gè)是類(lèi)似的。

我們看一下擴(kuò)容是怎么做的。

比如說(shuō)以現(xiàn)在的系統(tǒng)假設(shè),我們剛開(kāi)始說(shuō)只有三個(gè)節(jié)點(diǎn),有 Region1 分別是在 1 、2、4,我用虛線連接起來(lái)代表它是一個(gè) Raft group ,大家可以看到整個(gè)系統(tǒng)里面有三個(gè) Raft group ,在每一個(gè) Node 上面數(shù)據(jù)的分布是比較均勻的,在這個(gè)假設(shè)每一個(gè) Region 是 64M ,相當(dāng)于只有一個(gè) Node 上面負(fù)載比其他的稍微大一點(diǎn)點(diǎn)。

一個(gè)在線視頻默認(rèn)我們都是推薦 3 個(gè)副本或者 5 個(gè)副本的配置。Raft 本身有一個(gè)特點(diǎn),如果一個(gè) leader down 掉之后,其它的節(jié)點(diǎn)會(huì)選一個(gè)新的 leader ,那么這個(gè)新的 leader 會(huì)把它還沒(méi)有 commit 但已經(jīng) reply 過(guò)去的 log 做一個(gè) commit ,然后會(huì)再做 apply ,這個(gè)有點(diǎn)偏 Raft 協(xié)議,細(xì)節(jié)我不講了。

復(fù)制數(shù)據(jù)的小的 Region,它實(shí)際上是跨多個(gè)數(shù)據(jù)中心做的復(fù)制。這里面最重要的一點(diǎn)是永遠(yuǎn)不丟失數(shù)據(jù),無(wú)論如何我保證我的復(fù)制一定是復(fù)制到 majority ,任何時(shí)候我只要對(duì)外提供服務(wù),允許外面寫(xiě)入數(shù)據(jù)一定要復(fù)制到 majority 。很重要的一點(diǎn)就是恢復(fù)的過(guò)程一定要是自動(dòng)化的,我前面已經(jīng)強(qiáng)調(diào)過(guò),如果不能自動(dòng)化恢復(fù),那么中間的宕機(jī)時(shí)間或者對(duì)外不可服務(wù)的時(shí)間,便不是由整個(gè)系統(tǒng)決定的,這是相對(duì)回到了幾十年前的狀態(tài)。

MVCC

MVCC 我稍微仔細(xì)講一下這一塊。MVCC 的好處,它很好支持 Lock-free 的 snapshot read ,一會(huì)兒我有一個(gè)圖會(huì)展示 MVCC 是怎么做的。isolation level 就不講了, MySQL 里面的級(jí)別是可以調(diào)的,我們的 TiKV 有 SI,還有 SI+lock,默認(rèn)是支持 SI 的這種隔離級(jí)別,然后你寫(xiě)一個(gè) select for update 語(yǔ)句,這個(gè)會(huì)自動(dòng)的調(diào)整到 SI 加上 lock 這個(gè)隔離級(jí)別。這個(gè)隔離級(jí)別基本上和 SSI 是一致的。還有一個(gè)就是 GC 的問(wèn)題,如果你的系統(tǒng)里面的數(shù)據(jù)產(chǎn)生了很多版本,你需要把這個(gè)比較老的數(shù)據(jù)給 GC 掉,比如說(shuō)正常情況下我們是不刪除數(shù)據(jù)的, 你寫(xiě)入一行,然后再寫(xiě)入一行,不斷去 update 同一行的時(shí)候,每一次 update 會(huì)產(chǎn)生新的版本,新的版本就會(huì)在系統(tǒng)里存在,所以我們需要一個(gè) GC 的模塊把比較老的數(shù)據(jù)給 GC 掉,實(shí)際上這個(gè) GC 不是 Go 里面的GC,不是 Java 的 GC,而是數(shù)據(jù)的 GC。

這是一個(gè)數(shù)據(jù)版本,大家可以看到我們的數(shù)據(jù)分成兩塊,一個(gè)是 meta,一個(gè)是 data。meta 相對(duì)于描述我的數(shù)據(jù)當(dāng)前有多少個(gè)版本。大家可以看到綠色的部分,比如說(shuō)我們的 meta key 是 A ,keyA 有三個(gè)版本,是 A1 、A2、A3,我們把 key 自己和 version 拼到一起。那我們用 A1、A2、A3 分別描述 A 的三個(gè)版本,那么就是 version 1/2/3。meta 里面描述,就是我的整個(gè) key 相對(duì)應(yīng)哪個(gè)版本,我想找到那個(gè)版本。比如說(shuō)我現(xiàn)在要讀取 key A 的版本10,但顯然現(xiàn)在版本 10 是沒(méi)有的,那么小于版本 10 最大的版本是 3,所以這時(shí)我就能讀取到 3,這是它的隔離級(jí)別決定的。關(guān)于 data,我剛才已經(jīng)講過(guò)了。

分布式事務(wù)模型

接下來(lái)是分布式事務(wù)模型,其實(shí)是基于 Google Percolator,這是 Google 在 2006 發(fā)表的一篇論文,是 Google 在做內(nèi)部增量處理的時(shí)候發(fā)現(xiàn)了這個(gè)方法,本質(zhì)上還是二階段提交的。這使用的是一個(gè)樂(lè)觀鎖,比如說(shuō)我提供一個(gè) transaction ,我去改一個(gè)東西,改的時(shí)候是發(fā)布在本地的,并沒(méi)有馬上 commit 到數(shù)據(jù)存儲(chǔ)那一端,這個(gè)模型就是說(shuō),我修改的東西我馬上去 Lock 住,這個(gè)基本就是一個(gè)悲觀鎖。但如果到最后一刻我才提交出去,那么鎖住的這一小段的時(shí)間,這個(gè)時(shí)候?qū)崿F(xiàn)的是樂(lè)觀鎖。樂(lè)觀鎖的好處就是當(dāng)你沖突很小的時(shí)候可以得到非常好的性能,因?yàn)闆_突特別小,所以我本地修改通常都是有效的,所以我不需要去 Lock ,不需要去 roll back 。本質(zhì)上分布式事務(wù)就是 2PC (兩階段提交) 或者是 2+x PC,基本上沒(méi)有 1PC,除非你在別人的級(jí)別上做弱化。比如說(shuō)我允許你讀到當(dāng)前最新的版本,也允許你讀到前面的版本,書(shū)里面把這個(gè)叫做幻讀。如果你調(diào)到這個(gè)程度是比較容易做 1PC 的,這個(gè)實(shí)際上還是依賴用戶設(shè)定的隔離級(jí)別的,如果用戶需要更高的隔離級(jí)別,這個(gè) 1PC就不太好做了。

這是一個(gè)路由,正常來(lái)講,大家可能會(huì)好奇一個(gè) SQL 語(yǔ)句怎么最后會(huì)落到存儲(chǔ)層,然后能很好的運(yùn)行,最后怎么能映射到 KV 上面,又怎么能路由到正確的節(jié)點(diǎn),因?yàn)檎麄€(gè)系統(tǒng)可能有上千個(gè)節(jié)點(diǎn),你怎么能正確路由到那一個(gè)的節(jié)點(diǎn)。我們?cè)?TiDB 有一個(gè) TiKV driver , 另外 TiKV 對(duì)外使用的是 Google Protocol Buffer 來(lái)作為通訊的編碼格式。

Placement Driver

來(lái)說(shuō)一下 Placement Driver 。Placement Driver 是什么呢?整個(gè)系統(tǒng)里面有一個(gè)節(jié)點(diǎn),它會(huì)時(shí)刻知道現(xiàn)在整個(gè)系統(tǒng)的狀態(tài)。比如說(shuō)每個(gè)機(jī)器的負(fù)載,每個(gè)機(jī)器的容量,是否有新加的機(jī)器,新加機(jī)器的容量到底是怎么樣的,是不是可以把一部分?jǐn)?shù)據(jù)挪過(guò)去,是不是也是一樣下線, 如果一個(gè)節(jié)點(diǎn)在十分鐘之內(nèi)無(wú)法被其他節(jié)點(diǎn)探測(cè)到,我認(rèn)為它已經(jīng)掛了,不管它實(shí)際上是不是真的掛了,但是我也認(rèn)為它掛了。因?yàn)檫@個(gè)時(shí)候是有風(fēng)險(xiǎn)的,如果這個(gè)機(jī)器萬(wàn)一真的掛了,意味著你現(xiàn)在機(jī)器的副本數(shù)只有兩個(gè),有一部分?jǐn)?shù)據(jù)的副本數(shù)只有兩個(gè)。那么現(xiàn)在你必須馬上要在系統(tǒng)里面重新選一臺(tái)機(jī)器出來(lái),它上面有足夠的空間,讓我現(xiàn)在只有兩個(gè)副本的數(shù)據(jù)重新再做一份新的復(fù)制,系統(tǒng)始終維持在三個(gè)副本。整個(gè)系統(tǒng)里面如果機(jī)器掛掉了,副本數(shù)少了,這個(gè)時(shí)候應(yīng)該會(huì)被自動(dòng)發(fā)現(xiàn),馬上補(bǔ)充新的副本,這樣會(huì)維持整個(gè)系統(tǒng)的副本數(shù)。這是很重要的 ,為了避免數(shù)據(jù)丟失,必須維持足夠的副本數(shù),因?yàn)楦北緮?shù)每少一個(gè),你的風(fēng)險(xiǎn)就會(huì)再增加。這就是 Placement Driver 做的事情。

同時(shí),Placement Driver 還會(huì)根據(jù)性能負(fù)載,不斷去 move 這個(gè) data 。比如說(shuō)你這邊負(fù)載已經(jīng)很高了,一個(gè)磁盤(pán)假設(shè)有 100G,現(xiàn)在已經(jīng)用了 80G,另外一個(gè)機(jī)器上也是 100G,但是他只用了 20G,所以這上面還可以有幾十 G 的數(shù)據(jù),比如 40G 的數(shù)據(jù),你可以 move 過(guò)去,這樣可以保證系統(tǒng)有很好的負(fù)載,不會(huì)出現(xiàn)一個(gè)磁盤(pán)巨忙無(wú)比,數(shù)據(jù)已經(jīng)多的裝不下了,另外一個(gè)上面還沒(méi)有東西,這是 Placement Driver 要做的東西。

Raft 協(xié)議還提供一個(gè)很高級(jí)的特性叫 leader transfer。leader transfer 就是說(shuō)在我不移動(dòng)數(shù)據(jù)的時(shí)候,我把我的 leadership 給你,相當(dāng)于從這個(gè)角度來(lái)講,我把流量分給你,因?yàn)槲沂?leader,所以數(shù)據(jù)會(huì)到我這來(lái),但我現(xiàn)在把 leader給你,我讓你來(lái)當(dāng) leader,原來(lái)打給我的請(qǐng)求會(huì)被打給你,這樣我的負(fù)載就降下來(lái)。這就可以很好的動(dòng)態(tài)調(diào)整整個(gè)系統(tǒng)的負(fù)載,同時(shí)又不搬移數(shù)據(jù)。不搬移數(shù)據(jù)的好處就是,不會(huì)形成一個(gè)抖動(dòng)。

MySQL Sharding

MySQL Sharding 我前面已經(jīng)提到了它的各種天花板,MySQL Sharding 的方案很典型的就是解決基本問(wèn)題以后,業(yè)務(wù)稍微復(fù)雜一點(diǎn),你在 sharding 這一層根本搞不定。它永遠(yuǎn)需要一個(gè) sharding key,你必須要告訴我的 proxy,我的數(shù)據(jù)要到哪里找,對(duì)用戶來(lái)說(shuō)是極不友好的,比如我現(xiàn)在是一個(gè)單機(jī)的,現(xiàn)在我要切入到一個(gè)分布式的環(huán)境,這時(shí)我必須要改我的代碼,我必須要知道我這個(gè) key ,我的 row 應(yīng)該往哪里 Sharding。如果是用 ORM ,這個(gè)基本上就沒(méi)法做這個(gè)事情了。有很多 ORM 它本身假設(shè)我后面只有一個(gè) MySQL。但 TiDB 就可以很好的支持,因?yàn)槲宜械慕巧际菍?duì)的,我不需要關(guān)注 Sharding 、分庫(kù)、分表這類(lèi)的事情。

這里面有一個(gè)很重要的問(wèn)題沒(méi)有提,我怎么做 DDL。如果這個(gè)表非常大的話,比如說(shuō)我們有一百億吧,橫跨了四臺(tái)機(jī)器,這個(gè)時(shí)候你要給它做一個(gè)新的 Index,就是我要添加一個(gè)新的索引,這個(gè)時(shí)候你必須要不影響任何現(xiàn)有的業(yè)務(wù),實(shí)際上這是多階段提交的算法,這個(gè)是 Google 和 F1 一起發(fā)出來(lái)那篇論文。

簡(jiǎn)單來(lái)講是這樣的,先把狀態(tài)標(biāo)記成 delete only ,delete only 是什么意思呢?因?yàn)樵诜植际较到y(tǒng)里面,所有的系統(tǒng)對(duì)于 schema 的視野不是一致的,比如說(shuō)我現(xiàn)在改了一個(gè)值,有一部分人發(fā)現(xiàn)這個(gè)值被改了,但是還有一部分人還沒(méi)有開(kāi)始訪問(wèn)這個(gè),所以根本不知道它被改了。然后在一個(gè)分布系統(tǒng)里,你也不可能實(shí)時(shí)通知到所有人在同一時(shí)刻發(fā)現(xiàn)它改變了。比如說(shuō)從有索引到?jīng)]有索引,你不能一步切過(guò)去,因?yàn)橛械娜苏J(rèn)為它有索引,所以他給它建了一個(gè)索引,但是另外一個(gè)機(jī)器他認(rèn)為它沒(méi)有索引,所以他就把數(shù)據(jù)給刪了,索引就留在里面了。這樣遇到一個(gè)問(wèn)題,我通過(guò)索引找的時(shí)候告訴我有, 實(shí)際數(shù)據(jù)卻沒(méi)有了,這個(gè)時(shí)候一致性出了問(wèn)題。比如說(shuō)我 count 一個(gè) email 等于多少的,我通過(guò) email 建了一個(gè)索引,我認(rèn)為它是在,但是 UID 再轉(zhuǎn)過(guò)去的時(shí)候可能已經(jīng)不存在了。

比如說(shuō)我先標(biāo)記成 delete only,我刪除它的時(shí)候不管它現(xiàn)在有沒(méi)有索引,我都會(huì)嘗試刪除索引,所以我的數(shù)據(jù)是干凈的。如果我刪除掉的話,我不管結(jié)果是什么樣的,我嘗試去刪一下,可能這個(gè)索引還沒(méi) build 出來(lái),但是我仍然刪除,如果數(shù)據(jù)沒(méi)有了,索引一定沒(méi)有了,所以這可以很好的保持它的一致性。后面再類(lèi)似于前面,先標(biāo)記成 write only 這種方式,連續(xù)再迭代這個(gè)狀態(tài),就可以迭代到一個(gè)最終可以對(duì)外公開(kāi)的狀態(tài)。比如說(shuō)當(dāng)我迭代到一定程度的時(shí)候,我可以從后臺(tái) build index ,比如說(shuō)我一百億,正在操作的 index 會(huì)馬上 build,但是還有很多沒(méi)有 build index ,這個(gè)時(shí)候后臺(tái)不斷的跑 map-reduce 去 build index ,直到整個(gè)都 build 完成之后,再對(duì)外 public ,就是說(shuō)我這個(gè)索引已經(jīng)可用了,你可以直接拿索引來(lái)找,這個(gè)是非常經(jīng)典的。在這個(gè) Online,Asynchronous Schema Change in F1 paper之前,大家都不知道這事該怎么做。

Proxy Sharding 的方案不支持分布式事務(wù),更不用說(shuō)跨數(shù)據(jù)中心的一致性事務(wù)了。 TiKV 很好的支持 transaction,剛才提到的 Raft 除了增加副本之外,還有 leader transfer,這是一個(gè)傳統(tǒng)的方案都無(wú)法提供的特性。以及它帶來(lái)的好處,當(dāng)我瞬間平衡整個(gè)系統(tǒng)負(fù)載的時(shí)候,對(duì)外是透明的, 做 leader transfer 的時(shí)候并不需要移動(dòng)數(shù)據(jù),只是個(gè)簡(jiǎn)單的 leader transfer 消息。

然后說(shuō)一下如果大家想?yún)⑴c我們項(xiàng)目的話是怎樣的過(guò)程,因?yàn)檎麄€(gè)系統(tǒng)是完全開(kāi)源的,如果大家想?yún)⑴c其中任何一部分都可以,比如說(shuō)我想?yún)⑴c到分布式 KV,可以直接貢獻(xiàn)到 TiKV。TiKV 需要寫(xiě) Rust,如果大家對(duì)這塊特別有激情可以體驗(yàn)寫(xiě) Rust 的感覺(jué) 。

TiDB 是用 Go 寫(xiě)的,Go 在中國(guó)的群眾基礎(chǔ)是非常多的,目前也有很多人在貢獻(xiàn)。整個(gè) TiDB 和TiKV 是高度協(xié)作的項(xiàng)目,因?yàn)?TiDB 目前還用到了 etcd ,我們?cè)诤?CoreOS 在密切的合作,也特別感謝 CoreOS 幫我們做了很多的支持,我們也為 CoreOS 的 etcd 提了一些 patch。同時(shí),TiKV 使用 RocksDB ,所以我們也為 RocksDB 提了一些 patch 和 test,我們也非常感謝 Facebook RocksDB team 對(duì)我們項(xiàng)目的支持。

另外一個(gè)是 PD,就是我們前面提的 Placement Driver,它負(fù)責(zé)監(jiān)控整個(gè)系統(tǒng)。這部分的算法比較好玩,大家如果有興趣的話,可以去自己控制整個(gè)集群的調(diào)度,它和 Kubernetes 或者是Mesos 的調(diào)度算法是不一樣的,因?yàn)樗{(diào)度的維度實(shí)際上比那個(gè)要更多。比如說(shuō)磁盤(pán)的容量,你的 leader 的數(shù)量,你的網(wǎng)絡(luò)當(dāng)前的使用情況,你的 IO 的負(fù)載和 CPU 的負(fù)載都可以放進(jìn)去。同時(shí)你還可以讓它調(diào)度不要跨一個(gè)機(jī)房里面建多個(gè)副本。