从MySQL 5.7版本开始,MySQL不仅支持原有的压缩表格式(Table Compression),还支持一种称为透明页压缩的特性(Transparent Page Compression)。通过查阅资料和源码,我对这个特性有了一定的了解。以下我将从它的使用方法、实现原理等方面对它进行简单分析,并同压缩表格式进行一些对比。
官方文档对于透明页压缩的特性的说明仅仅一页,主要说明了它的使用方法,我也对这页官方文档进行过翻译,详见:InnoDB Page Compression MySQL文档翻译:InnoDB透明页压缩
对于透明页压缩的使用方法,和压缩表格式相同的是,都是通过CREATE TABLE或者ALTER TABLE语法对于一个表使用的。不同点是压缩表格式使用ROW_FORMAT=COMPRESSED这个字段,而透明页压缩使用COMPRESSION=“zlib”、COMPRESSION="lz4"或者COMPRESSION="None"这种字段。分别用两种压缩形式创建一个表的例子:
## 创建一个表,启用压缩表格式,块的大小为8K
CREATE TABLE t1(c1 INT) ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8;
# 创建一个表,启用透明页压缩,压缩算法为LZ4
CREATE TABLE t1(c1 INT) COMPRESSION="zlib”(“lz4”…);
另外要注意:开启透明页压缩需要文件系统和操作系统支持 Sparse File 和 Hole Punching 特性,并且需要开启InnoDB的file-per-table选项。更详细的使用方法见上边的那篇翻译。
对于传统的压缩表格式,其在开启时指定了一个压缩后的页的大小。比如上节的例子中指定的8KB。若每次UPDATE或者INSERT操作后都进行压缩,必然太浪费计算时间,所以InnoDB就在每个页中保有一个叫mlog的空闲区域,所有的修改和插入就都被保存这个空闲区域,当mlog快被填满时,页就会被重新压缩,如果8K不再足以存储压缩后的页,那么页就会分裂。
可见传统的压缩表格式的实现,和InnoDB的页面结构有很大的耦合性。此外,innodb buffer pool中可能会同时存在某个页的压缩和未压缩的形式,或者只包含这个页面的压缩形式,或者两者都不包含,其优化细节较为复杂。
透明页压缩虽然是最新特性,但是思想却十分简单,我认为其之所以新,也是因为利用了Linux punch hole的新特性[3]。其大概思路就是在压缩时采用了写入文件后进行打洞操作、读入文件后进行解压操作[4]。如下:
# 压缩
+---------------+ +---------------+ +---------------+ +----------------+
| | | | | | | |
| InnoDB原始页 +-----> 某种变换 +-----> 写入磁盘 +------> 文件打洞 |
| | | | | | | |
+---------------+ +---------------+ +---------------+ +----------------+
# 解压
+----------------+ +----------------+ +---------------+
| | | | | |
| 从磁盘读入的页 +-----> 对应的逆变换 +-----> 原始数据页 |
| | | | | |
+----------------+ +----------------+ +---------------+
框图中的变换和逆变换可以对应加密解密、压缩解压等操作,这里肯定是指的压缩和解压操作了。这种思路简介明了,直接将压缩的工作移动到了文件操作这一层,和页的操作解除了耦合。
当然,这也有缺点,因为读入时就全部解压,写入时全部压缩,所以buffer pool中保有的缓存页都是未压缩的,所以相对于buffer pool中多数为压缩页的“压缩表格式“,可能会需要更大的buffer pool(内存)。
extra/lz4这个文件夹包含了lz4的库函数,而对于InnoDB的透明页压缩的压缩和解压操作,貌似只用到了LZ4_compress_limitedOutput、LZ4_decompress_safe、LZ4_decompress_fast这三个函数。下面列出调用LZ4函数的函数:
以LZ4压缩算法为例:
//LZ4_compress_default函数原型:
int LZ4_compress_default(const char* source, char* dest, int sourceSize, int maxDestSize);
//参数分别是源数据的指针、分配好空间的压缩后数据的指针、源数据大小和最大的压缩后数据大小
//如果压缩成功,则返回写入dest地址的字节数
//如果压缩后数据大于maxDestSize,则压缩失败,返回0
static
byte*
os_file_compress_page(
Compression compression,
ulint block_size,
byte* src,
ulint src_len,
byte* dst,
ulint* dst_len)
{
//.......
switch (compression.m_type) {
case Compression::NONE:
ut_error;
case Compression::ZLIB: {
//.................
break;
}
case Compression::LZ4:
//这里LZ4_compress_limitedOutput是对LZ4库中LZ4_compress_default函数的直接封装,相当于改了个名字
len = LZ4_compress_limitedOutput(
reinterpret_cast<char*>(src) + FIL_PAGE_DATA,
reinterpret_cast<char*>(dst) + FIL_PAGE_DATA,
static_cast<int>(content_len),
static_cast<int>(out_len));
ut_a(len <= src_len - FIL_PAGE_DATA);
if (len == 0 || len >= out_len) {
*dst_len = src_len;
return(src);
}
break;
default:
*dst_len = src_len;
return(src);
}
//..........
//以下代码将len变量round up(向上取)到block_size(文件系统的block)的倍数
len += FIL_PAGE_DATA;
*dst_len = ut_calc_align(len, block_size); //zjc: dst_len = round up len to multiple of block_size
ut_ad(*dst_len >= len && *dst_len <= out_len + FIL_PAGE_DATA);
/* Clear out the unused portion of the page. */
if (len % block_size) {
memset(dst + len, 0x0, block_size - (len % block_size));
}
return(dst);
}
在MySQL源码storage/innobase/os/os0file.cc中:
static
dberr_t
os_file_io_complete(
const IORequest&type,
os_file_t fh,
byte* buf,
byte* scratch,
ulint src_len,
ulint offset,
ulint len)
{
//....
if (!type.is_compression_enabled()) { //对于没有开启压缩的页,什么也不做直接返回
return(DB_SUCCESS);
} else if (type.is_read()) { //对于读,需要对页面进行解压
//....
//os_file_decompress_page函数会直接调用下面解压小节中所引用的Compression::deserialize函数
return(os_file_decompress_page(
type.is_dblwr_recover(),
buf, scratch, len));
//....
} else if (type.punch_hole()) { //对于写,压缩已经完成,需要在这里进行打洞
//....
//这里检查压缩后的页大小len和文件偏移量offset是否是block_size(文件系统块大小)的整数倍
ut_ad((len % block_size) == 0);
ut_ad((offset % block_size) == 0);
ut_ad(len + block_size <= src_len);
//这里开始进行打洞,比如16K页面压到12K,会被打一个4K的洞
offset += len;
return(os_file_punch_hole(fh, offset, src_len - len));
}
//....
}
在MySQL源码storage/innobase/os/os0file.cc中:
dberr_t
Compression::deserialize(
bool dblwr_recover,
byte* src,
byte* dst,
ulint dst_len)
{
//.......
switch(compression.m_type) {
case Compression::ZLIB: {
//..........
break;
}
case Compression::LZ4:
if (dblwr_recover) {
ret = LZ4_decompress_safe(
reinterpret_cast<char*>(ptr),
reinterpret_cast<char*>(dst),
header.m_compressed_size,
header.m_original_size);
} else {
/* This can potentially read beyond the input
buffer if the data is malformed. According to
the LZ4 documentation it is a little faster
than the above function. When recovering from
the double write buffer we can afford to us the
slower function above. */
ret = LZ4_decompress_fast( //这里进行解压操作
reinterpret_cast<char*>(ptr),
reinterpret_cast<char*>(dst),
header.m_original_size);
}
if (ret < 0) {
if (block != NULL) {
os_free_block(block);
}
return(DB_IO_DECOMPRESS_FAIL);
}
break;
default:
//..........
return(DB_UNSUPPORTED);
}
//............
return(DB_SUCCESS);
}