MySQL大文字儲存壓縮

語言: CN / TW / HK

theme: devui-blue

背景

前面提到,我們有個雲文件專案的快照內容是直接儲存到db的,屬於大文字儲存,文件快照的內容欄位大部分都是kb級別,部分甚至到MB級別。目前對於資料的讀取,已經進行了CDN快取優化(靜態資源快取利器——CDN),對於資料的寫入和儲存還有待優化,如果可以通過一些壓縮演算法在大文字進行壓縮儲存,可以在很大程度上節省DB的儲存空間,緩解DB的I/O壓力。

存量資料分析

sql select table_name as '表名', table_rows as '記錄數', truncate(data_length/1024/1024, 2) as '資料容量(MB)', truncate(index_length/1024/1024, 2) as '索引容量(MB)', truncate(DATA_FREE/1024/1024, 2) as '碎片佔用(MB)' from information_schema.tables where table_schema=${資料庫名} order by data_length desc, index_length desc; image.png image.png

相關內容介紹

innodb引擎頁資料超出16kb怎麼辦?

我們都知道innodb的頁塊預設大小為16k,如果表中一行資料長度超出了16k,就會出現行溢位,溢位的行是存放在另外的地方(uncompress blob page)。由於innodb採用聚簇索引把資料進行存放起來,即B+Tree結構,因此每個頁塊中至少有兩行資料,否則就失去了B+Tree的意義,這樣就得出一行資料最大的長度限制為8k(大欄位在資料頁會儲存768個位元組資料,剩餘的資料溢位到另外的頁中,資料頁還有20個位元組記錄溢位頁的地址) * 對 dynamic 格式來說,如果大物件欄位(text/blob)儲存資料大小小於 40 位元組,那全部放在資料頁,剩餘的場景,資料頁只保留一個 20 位元組的指標指向溢位頁。 這種場景下,如果每個大物件欄位儲存的資料小於 40 個位元組,也就和 varchar(40),效果一樣。 * innodb-row-format-dynamic:https://dev.mysql.com/doc/refman/8.0/en/innodb-row-format.html#innodb-row-format-dynamic

Linux 稀疏檔案 & 空洞

  • 稀疏檔案(Sparse File):稀疏檔案與其他普通檔案基本相同,區別在於檔案中的部分資料全為0,且這部分資料不佔用磁碟空間
  • 檔案空洞:檔案位移量可以大於檔案的實際長度(位於檔案中但未被寫過的位元組被設為0),空洞是否佔用磁碟空間由作業系統決定
    • image.png

檔案空洞部分不佔用磁碟空間、檔案所佔用的磁碟空間仍然是連續的

innodb提供的壓縮方案

頁面壓縮

適用場景:由於資料量太大,磁碟空間不足,負載主要體現在IO上,而伺服器的CPU又有比較多的餘量的場景。

1)COMPRESS頁壓縮

相關文件:https://dev.mysql.com/doc/refman/8.0/en/innodb-compression-tuning.html * 在MySQL5.7版本之前就提供的頁壓縮功能,在建立表時指定 ROW_FORMAT = COMPRESS,並通過 KEY_BLOCK_SIZE 設定壓縮頁的大小 * 存在設計上的缺陷,有可能會導致效能下降明顯,然後其設計初衷是為了提升效能,引入了“日誌即資料”的理念 * 對於壓縮頁的資料修改,並不會直接修改頁本身,而是將修改日誌儲存在這個頁中,這確實對資料的變更比較友好,不用每次修改都進行壓縮/解壓 * image.png * 對於資料的讀取,壓縮的資料是無法直接讀取的,所以這種演算法會在記憶體中保留一個解壓後的16K的頁,以供資料的讀取 * image.png * 這就導致了一個頁在緩衝池中可能會有兩個版本(壓縮版和非壓縮版),引發一個非常嚴重的問題,即緩衝池中能快取的頁的數量大大的減少了,從而可能會導致資料庫的效能極大的下降

2)TPC(透明頁壓縮)

相關文件:https://dev.mysql.com/doc/refman/8.0/en/innodb-page-compression.html * 工作原理:寫入頁面時,使用指定的壓縮演算法對頁面進行壓縮,壓縮後寫入磁碟,其中通過打孔機制從頁面末尾釋放空(需要作業系統支援空洞特性) * ALTER TABLE xxx COMPRESSION = ZLIB 可以啟用TPC頁壓縮功能,但這只是對後續增量資料進行壓縮,如果期望對整個表進行壓縮,則需要執行 OPTIMIZE TABLE xxx * 實現過程:一個壓縮頁在緩衝池中都是一個16K的非壓縮頁,只有在資料刷盤的時候,會進行一次壓縮,壓縮後剩餘的空間會用 0x00 填滿,利用檔案系統的空洞特性(hole punch)對檔案進行裁剪,釋放 0x00 佔用的稀疏空間 * image.png * TPC雖好,但它依賴作業系統的 Hole Punch 特性,且裁剪後的檔案大小需要和檔案系統塊大小對齊(4K)。即假如壓縮後的頁大小是9K,那麼實際佔用的空間是12K

