從一個簡單的Delete刪資料場景談TiDB資料庫開發規範的重要性

balahoho發表於2021-12-02

故事背景

前段時間上線了一個從Oracle遷移到TiDB的專案,某一天應用端反饋有一個詭異的現象,就是有張小表做全表delete的時候執行比較慢,而且有越來越慢的跡象。這個表每次刪除的資料不超過20行,那為啥刪20行資料會這麼慢呢,我們來一探究竟。

問題排查

根據應用端提供的表名去慢查詢裡面搜尋,確實發現了大量全表刪除的SQL:

從列表中找一條來看看具體的時間分佈:

可以發現絕大部分時間都花了Coprocessor階段,這個階段表示請求已經被下推到了TiKV執行,我們繼續看看在TiKV裡面都做了些什麼。一看嚇一跳,一個很“小”表的刪除竟然會掃描了成千上萬個key:

這一點我們也可以從執行計劃中得出結論,時間幾乎都花在了資料掃描上面:

到這裡為止基本就能判斷出慢的原因就在於掃描了很多無效的key,上面這個例子最終刪除的資料只有9行,但是卻掃描了近80萬個key,很明顯這是由GC引發的一個慘案,因為這個叢集中gc_life_time設定的是48h。至於為什麼要設定這麼大,其中的故事我們不去討論。

問題似乎很簡單,但是這裡面涉及到的知識點很多也非常重要,我覺得有必要做一次系統梳理,防止新手踩坑。

刪資料的原理解析

要搞清楚刪除資料的原理,有幾個東西你必須要知道:

  • TiDB的GC和MVCC

  • Region的概念以及Key的構成

熟悉TiDB的朋友都知道,TiKV底層是直接使用Rocksdb來儲存kv資料,而Rocksdb使用的是LSM tree這種資料結構,它是一種append only模型,也就是說所有對資料的變更都體現在追加上。

這是什麼意思呢?比如說對一行資料做update,體現在儲存上的並不是找到原來的資料直接更新,而是新增一行資料,同時把原來的資料標記為舊版本,這些歷史版本就構成了MVCC,同理delete也是一樣,並不是直接把原資料刪了,而是一種邏輯刪除。

那究竟要保留多少歷史版本,如何去清理這些歷史版本呢,這個就是由GC單元去處理。系統變數tidb_gc_life_timetidb_gc_run_interval可以控制GC的行為,tidb_gc_life_time定義了歷史版本保留的時間,tidb_gc_run_interval定義了GC執行的週期,它們預設都是10分鐘。

Region是TiDB中對資料進行劃分的一種邏輯概念,是資料排程的最小單位,TiDB對資料的分片也體現在Region上。它是由一段連續的key範圍組成,我們可以通過如下方式查詢某張表由哪些Region組成:

Region裡的key是一種有規則的編碼,資料和索引都是以如下的方式轉換為KV鍵值對,最終儲存在Rocksdb中:

我們可以發現同一張表裡的資料,它的key字首都是相同的,這樣就方便對錶進行範圍查詢。

大家有可能看到的startkey和endkey中tableid不是同一個,這種是正常現象,因為對於比較小的表是存在多個表共用一個Region的。

結合前面介紹的GC和Region概念,可以發現如下可能存在的問題(摘自官網文件):

在資料頻繁更新的場景下,將 tidb_gc_life_time 的值設定得過大(如數天甚至數月)可能會導致一些潛在的問題,如:

  • 佔用更多的儲存空間。
  • 大量的歷史資料可能會在一定程度上影響系統效能,尤其是範圍的查詢(如 select count(*) from t)。

所以說,一旦涉及到範圍查詢並且沒有索引的情況下,GC對效能的影響就非常大。恰巧本文的這個delete整張表場景就是典型的全表掃描,這裡的全表掃描指的是掃描這個表包含的所有歷史版本key,而不僅僅是當前你能看到的那些資料。因此,對大表千萬千萬不要這樣清資料,它相當於全表掃一遍,再全表寫一遍,非常恐怖。

大家是不是普遍認為,我只刪9條資料那就掃描這9條資料的key就好了,為什麼要扯上那麼多無關的key?我也認為應該是這樣的,可能實現上有TiDB自己的考慮吧(或許是一個個key去判斷效率更慢?)。

既然我們改變不了這個現狀,那麼如何用正確的方式去刪資料就是要重點關心的了。

刪資料的最佳實踐

實際場景中,刪資料不外乎以下幾種情況:

  • 對某張表按過濾條件批量刪除
  • 刪除某張表的全部資料,俗稱清表
  • 刪表
  • 刪庫

對於第一種,如果結果集很大,最佳做法是把過濾條件進行細化,一批一批的去刪。它的好處是首先不容易觸發大事務限制,其次能夠減少誤刪的情況。不僅僅是批量刪除,批量更新也應該是同樣的做法,把條件拆的更細一些。我常用的做法是,按過濾條件找出對應資料行的rowid,然後把這些rowid進行分段,對這一段的範圍做更新或刪除,這樣能極大提升操作效率。

對於第二種全表刪除,極力推薦使用truncate,它相當於刪表重建新表,所以tableid必然是和以前不一樣了,那就肯定不會掃描到歷史版本資料,刪表建表也只涉及到後設資料操作,速度很快。還有一點,truncate資料以後,被GC掃過的歷史資料會直接清掉釋放出儲存空間,delete操作則不會釋放,要等到compaction才能被再次利用。

對於第三種,沒得選了,只有drop table。

對於第四種,也只有drop database。

那麼問題來了,以上幾種刪資料的方式,萬一是誤刪你想好了如何快速恢復嗎?還是想直接paolu。。。

TiDB開發規範

在這個專案中經歷過好幾次大批量修復資料造成資料庫不穩定的情況,因為這個系統的開發者和DBA都是Oracle背景,他們習慣了一上來就一條SQL對上億的大表做批量操作,這顯然在TiDB中不太適用,動不動就是SQL OOM或者各種too large,再就是導致CPU和記憶體飆升。

我覺得TiDB開發規範在早期的技術選型中就應該是要被重點考慮的一環,要充分了解TiDB的使用方式和限制條件是否能被開發運維團隊接受。確定使用TiDB以後,開發和運維人員還要繼續去落實執行,特別是一些高頻使用場景,這樣才能達到事半功倍的效果。

就比如常見的加索引,TiDB在有了資料以後加索引是特別慢的,而且是個序列操作。如果你發現有個join查詢特別慢,需要給兩張表分別加上索引,是馬上就加嗎,先加哪一個,加幾個合適?

社群裡有一篇非常全的開發規範說明值得每一位去細讀,希望大家都能收藏,時不時翻出來看看。

https://asktug.com/t/topic/93819

總結

本文提到的場景只是這個專案中的一個縮影,因為專案週期原因應用端很多不好的SQL(你能想象到還有where or幾千個條件?)都沒有來得及優化,所以暴露出了正確使用TiDB的重要性。

沒有絕對完美的產品,我們要充分了解它的原理,使用的時候做到揚長避短,這樣既能發揮它的價值也能提升我們的效率。

相關文章