InnoDB記錄壓縮及使用分析

這篇文章,源於RDS組內的一次飯後閑聊,兩位小夥伴在探討InnoDB啟用壓縮後的種種,比如在磁碟上是怎麼存放數據的,數據頁的大小是多少?怎麼知道一個頁裡面可以寫入多少壓縮前的數據等等。兩年前曾看過InnoDB壓縮的文檔和代碼,現在好多東西都模糊了,趁此機會,又重溫了一把,下面通過問答的方式來闡述。

壓縮幹嘛用呢?直白點,就是為了省空間。假如你的手機是16G版iphone 6s,那麼你肯定明白可用的存儲空間是多麼寶貴。如果能夠把10G的數據變成5G甚至2G,而且還能不丟信息,那麼你是不是樂翻了,做夢都在笑吧。好了,壓縮就是干這事兒了。當然在iphone上壓縮這招一般是行不通的,不信你試試。

壓縮是百利而無一害的嗎?騷年,如果有這種想法,那真是too young too simple了。雖然壓縮有可能節省存儲空間,但也是有代價的,壓縮是要消耗cpu資源的,甚至是過多的cpu資源。目前已知的壓縮演算法有很多種,不同演算法的壓縮比和cpu消耗也是不一樣,常用的演算法中zlib的壓縮比較高,但cpu消耗也偏高,早期程序一般採用zlib較多,如InnoDB的壓縮等。snappy和quicklz在壓縮比和cpu消耗上保存了較好的平衡,這兩種演算法在新開發的軟體中使用更為廣泛,包括TokuDB、MongoDB等。當然,如果你選擇JPG等已經壓縮過的圖片進行壓縮,那就是no zuo no die了,誰都救不了你。

什麼樣的數據適合做壓縮呢?我們拿MySQL表作為例子,通常情況下表結構中包含字元型數據列如char, varchar, text或blob等時,具有較高的壓縮率,而一些二進位數據,如整形或浮點型數據列,或者一些已經壓縮的多媒體文檔,如jpeg、jpg、png等格式圖片及mp4、avi等格式視頻,其壓縮率都不會好。你可以從一個非壓縮表中拷貝數據到一個相同的壓縮表中,觀察數據大小,來決定是否適合壓縮。

假如我的MySQL實例的表中就存在較多字元型數據列,我想啟用壓縮,怎麼整?不急,給你寶典,自己去練。首先你的MySQL版本必須高於5.1,現在都已經進入5.7時代了,這個不成問題。接下來把innodb_file_per_table設置為1,即除了系統表外,其他表都是單獨存一個文件,還需要把innodb_file_format設置為Barracuda。然後在建新表或修改現有表的語句中加入row_format=compressed key_block_size=8就可以了。你可以僅加入row_format=compressed,這樣key_block_size就取默認值8KB了。你也可以僅加入key_block_size={1/2/4/8/16},也會默認開啟壓縮。那麼,key_block_size到底設置為多大才合理呢,這是一個深奧的問題,官方建議如下:To determine the best value for KEY_BLOCK_SIZE, typically you create several copies of the same table with different values for this clause, then measure the size of the resulting .ibd files and see how well each performs with a realistic workload。

我的MySQL業務類型會對壓縮效益有影響嗎?為了盡量避免壓縮、解壓帶來的額外消耗,InnoDB在壓縮頁中新增了modification log區,通過記錄當前的頁的修改日誌,來避免頻繁壓縮解壓。不同的業務場景,會對壓縮效益產生不同的影響,下面幾種場景相對適用壓縮:如果業務中查詢佔了絕大部分,只有很少的更新,那麼只有較少的頁中會出現modification log空間不足,進而需要重組或者重新壓縮,這種場景下是個不錯的選擇;如果應用只是單純的插入,此時數據的插入按照主鍵遞增的方式組織,即使存在二級索引也基本上不需要頻繁重組和重壓縮;對於刪除操作,由於InnoDB刪除行採用打標誌位的方式來刪除,對記錄的刪除是通過修改頁中沒有被壓縮的元數據的方式實現,所以效率很高;對於更新操作,如果不是對索引列或者存儲在off-page的blob,text,長字元串的列的更新,這種場景下使用壓縮也是可以接受的。

以上說了那麼多,壓縮最終還是通過消耗更多的cpu資源來換取減少IO消耗,最終帶來性能的提升,如果應用是IO密集型,而不是cpu密集型,那麼可以利用剩餘的cpu來提升應用性能。

InnoDB的壓縮是怎麼實現的呢?其實,截止目前InnoDB已經包括兩種壓縮模式,一種是這裡提到的對記錄(包括索引)的壓縮。該壓縮從MySQL 5.1開始既已存在;另一種是在MySQL 5.7中加入的,是對數據塊的壓縮,本文不對其做介紹,有興趣的同學可以查看ks.netease.com/blog?及後續分析。

這裡對記錄壓縮做簡單介紹:InnoDB啟用壓縮後,btr_page_create會調用page_create_zip從磁碟分配一張空的大小為zip_size(即key_block_size)的壓縮頁,該頁在buffer pool(bp)中駐留,不斷接收插入及更新等操作的數據,InnoDB的flush或checkpoint機制將該頁數據寫回磁碟,在回刷前會調用page_zip_compress進行壓縮後,其實也不會每次寫入磁碟都會進行壓縮,就像之前問答中已經提到的,InnoDB壓縮頁中開闢有modification log區,壓縮頁中一段時間內的記錄增刪改會先寫入該區,再通過一定條件觸發合併,合併的過程就是將原來的壓縮數據解壓,再將modification log區的內容合併掉,重新進行壓縮。隨著數據的不斷寫入,到了一個時間點,總會出現壓縮後的數據大小仍超過了設定的key_block_size,這種場景下會觸發壓縮頁的分裂,此時就需要分配一個新頁,將原頁部分數據移到新頁中,再分別進行壓縮。如此,周而復始。

