深入理解MySQL——InnoDB透明页压缩分析

从MySQL 5.7版本开始,MySQL不仅支持原有的压缩表格式(Table Compression),还支持一种称为透明页压缩的特性(Transparent Page Compression)。通过查阅资料和源码,我对这个特性有了一定的了解。以下我将从它的使用方法、实现原理等方面对它进行简单分析,并同压缩表格式进行一些对比。

1. 开启方法

官方文档对于透明页压缩的特性的说明仅仅一页,主要说明了它的使用方法,我也对这页官方文档进行过翻译,详见: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选项。更详细的使用方法见上边的那篇翻译。

2. 原理简述

2.1. 先说说以前压缩表格式

对于传统的压缩表格式,其在开启时指定了一个压缩后的页的大小。比如上节的例子中指定的8KB。若每次UPDATE或者INSERT操作后都进行压缩,必然太浪费计算时间,所以InnoDB就在每个页中保有一个叫mlog的空闲区域,所有的修改和插入就都被保存这个空闲区域,当mlog快被填满时,页就会被重新压缩,如果8K不再足以存储压缩后的页,那么页就会分裂。

可见传统的压缩表格式的实现,和InnoDB的页面结构有很大的耦合性。此外,innodb buffer pool中可能会同时存在某个页的压缩和未压缩的形式,或者只包含这个页面的压缩形式,或者两者都不包含,其优化细节较为复杂。

2.2. 介绍透明页压缩原理

透明页压缩虽然是最新特性,但是思想却十分简单,我认为其之所以新,也是因为利用了Linux punch hole的新特性[3]。其大概思路就是在压缩时采用了写入文件后进行打洞操作、读入文件后进行解压操作[4]。如下:

# 压缩
+---------------+     +---------------+     +---------------+     +----------------+
|               |     |               |     |               |     |                |
|  InnoDB原始页  +----->    某种变换    +----->    写入磁盘   +------>   文件打洞     |
|               |     |               |     |               |     |                |
+---------------+     +---------------+     +---------------+     +----------------+

# 解压
+----------------+     +----------------+     +---------------+
|                |     |                |     |               |
|  从磁盘读入的页  +----->  对应的逆变换    +----->   原始数据页    |
|                |     |                |     |               |
+----------------+     +----------------+     +---------------+

框图中的变换和逆变换可以对应加密解密、压缩解压等操作,这里肯定是指的压缩和解压操作了。这种思路简介明了,直接将压缩的工作移动到了文件操作这一层,和页的操作解除了耦合。

当然,这也有缺点,因为读入时就全部解压,写入时全部压缩,所以buffer pool中保有的缓存页都是未压缩的,所以相对于buffer pool中多数为压缩页的“压缩表格式“,可能会需要更大的buffer pool(内存)。

3. 源码简析

  • MySQL 版本: 5.7.17

extra/lz4这个文件夹包含了lz4的库函数,而对于InnoDB的透明页压缩的压缩和解压操作,貌似只用到了LZ4_compress_limitedOutput、LZ4_decompress_safe、LZ4_decompress_fast这三个函数。下面列出调用LZ4函数的函数:

3.1. 压缩操作

以LZ4压缩算法为例:

  • 第一步,调用了LZ4_compress_limitedOutput,LZ4_compress_limitedOutput是LZ4库中LZ4_compress_default函数的直接封装。
 //LZ4_compress_default函数原型:
int LZ4_compress_default(const char* source, char* dest, int sourceSize, int maxDestSize);
//参数分别是源数据的指针、分配好空间的压缩后数据的指针、源数据大小和最大的压缩后数据大小
//如果压缩成功,则返回写入dest地址的字节数
//如果压缩后数据大于maxDestSize,则压缩失败,返回0
  • 第二步,将压缩后数据的大小赋值为文件系统块大小的倍数,方便后续的打洞操作。(比如16KB的数据压缩后为11KB,则把压缩后数据的大小赋值为12KB,并把11KB~12KB的空间全部赋值为空字符)。
  • 在MySQL源码storage/innobase/os/os0file.cc中:
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);
}

3.2. 打洞(hole punching)操作

在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)); 
    }

        //....

}

3.3. 解压缩操作

在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);
}

