李雨來 :Query Cache in TiDB
本文為 TiDB DevCon2018 實錄系列第一篇,是由 TiDB Committer 李雨來分享的《Query Cache in TiDB》的完整視頻及文字實錄。enjoy~~
Query Cache in TiDB_騰訊視頻
大家好,我叫李雨來,是 SpeedyCloud 的架構師。同時也是 TiDB 的一個 Contributor,我今天幫大家介紹一下在業餘時間裡幫 TiDB 做的一個 commit,這個功能包含了一個和查詢緩存相關的一個功能。
說到查詢緩存,大家可能第一個想的到的就是跟 OLAP 相關的一些應用程序。比如說,我這邊寫了兩個比較簡單的 SQL 的模型,第一個統計類查詢和聚合,去做一些 SUM,或者做一些 AVERAGE,同時還有 GROUP BY,ORDER BY 。另外一個是排序類查詢,比如我查 Top 10,Top 100。然後同時還有一些 WHERE 的查詢在裡面,這個其實對於像 OLAP 的一些 Web 的應用程序來講,大家可能很容易就想到,就是我畫一張圖,或者說出一個表,或者說給基本上對應的界面上的這兩種顯示。那麼這兩種類型的 SQL 如果我在 TiDB 裡面做的話,我發現有幾個痛點。
第一,多個同時查同一類型的執行的話,發現同一個 SQL 我要執行多次,會導致 TiDB 做一些重複計算。一千個人來訪問這個頁面,做了一千次的查詢,那麼這一千次的查詢,對 TiDB 和 TiKV 來講的話,好多的結果是一樣的。沒有查詢緩存的話,肯定是重複計算,重複計算另外一個問題是當我並發量大到一定程度的時候,TiKV 的計算資源可能會變的更緊張一些。因為我們的 CPU 核數是有限的,我掃同樣的數據,我掃一遍跟掃一千遍,對於 CPU 來講,他要佔用更多的 CPU 的計算時間。那麼很多人就想到,我肯定要加一個緩存把一些重複計算的內容,給他緩存起來,下一次請求的時候,給他返回。緩存的話,一般我們會想到,兩個地方可以加緩存,第一個我應用程序加一下緩存,比如像 Redis,Memcached。這個時候會有一個問題,在有寫操作的時候,我需要刷這個緩存,如果我對緩存的要求又比較實時性,比如說我先插入一條數據之後,我希望我這個緩存更新的數據,跟我新插入的這個數據是一樣的,那麼就有一個問題,我需要控制好這個緩存的刷新節奏。
比如大家如果做過 Web 開發的應該都知道,前面的是 N 台 Web 伺服器,我每台伺服器在讀寫相應緩存的刷的邏輯都要寫到 Web 應用程序裡頭去,而且我同時保證並發的時候,大家在刷的時候,是一樣的。我整個刷的邏輯,刷的節奏都要保證他是一致性的。這個其實對於開發者來講的話,可能是有點負擔,因為畢竟我要多寫一些代碼去做一些緩存的刷新。另外一個,我看在 TiDB 這個層面上,可不可以做一個 Cache,這樣的話,我直接丟一個 SQL 進去就可以了。
TiDB 實現緩存有兩種方式,可以在 TiDB 層面上做緩存,也可以在 TiKV 層面做緩存。TiDB 層面做緩存,可能有一個問題,比應用程序操作反而還複雜的就是說,TiDB 是無狀態的,大家可以理解為 TiDB 本身的 TiDB Server 這個進程,可以是無狀態的。他所有的狀態其實都在 TiKV 這個 Server 裡頭。我在 TiDB 想緩存的話,最後有一個問題,我多個 TiDB Server 之間,我要同步緩存刷新。因為我不可能在 A TiDB 伺服器上插入一條數據,把 A 這個伺服器的緩存刷新之後,我 B 伺服器沒有刷新,當我下一個請求,落到 B 伺服器的話,我這數據其實是個錯的(過期的)。另外一點,接下來就是我可以在 TiKV 這個地方加緩存,他也有他的好處,因為我 TiDB 讀數據的時候,他其實是選 Raft Leader 去讀和寫數據。那麼我現在就是說,基於現有的 TiKV 設計的話,我在 TiKV 一側加緩存的優勢就是我不需要去在多台 TiKV 服務之間去同步我實際緩存的最終結果。
這個圖解釋了一個 Query 的執行在整個 TiDB 集群里是怎麼做的。
當一個 Query 進來之後,通過 TiDB 的 Server 去獲取到這個請求,然後通過程序去解析邏輯的計劃,然後再轉成物理計劃。轉成物理計劃之後,它會把物理計劃,根據你所在的 Region 區間,去給每一個 Region 生成一個 DistSQL,然後再通過 gRPC 把 DistSQL(分散式計算單元)發到對應的 TiKV 的伺服器上,然後 TiKV 再進行執行。TiKV 裡頭有 Coprocessor 這個其實就是說我把計算任務下推下去,然後 Coprocessor 再去通過 RocksDB 去讀到真正的數據,進行計算,最終返回出來。那麼加上這個 DistSQL Cache 的話,現在整個架構就變成這樣。
就是說我在 Coprocessor 的層面上,再掛一個 Cache,如果 Coprocessor 發現這個 DistSQL 曾經被我執行過,然後在緩存裡頭也有執行的結果,那麼我 Coprocessor 不用再把 DistSQL 重新做一次計算,直接返回到 TiDB上,這樣能節省很多時間。尤其像這種 OLAP,我對整個 Region 進行掃描,再對不同的列進行 SUM,AVERAGRE 的計算的時候,這塊其實能提高很大的性能。
我現在實現的方式,其實就是 TiKV 側加了一個緩存。他針對 DistSQL 做結果的一個緩存。目前用了一個最簡單的緩存演算法,就是 LRU Cache。另外一個好處呢,就是我這個是一個天然的分散式的緩存。也就是說,我每一個 TiKV 可以配一個很小的緩存,但是我多台 TiKV 服務加在一起的話,我的緩存量會變的很大。
另外他的優勢,剛才也提到了,我不需要維護多台 TiDB Server 之間的緩存一致性。大家了解過,CPU的 L2 Cache 和 L3 Cache 開始刷新的時候,你也知道這個東西是有多麼熱鬧。可能會導致性能問題。其實現在好多比如像 C++ 的編譯器,在對緩存這塊,要做很多很多的事情,才讓我們看到,我們現在應用程序執行的非常好。如果你在 CPU 的緩存上,有些地方,做的不是特別協調的話,可能執行速度反而會更慢當然這些這都是一些題外話。
另外一點,DistSQL Cache 還有一個比較有意思的特性,也是我自己在執行做測試的時候才發現,查詢他會有一個局部命中的這麼一個概念,比如我查一條 SQL 的話,我落到十個 Region,這十個 Region 的 Cache 被命中的時候,我認為是一個百分之百的命中然後返回的,但是如果我新的查詢,查詢條件是一樣的,然後你發現,我 TiDB 的 Cache Miss 之後,我發現我下推計算的運算元是一樣的情況下。但是我增加了第十一個 Region 的話,你會發現這個特別有意思的效果,就是說我只用算第十一個 Region 的數據,剩下的十個 Region 的數據,其實他也是為 Cache 直接就能返回的。比如像我查一些歷史數據,從 1 日查到 15 日,我可能重新建一個 1 日到 15 日的緩存。如果另外一個情況,我 1 日到 16 日,整個我的查詢執行是只是重新算一遍 16 日的數據,然後從 1 日到 15 日的數據,我可以直接從 Cache 拿走。這樣的話不用重新來算 1 日到 16 日,這樣可能數據計算快很多。
另外一個就是,我們的緩存要緩存什麼內容。因為看一下這個 TiDB 本身的配置文件,我們也知道,其實 RocksDB 本身有一個 Cache,所以第二條我寫了簡單的這種無須計算的 RocksDB 本身就有一個 Cache 我們可以直接用起來的優勢。當然了,我這邊想法,主要是我在聚合和 TopN 這種類型的查詢的時候,包括我需要做大量的 CPU 計算。這樣的話,我就是說把這些東西緩存起來,減少我的整個重複計算的次數。另外一個點,在這塊,我最後一條寫的就是說我們要限制緩存的結果的大小,這個主要原因是,如果我一個數據,本身如果一個 DistSQL 查詢,我可能把整個 Region 的數據全讀取出來了,那麼其實你會發現,TiKV 跟 TiDB 之間的網路的 I/O 可能也會比較高。因為我們曾經在一個項目做測試的時候發現一個問題,TiDB 執行一個查詢下去了,可能做一個全表掃描,如果 plan 做的不太好的話。他可能會把所有的數據推到 TiDB 層面上。然後 TiDB 再給你做計算,如果 TiDB 跟 TiKV 之間,你做一個全表掃描的時候你會發現,你慢的其實不是TiKV,是你 TiDB 的網卡。比如說,可能帶寬會達到八九百兆,你可能萬兆的(網)感覺不出來,但是如果你是千兆的時候,單台 TiDB 的伺服器的帶寬,有可能會因為你把所有的數據,載入到 TiDB 而把你的帶寬給打滿,所以這塊緩存的話,如果你一個查詢的數據量非常大,TiKV把數據推上來的時候,其實你瓶頸可能在網卡,未必是在你的 CPU 的計算,還有磁碟的讀寫。網卡這塊,如果被打滿的話,其實整個這個執行過程還是挺疼的。
那我們怎麼去控制這個緩存的刷新呢,這也是我最開始做這個 Pull Request 之後,然後 PingCAP 另一個工程師做的 Pull Request,他其實提供了一個 Coprocessor Observer,我通過這個可以去獲取一些在 TiKV 的讀寫的事件,通過這讀寫的事件,我可以對緩存做一定的刷新,讀寫的事件,他一般包括什麼呢,其實這塊的話,我可以告訴大家一些名詞,具體的代碼肯定還得去看裡面的東西。其實在 Region 調度的時候,他會有一些事件,當我 Region 去寫入數據的時候,他也會有一些事件,然後這些事件,就可以為我們來去更新整個緩存的生命周期,比如像 pre_apply 和 post_apply 這兩個系列的請求。
另外一個點就是說,我跟 TiDB 的開發同學交流了之後,他告訴我的一個點,我在 TiKV 去寫數據的時候,其實是有 Critical Section 的,因為寫數據,肯定不是說我一寫一瞬間就落盤了,他可能在落盤和完全落盤之間有一個時間差。在這個時間差的情況下,我需要把對應 Region 的 Cache 關掉,因為這種情況下,會導致我的 Cache 本身 Cache 一個不正確的數據。
這個 PPT 寫的就是整個邏輯,比如在 pre_apply 的時候,我就要關了整個 Region 的 DistSQL Cache,然後我在 post_apply 的時候,我數據已經寫到資料庫,寫入到磁碟的時候,我希望刷新相關 Region 的 Cache。然後再把整個 DistSQL Cache 打開,這個 PR,其實會有一個副作用是什麼呢,因為我引入了 Cache,那麼這個 Cache 應該是全局唯一的一個單件的模式,必然,多線程的去對 Cache 進行讀寫的時候,我肯定要加鎖,加鎖必然會降低我並發的時候請求的性能。為了繞開這個問題,如果你對並發要求很高,然後你對 Cache 這個東西,你發現沒有什麼太大的意義的話,你可以通過配置文件去關掉。其實這個東西很難去避免的。如果大家用的 MySQL 的話,也知道的,如果你把 Query Cache 打開的話,當你並發的環境大的時候。你 show processlist 你可以看到有一部分查詢的狀態是在等待 Query Cache 的鎖,這個 MySQL 本身是有這個問題的。反過來到 TiDB 的話,這個問題,如果你加 Cache 的話,你很難去繞開這個點。但是相比 MySQL 的這個 Query Cache 的 lock 來講的話。TiKV 的 lock,其實還是比較細的。因為他是針對 TiKV Server 這層的,那麼我們來看看效果,這個是在我本地,我自己的筆記本電腦上做的一個測試。
第一個左邊的,我第一次執行的一個 COUNT,我電腦也不是什麼特別好機型,這些行 COUNT 完之後是 1.35 秒,那麼第二次我在執行的時候,其實就應用緩存了。直接從內存里拉出來。這應該是 10 毫秒以下的速度。
謝謝大家。
4 分鐘看完 TiDB DevCon2018
推薦閱讀:
※為 TiDB 重構 built-in 函數
※TiDB 在摩拜單車在線數據業務的應用和實踐
※TiSpark (Beta) 用戶指南
※偷得浮生半日閑—TIDB上海分舵遊記
TAG:TiDB |