列壓縮

MySQL目前沒有直接針對列壓縮的方案,有一個曲線救國的方法,就是在業務層使用MySQL提供的壓縮和解壓函式來針對列進行壓縮和解壓操作。也就是如果需要對某一列做壓縮,在寫入時呼叫COMPRESS函式對那個列的內容進行壓縮,讀取的時候,使用UNCOMPRESS函式對壓縮過的資料進行解壓。 * 使用場景:針對表中某些列資料長度比較大的情況,一般是 varchar、text、blob、json等資料型別 * 相關函式: * 壓縮函式:COMPRESS() * 解壓縮函式:UNCOMPRESS() * 字串長度函式:LENGTH() * 未解壓字串長度函式:UNCOMPRESSED_LENGTH() * 測試: * 插入資料:insert into xxx (content) values (compress('xxx....')) * 讀取壓縮的資料:select c_id, uncompressed_length(c_content) uncompress_len, length(c_content) compress_len from xxx * image.png

為什麼innodb提供的都是基於頁面的壓縮技術?

  • 記錄壓縮:每次讀寫記錄的時候,都要進行壓縮或解壓,過度依賴CPU的計算能力,效能相對會比較差
  • 表空間壓縮:壓縮效率高,但要求表空間檔案是靜態不增長的,這對於我們大部分的場景都是不適用的
  • 頁面壓縮:既能提升效率,又能在效能中取得一定的平衡

總結

  • 對於一些效能不敏感的業務表,如日誌表、監控表、告警表等,這些表只期望對儲存空間進行優化,對效能的影響不是很關注,可以使用COMPRESS頁壓縮
  • 對於一些比較核心的表,則比較推薦使用TPC壓縮
  • 列壓縮過度依賴CPU,效能方面會稍差,且對業務有一定的改造成本,不夠靈活,需要評估影響範圍,做好切換的方案。好處是可以由業務端決定哪些資料需要壓縮,並控制解壓操作
  • 對頁面進行壓縮,在業務側不用進行什麼改動,對線上完全透明,壓縮方案也非常成熟

為什麼要進行資料壓縮?

  • 由於處理器和快取記憶體儲存器的速度提高超過了磁碟儲存裝置,因此很多時候工作負載都是受限於磁碟I/O。資料壓縮可以使資料佔用更小的空間,可以節省磁碟I/O、減少網路I/O從而提高吞吐量,雖然會犧牲部分CPU資源作為代價
  • 對於OLTP系統,經常進行update、delete、insert等操作,通過壓縮表能夠減少儲存佔用和IO消耗
  • 壓縮其實是一種平衡,並不一定是為了提升資料庫的效能,這種平衡取決於解壓縮帶來的收益和開銷之間的一種權衡,但壓縮對儲存空間來說,收益無疑是很大的

簡單測試

innodb透明頁壓縮(TPC)

參考:https://dev.mysql.com/doc/refman/8.0/en/innodb-page-compression.html

測試資料

1)建立表

  • create table table_origin ( ...... ) comment '測試原表';
  • create table table_compression_zlib ( ...... ) comment '測試壓縮表_zlib' compression = 'zlib';
  • create table table_compression_lz4 ( ...... ) comment '測試壓縮表_lz4' compression = 'lz4';

2)往表中寫入10w行測試資料

壓縮率

sql SELECT NAME, FS_BLOCK_SIZE, FILE_SIZE, ALLOCATED_SIZE FROM information_schema.INNODB_TABLESPACES WHERE NAME like 'test_compress%'; image.png * FS_BLOCK_SIZE:檔案系統塊大小,也就是打孔使用的單位大小 * FILE_SIZE:檔案的表觀大小,表示檔案的最大大小,未壓縮 * ALLOCATED_SIZE:檔案的實際大小,即磁碟上分配的空間量

壓縮率: * zlib:1320636416/3489660928 = 37.8% * lz4:1566949376/3489660928 = 45%

耗時

  • 迴圈插入10w條記錄
    • 原表:918275 ms
    • zlib:878540 ms
    • lz4:875259 ms
  • 迴圈查詢10w條記錄
    • 原表:332519 ms
    • zlib:373387 ms
    • lz4:343501 ms