GopherChina 2017 演講實錄|申礫:Go in TiDB
各位好,我叫申礫,來自 PingCAP。今天和大家分享一下 Go 語言在分散式資料庫 TiDB 開發中的一些使用經驗。
先調查一下,請問有多少人聽說過 TiDB 請舉手。有多少人下載並且搭建起來過 TiDB 請舉手。(此處舉手無數,小編很感動~~)
看來大家都或多或少有一些了解。我先會介紹一下 TiDB 是什麼,有什麼樣的特點。然後介紹 TiDB 的整體架構特別是 SQL 層的一些東西,最後會介紹一些我們開發過程中使用 Golang 的一些實踐經驗。
首先介紹一下我自己,我之前在網易有道和 360 搜索做過各種垂直搜索主要設計信息檢索、數據挖掘、分散式計算。目前在 PingCAP 負責 TiDB 的技術,主要做分散式存儲/計算。
TiDB 是什麼,簡單來講,TiDB 是一個分散式資料庫。資料庫有很多種,各有各的特點,解決各自的應用場景,那麼為什麼我們要開發一個新的資料庫,這個資料庫有什麼特點?一些場景下,資料庫問題已經被很好的解決,比如小數據量,以及中小規模的查詢量,單機資料庫已經很好的解決了問題。但是在一些場景下,問題還遠沒有解決,而且這些場景出現的越來越多。我們認為下一代分散式資料庫應該有這樣一些特點:
首先必須是可以水平擴展的。大家應用場景中數據越來越多,能把數據順利的保存下來是基本的要求。所以才有了各種分庫分表、中間件、NoSQLn n資料庫,或者是各種通過共享存儲解決容量問題分方案。一句話講,數據存不下來,其他的一切都是無用功。我們希望有一種方式能通過簡單的增加節點就能實現水平擴展的方案。
第二是高可用以及 Auto-Failover。當你的資料庫能水平擴展,業務蒸蒸日上,用戶越來越多,集群規模越來越大時,那麼恭喜你已經走上人生巔峰。但是,機器是會出現故障的,硬碟、網卡、電源、光纖被挖斷,總有一款適合你。集群規模大了之後,故障就會變得常見。那麼我們就需要應對這種情況。靠人肉運維是不可能的,我們需要集群能夠在出現少量節點故障的情況下依舊能服務,並且能自動恢復。
第三是 ACID,也就是事務。很多資料庫方案為了實現之前的兩點,放棄了事務,這樣開發資料庫的難度一下子就下來了不少,如果業務用不到事務還好,一但需要,就很痛苦,需要自己去寫各種邏輯,包括正常情況的處理、衝突情況的處理以及各種異常情況的處理。Jeffn Dean 說過,他很後悔沒給 BigTable 加上事務,導致了內部團隊各種造輪子。不但造成重複勞動,而且大家造的也不一定是對的。
最後是 SQL。提供n KV 介面已經能解決不少問題,並且只提供簡單的 KV 介面能夠簡化存儲模型。但是 SQL n這門古老而強大的語言在資料庫中被廣泛使用,在現在以及可見的將來都有很強的生命力。支持 SQL n對業務端能提供非常好的支持。所以我們做了一個新的資料庫,具備上述四個特點,這就是 TiDB。
接下來我會逐步介紹一些 TiDB 內部的東西,包括整體架構核實現細節。
上圖是n TiDB 的整體架構,我們可以看到最下面是 TiKV 集群,也就是整個 TiDB 的存儲引擎層,這裡是一個帶事務的分散式KV,且內置一個和 nTiDB SQL 層一起使用的分散式計算框架。我們會把一些計算邏輯發送到 TiKV 處理。右邊是 PD 集群,主要負責三件事情:
1. 管理 TiKV 集群的元信息,主要是有哪些實例、哪些 region、每個 region 的 startkey 和 endkey 是什麼。
2. 對 TiKV 集群做調度,包括新增結點後,能夠以 region 為單位做調度,結點 down 機後,能夠將上面的數據調度走,還有就是根據容災策略、機器負載做調度。
3. 生成時間戳,用於事務。
中間這層是 SQL 層,提供對業務的介面,負責處理 protocol、SQL 解析、查詢計劃生成、整個執行器的控制和調度。
在這層之上,可以添加 proxy,用來負載均衡。這個已經有很多優秀的方案,直接拿來用就好。我們對外暴露的就是 MySQL 協議,業務端不需要感知下面是 TiDB 還是 MySQL。
大部分人都用過資料庫,但是真正自己開發過資料庫或者是看過一些資料庫實現的人可能不多。我從 SQL 的角度簡單介紹一下 TiDB 的核心流程。
這張圖是n SQL 層的架構。首先前端要在某個埠上監聽 client 請求,在一個 connection n的生命周期內,我們需要維護他的一些信息,還要做編解碼轉換,得到結構化的請求。這個請求就會丟進 core layer 開始處理。我們還會維護一個 nsession conext,包括這個 session 的各種信息,比如 session scope 的 variable 值。這個 ncontext 也是整個 SQL 層的入口。SQL 進來之後先交給 parser 從文本處理成結構化的數據,也就是 ast,也就是從 nstring n轉換成為一個易於處理的內部樹形結構。接下來對這個結構進行各種處理,包括做合法性驗證、名字解析、類型推導、許可權檢查等等,為之後的查詢計劃制定做各種準備。
下面進去了核心組件:optimizer。首先是進行邏輯優化,將每個n ast 的 node 轉化成邏輯上的 plan,並且從數學角度做等價變化,簡化 SQL 的邏輯,比如做常量摺疊,select * from tn where 1=1; 被轉換為 select * from t;,還有常量傳播:select * from t where c1=1 and nc2 = c1; 被轉換為 select * from t c1=1 and c2=1;。
接下來根據邏輯計劃制定物理計劃,這兩個有什麼區別呢?簡單來講,邏輯計劃是不關係細節,只關心n SQL 的邏輯,比如 select * from t where c > 10; n不管這個表是否有索引,是本地內存表還是遠程的存儲引擎中的表,都認為 from t 是一個 ndatasource。但是物理優化需要考慮各種現實問題,比如去哪裡讀數據、怎麼去讀數據會比較快。再舉個通俗點的例子,老婆說我要吃好吃的,那麼這個就是一個邏輯計劃,沒有任何現實考慮。老公就要制定物理計劃,比如在家裡吃還是外面買吃,要吃什麼口味的,花多少錢比較合適,有沒有團購可用、幾點出發、做什麼車去、會不會堵車排隊、吃完這頓還有沒有錢吃下頓,怎麼才能讓這頓飯吃的多快好省。
下面這一層是執行器,就是能按照物理計劃老老實實的做各種操作,優化器就是老闆,負責制定計劃,執行器就是員工,來做執行。執行的時候,少不了要和存儲交互,取數據、做運算。
整個 SQL 層除了這些之外,還包括很多重要組件,比如許可權管理、schema 管理、DDL worker、統計信息管理等。
上面講了流程,這裡以一個 SQL 為例進行說明。
假設我們有這樣一個 schema,兩個欄位,一個索引。現在要處理這樣一條 SQL,有查詢條件以及一個 count 操作,很簡單的一條 SQL。
我們拿到這個 AST 之後,先不考慮那麼多,制定出邏輯的計劃。這條 SQL 比較簡單,首先是從一個數據源拿數據,然後看看每行數據是否滿足要求,滿足這個 where 條件的,就增加計數器。
接下來就要開始考慮現實的問題,表結構是什麼樣的,數據從哪裡來、到哪裡去、如何處理。比如這裡可以選擇通過n index1 來過濾數據,再檢查 c2 上面的條件是否滿足,最後計算 count。當然這裡還會取決於數據的分布,假設所有的數據中 c1 n都是大於 10 的,那麼索引就沒起作用,還不去直接去讀取數據來的快。這裡就需要計算 plan 的 cost,也就是大家常說的 ncost-based optimize。
在單機情況下,剛才的n plan 已經不錯了,但是我們做的是一個分散式資料庫,大部分情況都要處理分散式的計算。剛才的那個 SQL 丟給 TiDB 最終會制定出這樣一個n plan,SQL 層和 TiKV 層會一起協作,一部分數據交給 TiKV 層做計算,最終結果匯總到 SQL 層,做 final naggregate。這樣有很多好處,首先參與計算的節點變多,計算速度加快,第二計算靠近儲存,能夠很快的讀取數據出來,並且做了過濾以及聚合,減少了大量的網路傳輸。
大家從上面可以看到,整個邏輯是非常複雜的。我們構建這個分散式資料庫面臨了很多挑戰。
首先,我們構建的是一個大規模的分散式系統,而且是一個很複雜的系統,各種分散式系統中的問題我們都會遇到,比如一致性問題。
第二,分散式資料庫和單機資料庫一個很大的區別是,需要大量的網路操作,無論是維護集群狀態,還是處理 SQL 語句,都需要大量的網路操作系統,我們需要簡單方便的網路操作。
第三,資料庫作為業務的核心,其性能至關重要,對於 OLTP 業務,一般來說對用戶的響應時間低於 200ms,用戶就感受不到延遲,這 200ms 在網路上就要消耗不少,再加上業務代碼的處理,真正留給資料庫的時間並不多。
第四,資料庫往往要處理海量的數據,那麼請求處理過程中,這些數據需要申請內存,用完之後還要銷毀內存。有 GC 會幫助很大。
第五,資料庫需要抗並發能力強,OLTP 業務往往需要很高的吞吐,到幾十萬甚至上百萬。
第六,作為一個存儲了海量數據的資料庫,不提供 OLAP 能力是很不方便的,否則用戶還需要用 ETL 工具將數據倒出去,浪費資源並且費時費力、無法獲取實時數據。OLAP 是一個大坑,各種複雜的查詢處理起來還是很難的。
還好,面對這麼多挑戰,有 Go 語言這個好幫手。
我們為什麼選擇了 Go 語言?它有哪些地方吸引我們,幫我們解決問題?
首先是開發效率,構建一個如此大規模的系統,並且支持n SQL 這麼複雜的東西,再加上我們支持了 MySQL 的協議和行為。支持協議簡單,支持行為很難,特別是我們還需要支持 MySQL 的 nbug(因為有些用戶已經把 bug 當做 feature 用了)。我們需要一門開發效率高、工程上優秀的語言。
Go 語言最大的特點就是並發寫起來很舒服方便、這正適合一個需要抗高並發、需要做並行計算提速的資料庫。
Go 的網路編程很簡單,我們很容易寫出高質量的網路程序。
GC,有了 GC 降低了很多心智負擔,也降低了代碼中出問題的概率。當然 GC 也會帶來延遲和抖動、我們也需要很好的去和 GC 相處。
Go 有強大的標準庫和豐富的第三方庫,import 機制也很好用。很少出現缺失庫的問題。
Go 官方還提供很多好用的工具,比如 pprof,trace 等,多次幫助我們定位了問題。
性能對資料庫至關重要,Go 語言運行速度非常不錯,雖然還是趕不上 C/C++,但是已經讓我們很滿意。
另外還有一點,Go 的開發還是很活躍,半年一個版本,每次都有令人興奮的特性。這也給我們很好的支持。
Go 語言在 PingCAP 有大量的應用,除了 TiKV 和一些自動化部署工具之外,所有的組件都是 Go 來開發的。我統計了一下 SQL 層的代碼,目前已經超過 11w 行 Go 代碼,並且有 100 多個 contributor 貢獻過代碼。我們在實踐中積累了一些實踐經驗,和大家分享一下。
首先看n Goroutine。Goroutine 是 Go 最大的特點,語言名字就是 Go 這個關鍵詞。在代碼中,我們可以很簡單方便的啟動一個新的 nGoroutine,並且 Goroutine n之間有原生的通訊機制(channel),這樣我們就很容易寫成並發程序,充分利用現代的多核計算資源。接下來介紹兩個例子,我們是如何通過並行計算提高計算效率。
先看一個並行讀取數據的例子。
通過索引讀取數據是一個普遍的需求,TiDBn 支持 Global nIndex,索引數據和表中的數據並不一定在一台機器上,那麼我們就需要先去一些機器上讀取索引,再根據這些索引信息讀取真實的數據。由於索引分散在很多n TiKV 上面,需要向這些節點發消息,等待返回結果。這個時候我們是並行發送請求,並且拿到第一個索引結果後,就可以開始查詢 Table n中的數據,同樣查詢 Table n中的數據請求也是並行發送。這樣我們通過並行+流水線的方式,將整個過程儘可能並行,儘可能快的進行,減小啟動時間以及整體執行時間。
下面是一個並行 HashJoin 的例子。
HashJoinn 在中小規模數據量上總是表現的不錯,我們也實現這個運算元,同時我們做了一些優化,儘可能的並行處理數據。首先參與 Join n的表是並行讀取,讀取小表時,會建立 Hash 表,讀取大表數據後,會交給多個 Worker。當 Hash 表建完後,會通知所有的 nWorker,就可以開始進行 Join 操作,輸出結果。
我們項目中還有很多並行的例子,我們也在不斷將各種運算元並行化。
Goroutinen 雖然好,但是用不好就會有內存泄露的問題。一般情況下泄露會出現在讀取一個沒人寫入的 Channel,或者是向一個沒人讀取的 Channel n寫入數據。Goroutine 泄露後,除了對 Go runtime 的調度造成壓力之外,更重要的是會將這些 Goroutine n持有的資源泄露,包括內存、fd 等等。當出現泄露的時候,我們可以用 Go 的 block profile 工具查看當前阻塞的 nGoroutine,定位泄露位置。當然這是亡羊補牢的方法。我們還是儘可能的減少泄露的出現,一般來說可以在代碼中加入等待超時,或者是通過 ncontext 的 cancel 機制保證來清理資源。
這裡給大家推薦一個 Goroutine 泄露檢測工具,這個工具來自於 Go 的標準庫 net/http。簡單來講就是在測試前將現有的 Goroutine 記錄下來,測試完成後,看看是否有新增的 Goroutine,有的話就可能有泄露。
接下來聊一下內存和n GC。TiDB 面臨 OLTP/OLAP 混合業務,在 OLAP 業務中,往往需要處理海量的數據,處理幾千萬甚至上億行數據,在一些較為複雜的 nSQL 運行過程中,我們通過 profile 工具往往發現內存分配和 GC 的時間可以佔到總時間的一半。
大量的內存開銷一方面拖慢了運行速度,第二程序會面臨 OOM 的風險。所以內存使用是一個需要特別重視的問題。我們花費了大量的精力來優化內存使用,有幾點經驗和大家分享。
首先是想辦法降低內存分配的次數。相比分配的大小,分配次數是更關鍵的指標,我們需要儘可能將分配次數減小,比如一次分配足夠的 Slice,或者是將多個結構組合在一起,一次分配。
第二點是儘可能重用對象。舉一個實際的例子,我們在 TiDB 的 parser 中引入了一個對象 cache,一個 Session 的多個 Query 在進行 parse 的時候會使用一個對象 stack,經過測試我們發現對內存使用優化效果明顯。
第三點是盡量使用 sync.Pool。這是標準庫中提供的重用對象的工具,在 TiDB 廣泛應用。並發安全,使用簡單。我們用 sync.Pool 實現了一個 bytespool,在大查詢下顯著的提升了性能。
Protobufn 是比較標準的 rpc 序列化工具,被廣泛的應用,TiDB 也用 protobuf 作為序列化工具。當涉及到大數據量傳輸以及大量的請求時,rpcn 的效率至關重要。我們開始的時候用的 golang/protobuf,但是在 profile n的時候發現速度並不理想,並且會申請很多內存,後來看到了 gogo/protobuf 這個項目,提供更快的編解碼速度,並且對於原始類型,可以設置 nnullable=fasle 標籤,一方面減少指針的數量,減少內存分配次數,另一方面代碼寫起來也方便些。
處理優化內存使用之外。我們還嘗試對內存進行監控,包括對整個進程的內存使用以及每個 Session 的內存使用。對於單個 Session 而言,我們會對大塊分配的內存以及比較消耗內存的運算元進行監控,比如 HashJoin,這樣對內存使用做一個粗略的統計。
Go1.8n 發布後帶來一些新特性,其中有兩個特性對我們比較有用。第一是 GC 進一步優化,很多時候 GC 能夠在 10 μs 內完成,這點對提高 TiDBn 穩定性很有用。第二點是增加了 sort.Slice 方法,TiDB 中有大量的排序需求,我們經過 benchmark 發現這個方法比以前的 nsort.Sort 有不小的優勢。
講完了我們的使用經驗之後,和大家分享一下我們接下來的一些計劃。首先是馬上會將通訊框架換成n gRPC,之前沒用 gRPC 是因為 Rust 缺少對應的庫,切換到 gRPC 之後就可以通過 streaming n的方式獲取數據,提高計算效率。第二,我們計劃支持文檔操作,具體的形式是仿照 MySQL 的 document nstore,這個會在近期啟動。第三是會持續優化統計信息以及基於代價的查詢優化,這個非常有技術難度,我們已經取得了不錯的成果,在 GA n版本中大家會看到更多的成果。第四,我們會開發一個更快更通用的分散式計算引擎,用來處理 OLAP 需求。最後,我們正在將 Spark 接到 nTiKV 上,這樣可以直接讀取數據進行分析,很多數據挖掘、機器學習的工作也可以通過 Spark + TiDB 的方式解決。
最後給大家分享一首小詩:
May all the data find their ownplace,
May all the SQL queries speed up!
Come, and join us,
To make the best NewSQL database!
感謝大家!
推薦閱讀:
※TiDB 在 360 金融貸款實時風控場景應用
※gRPC-rs:從 C 到 Rust
※基於 Tile 連接 Row-Store 和 Column-Store
※TiDB 在 G7 的實踐和未來
TAG:TiDB |