這次談談這個大救星的具體實現細節。

我前段時間發的「DBA和運維同學的大救星來了」的文章,引起了業界同行的廣泛關注,很多DBA和運維同學表示,能夠找回刪除的庫表(下文簡稱 「庫表回收站」 功能)對他們會非常有幫助,希望能夠合併到官方mysql代碼中。目前來說,庫表回收站是TDSQL 特有的功能,未來的話,什麼好事都有可能發生的。 今天我主要講一下,我是怎麼設計和實現這個庫表回收站功能的,另外還會講一下與之相關的表文件慢速刪除功能。

在此之前先補充介紹一下自上次撰文後,我對這塊功能做了一些調整,主要是當你drop table t1;或者drop database db1後再立刻create table t1或者create database db1的話,原來是會失敗報錯的,現在修改為可以成功。並且這樣的drop->create可以不做等待無數次重複。示例如下:

MySQL的表定義簡介

首先簡述一下mysql的元數據管理。 MySQL的表定義是存儲在一個 .frm文件中的,每個表有一個同名(這裡暫且忽略lower_case_table_names的細節)的.frm 文件,該文件位於數據目錄的database子目錄下面。比如test.t1這個表,它的frm文件和ibd文件位於該db實例的數據目錄下的test/t1.frm 和test/t1.ibd 。 一個表的frm文件存儲這個表的元數據,包括庫表名稱,每個列的信息(名稱,數據類型,長度,約束等),索引信息,使用的存儲引擎,trigger,表分區定義等。每執行一個SQL語句,都需要在本連接中打開這個語句使用的每個表,語句執行結束後關閉這些表。打開一個表的時候,mysql會讀取frm文件的元數據信息,裝入一個TABLE 對象,這些信息都是查詢執行過程中會用到的。另外,視圖的定義也是存儲在frm文件中的,不過其內容和格式與表的frm完全不同,本功能不涉及視圖,因此不再贅述。

隱藏和找回表的實現表的Frm文件頭部數據結構中,有一些未使用的空白位元組(值為0),這些位元組不存儲任何信息,並且其中很多位元組是mysql沒有為未來預留的,我的實現就利用了其中的第46個位元組(下文簡稱為hide_byte,意為表的隱藏位元組),該位元組為0代表這個表可見,為1代表它被隱藏了。另外說一句,由於下一個mysql大版本(8.0)已經不再使用frm文件,所以也更不需要擔心會與未來的mysql版本衝突。 當mysql執行一個drop table語句時候,在獲取mysql server層的表級IX lock並且許可權檢查通過後,就會刪除其frm文件並且通過handler介面調用存儲引擎的表刪除功能。在TDSQL加入庫表回收站功能後,執行drop table t1; 語句時,不會刪除frm文件也不會做存儲引擎的表刪除功能,而是隻把其hide_byte從0修改為1。當打開一個表的時候,總是檢查hide_byte,如果是0則正常打開,如果是1則open_table_def() 函數返回特殊的錯誤,由上層調用代碼處理。這樣隱藏表的好處是mysql server層完全無法看到這個表的存在,但是表的定義文件(.frm)和數據文件(.ibd)都完好無損。這個表對於innodb來說仍然存在,所以會對它做正常的維護,包括redo/undo日誌的處理,purge,刷臟頁,change buffering合併和啟動時刻的數據恢復。另外,表文件也不需要為了隱藏而做任何重命名等操作。 當需要找回一個隱藏的表時候,只需要把這個hide_byte重新改為0,即可正常打開了,這個表也就找回了。當show tables時只返回hide_byte為0的表;當show hidden tables 時只返回hide_byte為1的表。當drop table immediate時候就不再設置hide_byte 而是按照原來的做法刪除表。 如果在drop table t1後立刻執行create table t1,那麼會對已經存在的隱藏的t1表做rename:當再次create table t1的時候,發現已經有重名的隱藏的表t1,所以就rename這個t1表(而不是rename t1.frm文件),給表名後綴一小段tdsql特定的字元串(_tdsqlhid_6個數字)。這6個數字是操作執行當時的time()秒數後3位和微秒數的後3位。當找回一個表時,如果這個表已經被重命名為後綴了tdsqlhid_6個數字的話,這個表名並不會修改,用戶需要手動rename為他需要的名字。如果這個後綴導致表名超長的話,那麼第二次create table就會失敗。這段話如果不夠形象的話,請結合本文開頭的示例來理解。

