吳鏑:TiDB 在今日頭條的實踐

PingCAP發表於2018-03-16

吳鏑:TiDB 在今日頭條的實踐

本文整理自今日頭條資料庫中介軟體/分散式資料庫負責人吳鏑(知乎 ID:吳鏑)在TiDB DevCon2018 上的分享內容。

TiDB 主要應用在今日頭條核心 OLTP 系統 - 物件儲存系統中,儲存其中一部分後設資料,支援頭條圖片和視訊相關業務,比如抖音等。

如今(資料截至發文),TiDB 支撐著今日頭條 OLTP 系統裡 QPS 比較高的場景:叢集容量約幾十 T,日常 QPS 峰值會達到幾十萬。

為什麼我們需要用 TiDB

今日頭條內部有一些業務資料量非常大,之前用的 MySQL 的單機盤是大概 2.8T 的 SSD 盤。我們做物件儲存。因為頭條不但做視訊,還做圖片,這些視訊和圖片當中基本上都是用我們自研的 S3 儲存系統,這種儲存系統需要一個後設資料,比如一個圖片存下來,它存在 S3 系統的哪個機器、哪個檔案、哪個偏移裡面的資料,還有比如一個大的視訊,S3 會把它切成很多小的視訊片段,每一個分片的位置,都會存在後設資料裡面。

用 TiDB 之前,後設資料是存在 MySQL 裡的一個 2.8TB 的盤,因為增長的特別快,所以導致磁碟不夠用,只能用分庫分表的方案。我們以前用的的分庫分表方案是 MyCAT。但用這個方案的過程中我們有遇到了一些問題,比如丟資料。某一個資料我 commit 了之後,最後發現這個資料丟了。

再就是連線的問題,目前頭條做分片是大概固定分 100 個片。如果你的業務是需要分庫分表,那你這邊搞 101 個分片,這樣有些業務,他用了一個分片鍵,用分片鍵來做查詢,那可能中介軟體只用一個連線就可以找到相關資料。但有些業務,確實有不帶分片鍵的請求。會導致 select 語句過來的時候,下面會建 101 個對後端的連線,也就是說,因為有連線的限制,有一個沒有帶分片鍵的這種請求過來之後, MyCAT 可以啟 101 個連線到後面的每一個 MySQL 庫。那這樣的話,有時候我給它 5 萬個連線,他一下子就把一百萬用掉了。這樣會導致它在非分片鍵的 select 請求,它連線速度消耗非常快,經常在業務這邊會丟擲說,連線數不夠。

頭條的資料庫主要用的是 MySQL 和 MongoDB,相對比較單一,所我們也想多嘗試一些其他的資料庫。

主要使用場景

目前,TiDB 主要在以下兩個場景下使用:

首先是 OLTP 的場景,也就是大資料量的場景,我們不僅僅是考慮到延時,而是考慮到資料量單機裝不下,需要擴充套件性;

還有 OLAP 場景,有些使用者,他用的是 Hive 或者 Tableau,然後用的過程中發現,因為後面都是接 MySQL,做一些 OLAP 的方式查詢就比較慢。後來公司在推廣 TiDB,所以就接了一些 OLAP 的業務場景。

頭條的自研物件儲存系統後設資料量非常大,而且增長非常快。以其中最大的一個叢集舉例:該叢集有兩種方式,一是分片資訊最早是用 MySQL 的。如果想用 TiDB 的話,可能先得把 TiDB 做了 MySQL 的備,用 TiDB 提供的 syncer 來同步資料,有些讀請求我們可以從 MySQL 上切到 TiDB 上來。

我們用了一段時間,覺得 TiDB 其實挺穩定的。然後,公司會有這種需求,比如說突然接了一個元旦的活動,這個時候上傳的圖片就比較多,資料增長的就太大了,這種活動中 S3 系統壓力比較大。我們 MySQL 的單盤基本上穩定的在 2.0TB 以上(盤總記 2.8TB),對此我們就只能刪資料(一些很老的資料),跟業務部門溝通說,這個資料不要了,從 MySQL 的單盤裡刪掉,通過這種方式來支撐。

但即使這麼做,單盤還是扛不住現在資料增長的需求。然後當時就想幹脆激進點,把一些寫進來後立即就讀、並且以後都不會讀的一些流量切到 TiDB 裡。因為 S3 儲存分很多 bucket ,做活動的人就去新建一些 bucket, 這些 bucket 的後設資料就直接存在 TiDB 裡面,就不存 MySQL 了。