4. 参考

  1. InnoDB Page Compression MySQL文档翻译:InnoDB透明页压缩, blog.jcix.top, 2017
  2. How Compression Works for InnoDB Tables MySQL文档翻译:InnoDB表压缩工作原理, blog.jcix.top, 2017
  3. Punching holes in files, LWN.net, Jonathan Corbet, November 17, 2010
  4. InnoDB Transparent Page Compression, mysql server team, August 18, 2015
  5. how innodb lost its advantage, Domas Mituzas, 2015/04/09
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 确实,MySQL的varchar类型在InnoDB存储引擎中的存储结构比较复杂。在InnoDB中,每个记录都被存储为一个B+树节点,每个节点都有一个固定大小的,通常为16KB。 当一个varchar类型的列被插入到InnoDB表中时,它会被拆分成两个部分:一个是实际的数据,另一个是长度信息。长度信息会被存储在记录头中,而实际的数据会被存储在记录的数据中。 在InnoDB中,如果一个varchar类型的列的长度小于等于768个字节,那么它会被存储在记录的数据中。如果一个varchar类型的列的长度超过了768个字节,那么它会被存储在单独的中,并且在记录中只存储一个指向这个的指针。 此外,由于InnoDB使用了行级锁定,每个记录都需要存储一个事务ID,用于实现MVCC(多版本并发控制)。因此,在InnoDB中,每个记录头还需要存储一个6字节的事务ID和一个2字节的回滚指针。 综上所述,当使用varchar类型时,需要注意其实际数据的长度和存储引擎的存储结构,以便更好地设计表结构和查询语句。 ### 回答2: MySQL的varchar存储结构确实是相当深奥的。在InnoDB存储引擎中,varchar类型的数据存储在表的记录中,其存储结构会影响数据写入、存储空间占用和查询性能。 首先,varchar类型的数据在记录中是以变长字符串的形式进行存储的。这意味着,varchar字段占用的存储空间与其实际存储的数据长度相关,而不是固定的。相比之下,固定长度的数据类型(如char)在存储时会占用固定的存储空间,无论实际数据的长度是多少。 其次,varchar类型的数据在记录中的存储格式是由一个表示长度的字节和真实字符串数据构成的。这个长度字段用于指示存储的实际数据的长度,使得数据库可以根据需要动态地分配存储空间,从而节省了存储空间。 此外,在InnoDB存储引擎中,varchar字段的数据存储在内部的某个位置,而不是直接存储在上。这是由于InnoDB采用了B+树的数据结构来组织数据,为了节省存储空间和提高数据访问效率,varchar字段的数据会被存储在叶子节点中。这样一来,在查询时可以更快地遍历和定位数据,提高查询性能。 综上所述,MySQL的varchar存储结构的深度体现在其变长存储方式、长度字段和数据存储位置等方面。了解和理解这些存储结构对于正确使用varchar类型的字段、优化存储空间和提高查询性能都是非常重要的。 ### 回答3: MySQL的varchar存储结构在InnoDB引擎中确实是一个很深入的话题。InnoDB引擎是MySQL的默认引擎,它采用了B+树索引来存储数据。在InnoDB的记录存储结构中,varchar类型字段经过了一系列处理。 首先,InnoDB将每个记录分为固定长度部分和变长长度部分。varchar字段属于变长长度部分。对于varchar字段,MySQL会额外存储一个指针,指向数据存储区域。 其次,在实际存储varchar字段值时,InnoDB会使用两种方式。对于较短的varchar字段值,会直接将其存储在记录的数据域中。这样做的好处是可以减少额外的存储开销。 而对于较长的varchar字段值,InnoDB会将其存储在一个称为“Overflow Page”的额外存储空间中。Overflow Page的指针存储在记录的数据域中。Overflow Page与主记录有一个单独的物理连接。 另外,需要注意的是,在InnoDB中,varchar字段的长度是可变的,存储的最大长度由定义时的最大长度决定。这与char字段是不同的,char字段的长度是固定的。 总之,MySQL的varchar存储结构在InnoDB引擎中是相对复杂的。它采用了不同的存储方式来处理不同长度的字段值,既保证了数据的存储效率,又满足了灵活性的要求。对于开发人员来说,了解varchar存储结构对于正确使用和优化数据库非常重要。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三月微风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值