讀者知道為什麼不能簡單地重命名frm文件和/或對應的ibd文件嗎? 歡迎在評論中留言回答。

隱藏和找回資料庫Mysql中一個資料庫對應數據目錄下面的一個同名目錄(暫不考慮lower_case_table_names的細節)。由於db沒有類似表的frm文件,所以隱藏db的方法,就是重命名這個db,在其名稱末尾增加 『_tdsqlhid_6個數字』。也就是用』tdsqlhid』來標識一個db為隱藏的db。這6個數字也是操作執行當時的time()秒數後3位和微秒數的後3位。當create database時候,禁止名稱中出現包含 tdsqlhid字元串; 當要打開一個表的時候,禁止庫名中包含tdsqlhid字元串,也就是不許訪問隱藏的庫中的表;當show databases;時候,不顯示包含』tdsqlhid』字元串的db;當show databases hidden;的時候,只顯示名稱中包含tdsqlhid的db。當要找回一個隱藏的db時候,需要重命名這個db,去掉其中的tdsqlhid字元串,並且用戶可以在形如expose database test_tdsqlhid_123456 to test1的語句中為找回的庫test_tdsqlhid_123456指定新名稱test1。見本文開頭的示例。 重命名一個db,主要涉及的重命名其中的表,把每個表的db名稱改為新db名稱;以及重命名db目錄。讀者知道為什麼不能簡單地重命名db目錄名文件嗎? 歡迎在評論中留言回答。庫表隱藏與複製與隱藏,找回,刪除庫表相關的所有sql語句都會記錄binlog並且備機能夠正確地複製執行。庫表的隱藏功能,是由2個session變數drop_hide_table和drop_hide_db來動態打開或者關閉的,默認是打開的。如果關閉的話則刪除庫表將按照原來的方式進行,不需要加immediate 就可以立刻刪除庫表。這就要求複製的時候對每一個drop table/drop database語句的binlog事件,記錄該語句執行當時的drop_hide_table和drop_hide_db的值,並且在複製的時候,根據該binlog事件中記錄的值來設置slave的對應的drop_hide_table和drop_hide_db session變數,從而在slave這邊也做相同的事情(隱藏或者直接刪除)。記錄的方法是使用binlog事件頭部的一個空閑的標誌位。 庫表隱藏功能要求備機必須也是tdsql的percona/mariadb資料庫。如果主機是tdsql的percona/mariadb 資料庫但是備機不是的話,那麼主機上面隱藏的庫表會在備機那邊直接刪除。然後,主執行的expose table/database語句在備那邊根本無法執行。庫表隱藏與資料庫恢復

每次隱藏一個庫表時,這個庫表名稱會記錄到後臺刪除線程所使用的一個全局對象中,然後後臺線程定期掃描待刪除庫表,達到計時時間的就做真正的刪除。當mysqld每次重啟時,會掃描數據目錄,找到所有已經隱藏的庫表,讓後臺線程重新開始計時刪除。