這兩個 case,就是目前在頭條的 OLAP 和 OLTP 裡資料流量最大、QPS 最大的一個場景。

叢集部署狀態

關於部署,我們把 TiDB 和 PD 部在一起,都是 3 個。TiKV 我們一共是用了幾十臺的機器。CPU 是 40 個虛擬的 CPU,256G 的記憶體。

目前平均值 QPS 在十幾萬,用了 3 個 TiDB,3 個 TiDB 總的連線數加起來大概 14K,然後 Latency 的 pct99 小於 60ms。這其實都屬於挺高峰時期的資料了,做活動的時候 QPS 會達到幾十萬。

與 MySQL 的延時對比

在使用 TiDB 過程中,我們也比較了一下 TiDB 和 MySQL 的延時:

吳鏑:TiDB 在今日頭條的實踐

第一條線就是 MySQL 的延時,pct99 的,下面的黑線是 TiDB 的延時。可以看到,在 MySQL 的資料量非常大的情況下,TiDB 是明顯 Latency 更優的,雖然說它用的機器會稍微多點。

一些使用中的吐槽和經驗

使用的過程中我們也碰到了一些槽點,這些槽點 TiDB 現在的版本已經基本都解決了。

第一個就是直方圖。大家知道基於 CBO 的這種優化器,肯定要用一些統計資訊,TiDB 在之前的版本里對直方圖的統計資訊的更新沒有做到很及時,導致我拿了一個 SQL 選執行計劃的時候我會選錯。比如說我可以選一個索引,但是實際上,因為這個更新資訊不實時,所以它可能會做全表掃描。

大家以後發現這種你可以用 explain 這個命令看執行計劃,如果有這樣的問題就可以用 analyze 這個命令,他可以把一張表統計資訊給更新,更新之後再一次執行 SQL 語句,你會發現他的執行計劃已經變了。

第二個就是 raft leader。因為大家都知道,每個 region 是一個 raft ,TiDB 有一個監控指標,給出每個機器上有多少個 raft leader。當我們資料量跑到 10TB+,大概 20TB 的時候,會發現這個 raft leader 頻繁掉線。掉線的原因主要是由於做 region 遷移的時候,比如你後邊做遷移或者做負載均衡,會把 RocksDB 裡面一個 range 的資料發到你要遷移的目標機器上面去。發過去了之後,目標端相當於要把 SST 檔案 load 到 RocksDB 裡,這個過程中,由於 RocksDB 實現的一個問題,導致把 SST 加到 RocksDB 的裡面去的這個過程花費了大概 30 到 40 秒,正常情況下可能就毫秒級或者 1 秒。RocksDB 實現 ingest file 的時候,它開啟了一些其實不需要開啟的檔案。因為 LevelDB、RocksDB 有很多層,把一個 file 給 ingest 進去的時候其實你要和一些 overlap 的資料做合併,因為它的實現問題,導致有一些沒有必要去 touch 的 SST 它都會去 touch,會產生大量 IO 。因為我們資料量比較大, SST 就非常多,所以在資料量非常大的情況下就會踩到這個坑。

然後,RocksDB ingest 一個檔案時間過長,導致 Raft 的心跳就斷了。因為 Raft 協議要維持你的 lease,你要發心跳包,這個時候心跳包都給堵在後面,因為前面 ingest file 時間太長了。然後 Raft leader 就掉,掉了以後很多讀寫請求就會有問題。

第三個是大量的短連結。我們的業務使用資料庫的時候,經常建了非常多短連結。因為大部分業務都是不大會使用資料庫的,它也不知道要設定連線池,idle connection 這種東西。所以經常用完一個連線後就關掉。這種大量的短連結最後打到 TiDB,TiDB 連線建立了之後要去查一個 System 的變數,這些變數在 TiDB 裡面是存在某幾個 TiKV 例項裡面的,那如果有大量短連結,這些短連結一上來,就會去查這些系統變數,剛好這些系統變數就聚在幾臺機器上面,導致說這幾臺機器就負載特別大。然後就會報警讀請求堆積。TiKV 使用的是執行緒模型,請求過來之後,丟到佇列裡面去。然後執行緒再拿出來處理。現在 PingCAP 也在做優化,把這些 Cache 在 TiDB 這個程式裡面。