在內存充足的情況下,InnoDB bp中包括壓縮頁的壓縮狀態(buf_page_t*)page->zip.data和解壓狀態(buf_block_t*)page->frame。當內存不足時,InnoDB可能將解壓的frame回收,保留壓縮的zip.data在bp中。如果一個壓縮頁在一段時間內沒有被使用,壓縮格式的zip.data也會被寫回到磁碟中,以釋放內存。

Innodb使用一個自適應 LRU演算法來維持bf內zip.data和frame的平衡,目的是為了避免在cpu繁忙時減少解壓頁的開銷,當cpu富餘時避免過多的IO。當系統是IO密集型時,傾向於回收frame,以留下內存給其他從磁碟讀入的頁。當cpu密集型時,InnoDB選擇全部回收zip.data和frame,也就是回收整個壓縮頁,這樣內存可以更多的留給熱點數據,並減少內存中需要解壓的頁。

最後,如何逼格更高得使用InnoDB壓縮呢? InnoDB一如既往得友好,information_schema中提供了兩組系統表來查看運行時的壓縮行為。

系統表innodb_cmp和innodb_cmp_reset用來分析運行中的壓縮狀態,包括壓縮頁的壓縮/解壓次數,所花費時間,壓縮成功次數以及壓縮頁的大小等。其中innodb_cmp保存歷史匯總數據,而innodb_cmp_reset則記錄的是一個較為實時的統計值,表結構如下:

mysql> show create table innodb_cmpG

*************************** 1. row ***************************

Table: INNODB_CMP

Create Table: CREATE TEMPORARY TABLE `INNODB_CMP` (

`page_size` int(5) NOT NULL DEFAULT 0,

`compress_ops` int(11) NOT NULL DEFAULT 0,

`compress_ops_ok` int(11) NOT NULL DEFAULT 0,

`compress_time` int(11) NOT NULL DEFAULT 0,

`uncompress_ops` int(11) NOT NULL DEFAULT 0,

`uncompress_time` int(11) NOT NULL DEFAULT 0

) ENGINE=MEMORY DEFAULT CHARSET=utf8

1 row in set (0.00 sec)

mysql> show create table innodb_cmp_resetG

*************************** 1. row ***************************

Table: INNODB_CMP_RESET

Create Table: CREATE TEMPORARY TABLE `INNODB_CMP_RESET` (

`page_size` int(5) NOT NULL DEFAULT 0,

`compress_ops` int(11) NOT NULL DEFAULT 0,

`compress_ops_ok` int(11) NOT NULL DEFAULT 0,

`compress_time` int(11) NOT NULL DEFAULT 0,

`uncompress_ops` int(11) NOT NULL DEFAULT 0,

`uncompress_time` int(11) NOT NULL DEFAULT 0

) ENGINE=MEMORY DEFAULT CHARSET=utf8

1 row in set (0.00 sec)

如果compress_ops_ok/compress_ops的比率很高的話,則表明系統運行的良好,反之則表明InnoDB經常進行reorganize, recompress和split,此時對該表禁用壓縮或者選擇更大的key_block_size。

InnoDB bp使用夥伴分配系統(buddy allocator)來分配不同大小的內存頁用來緩存壓縮數據:1KB到16KB(即對應不同的key_block_size),information_schema為此提供了innodb_cmpmem和innodb_cmpmem_reset來記錄壓縮頁使用bp的一些信息,包括:bp為壓縮頁分配的正在使用的頁數,可供使用的空閑頁數,夥伴系統重新分配頁的次數及時間等,詳細表結構如下:

mysql> show create table innodb_cmpmemG

*************************** 1. row ***************************

Table: INNODB_CMPMEM

Create Table: CREATE TEMPORARY TABLE `INNODB_CMPMEM` (

`page_size` int(5) NOT NULL DEFAULT 0,

`buffer_pool_instance` int(11) NOT NULL DEFAULT 0,

`pages_used` int(11) NOT NULL DEFAULT 0,

`pages_free` int(11) NOT NULL DEFAULT 0,

`relocation_ops` bigint(21) NOT NULL DEFAULT 0,

`relocation_time` int(11) NOT NULL DEFAULT 0

) ENGINE=MEMORY DEFAULT CHARSET=utf8

1 row in set (0.00 sec)

mysql> show create table innodb_cmpmem_resetG

*************************** 1. row ***************************

Table: INNODB_CMPMEM_RESET

Create Table: CREATE TEMPORARY TABLE `INNODB_CMPMEM_RESET` (

`page_size` int(5) NOT NULL DEFAULT 0,

`buffer_pool_instance` int(11) NOT NULL DEFAULT 0,

`pages_used` int(11) NOT NULL DEFAULT 0,

`pages_free` int(11) NOT NULL DEFAULT 0,

`relocation_ops` bigint(21) NOT NULL DEFAULT 0,

`relocation_time` int(11) NOT NULL DEFAULT 0

) ENGINE=MEMORY DEFAULT CHARSET=utf8

1 row in set (0.00 sec)

在使用InnoDB壓縮時,周期性觀察information_schema中提供的這兩組壓縮相關的臨時表,對於評估壓縮的效果和性能非常有參考價值。
推薦閱讀:

TAG:資料庫 | MySQL | 數據壓縮 |