讀到這裡,不知道讀者有沒有想到一個問題:mysqld重啟之後到了keep_hidden_hrs(tdsql新增的變數)個小時之後,後臺刪除線程要在同一個時間點刪除所有啟動期間掃描到的隱藏的庫表。這會對機器的文件系統帶來一個突增的負載,而且可能持續較長時間,可能導致機器上面所有db實例在此期間性能嚴重下降。 這在tdsql上面是不會發生的,為什麼呢? 請看下文的「表文件慢速刪除」 這部分。 與資料庫恢復相關的另一個事情就是庫表重命名,這與我在上文中給讀者提出的兩個問題有關,這裡先賣個關子,歡迎大家在評論中說出自己的理解。表數據文件慢速刪除當一個表真正要做刪除時候,如果其數據量很大,比如幾十GB,那麼刪除這麼一個文件會對io設備帶來突增的負載,在負載突增期間這個機器上面所有的db實例的qps會有一個深深的波谷,響應時間會有一個高聳的波峯,這對於很多業務來說是無法忍受的。所以,tdsql的percona和mariadb資料庫內核中,我實現了一個慢速刪除的機制,來避免這種波峯和波谷。如何做慢速刪除呢? 首先要講一下文件系統的工作原理。與本文相關的文件系統工作原理,有以下兩部分:1. 空閑空間管理主要是把空閑的不相鄰的磁碟塊串聯為一個鏈表。當需要分片若干個磁碟塊時候,就從空閑鏈表中去找;當有文件被刪除或者截斷而釋放了磁碟塊時候,需要把這個文件的磁碟塊合併到空閑鏈表中。注意這個合併過程,不是簡單地把文件的所有塊串聯到空閑鏈表末尾,而是要合併到空閑鏈表中,比如空閑鏈表是(1,2)->(4,5)->(9,10),現在要合併磁碟塊3,那麼合併後的磁碟塊就是(1,5)->(9,10)。只有做合併纔能有效地做大塊空間的分配,在ssd之前的時代,連續存放的文件,讀寫都要快的多。合併磁碟塊是truncate或者刪除文件的操作中負載最高的部分。我這個慢速刪除就是把這部分操作,拉長了。2. 文件空間管理每個文件的磁碟塊形成一個鏈表,這些磁碟塊通常並不相鄰。隨著文件的增長,文件系統為該文件分配更多的磁碟塊。當文件被truncate時候,就把磁碟塊歸還給文件系統。

所以,慢速刪除一個文件,其實就是把歸還和合併一個文件中所有的磁碟塊這個高負載的操作,分批來做。通過多次調用truncate()系統調用,分多步驟每次把一個數據文件縮小16MB,直到最後它小於16MB 再一次性刪除,兩次truncate之間等待一段時間(由file_slow_delete_rate變數控制,該變數是慢速刪除的速率,單位是MB/s)。例如,本來要一次性歸還併合並的一千萬個磁碟塊,分為1000次,每次1萬個,這樣文件系統的負載就不會突增了。當執行drop table t1的時候,在innodb內部,我會把t1.ibd文件重命名為t.ibd.delayed_drop,然後放到慢速刪除線程的任務隊列中。慢速刪除線程會按照上述做法完成該文件的縮小和最終刪除。當mysqld啟動時候,會掃描其數據目錄中所有的*.delayed_drop文件,放入其工作隊列中,把它們慢慢處理掉。 目前慢速刪除只對innodb的表文件有效,不處理rocksdb和myisam等。Myisam想處理並不難,不過tdsql本來就禁止用戶使用myisam表的,所以不做支持。而rocksdb由於其特殊的存儲方式,數據並沒有以表為單位分別存放到不同文件中,因而想做到表級別的慢速刪除是不現實的。另外,XFS文件系統已經避免了刪除大文件導致的io負載突增,我沒有看XFS的技術文檔,但是我想它可能有兩種辦法避免這個問題:1. XFS可能把合併被刪除的磁碟塊這個動作,做成了非同步操作,在後臺進程中完成。現在的文件系統也有事務日誌的,完全可以做可靠的恢復;2. XFS可能從空閑空間管理方面做了改變,不再合併相鄰磁碟塊。在SSD時代,磁碟塊隨機讀寫與順序讀寫是同樣的性能,所以就不需要合併了,直接把文件的磁碟塊列表掛到空閑磁碟塊鏈表末尾(或者頭部)即可。 本文到此結束,歡迎大家留言回復討論。
推薦閱讀:
相關文章