第四點,嚴格來說這不算是 TiKV 的問題,算是 prometheus 的客戶端有問題。我們當時遇到這麼一個情況:部署 prometheus 的這個機器宕掉了,重啟之後,我們會發現很多 TiKV 的監控資訊都沒有上報。後來查的時候發現壓根 TiKV 這臺機器都沒有到 prometheus 這臺機器的連線。所以我們就覺得 prometheus 這邊客戶端實現有問題。

第五個問題就是 Row id 的打散。這個問題正好是我們這邊碰到的一個效能上的問題。因為 TiDB 儲存資料是這麼存的:我要插入一行資料,他會有兩行,第一行是索引,索引是 Key ,然後 value 是 row id;第二行是 row id 是 Key,value 是整行的資料,相當於第二行有點像聚集索引這種東西。但是這個聚集索引的 Key 是 row id。原來的版本實現上是說這個 row id 是個遞增了,所以這種就導致不管你插入什麼資料,這個 row id 都是遞增的,因為 row id 一遞增,這些資料都會打到一個 TiKV 的一個 region 上面。因為我的 TiKV 是一個有序的 Map,所以說 row id 如果遞增的話,肯定大家插入的時候都是打到一個 TiKV 上面。我們當時業務的壓力比較大,導致客戶發現他把這個業務的機器例項數給擴容上去之後,會發現這個 insert 的 TPS 大概也就在兩萬,一行大概就一百多個位元組吧,你再怎麼加他上不去了,也就是說 insert 的這個 QPS 上不去了。

這一點 TiDB 新版本的方案就是,row id 不是單調遞增,而是把 row id 打的很散,這種方案效能會比較好,沒有熱點。

最後這個問題,因為 TiDB 這種事務模型,是需要拿一個事務版本,這個事務版本在 TiDB 裡面是一個時間戳,並且這個時間戳是由 PD 這個元件來管理的。相當於每一個事務基本上連上來之後,它都要去訪問 PD 這個元件拿時間戳。其實做 rpc 的時候拿時間戳延遲不會太長,也就是個位數毫秒級。但因為 TiDB 是 Go 寫的,有排程開銷。從 PD 拿回來一堆時間戳的 goroutine 把這堆時間戳發放給執行事務的一堆 goroutine 很慢,在連結數和壓力都比較大的時候,大概有 30 毫秒左右的延時。可能調 rpc 的時候也就大概需要 1 毫秒,不到 2 毫秒。但由於 Go 的開銷,能把這個延時翻幾倍。

以上這些講的都是 TiDB 在頭條做 OLTP 的場景下,碰到的一些主要的問題,這些問題大部分現在已經修復。

頭條在 OLAP 上的一些應用

在 OLAP 的場景下內容就比較少了。前面的一些業務喜歡用 tableau 這種客戶端後面連線 MySQL,這就太慢了。可以用 syncer 把一些資料從 MySQL 同步到 TiDB。

這就可能碰到一個問題:我們公司有一個元件,是會把 Hive 的資料批量的同步到 MySQL 的一個工具,很多做資料分析的同學就會把 Hive 裡的資料同步到 TiDB。但是這個工具產生的事務非常大,而 TiDB 本身對事務的大小是有一個限制的。

此時,把下面這兩個配置項開啟之後,TiDB 內部會把這種大的事務切成很多小的事務,就沒有這個問題:

  • set @@tidb_batch_insert =ON

  • set @@tidb_batch_delete = ON

有事務大小的限制主要在於 TiKV 的實現用了一致性協議。對於任何一個分散式資料庫,如果你要用一致性協議去做這種複製,肯定要避免非常大的事務。所以這個問題不是 TiDB 的問題。基本上,每個想要做分散式資料庫的肯定都會碰到這麼一個問題。在 OLAP 場景下,大家對資料的事務性要求沒那麼高,所以把這個配置項開啟沒什麼問題。

這就是頭條在 OLAP 上的一些應用:比如說 ugc 點選量,app crash 的需求是客戶端請求掛掉之後,要打一個 log 在 TiDB 的叢集裡面。druid 這個 OLAP 這個引擎,他會有 MySQL 的資料做後設資料,有些人就把這個後設資料存在 TiDB 上了,還有一些問答業務,也是把一些相關的資料放到 TiDB 上。

相關文章