当前位置: 首页 > 后端技术 > Java

InnoDB学习(一)BufferPool

时间:2023-04-02 09:59:02 Java

我们知道InnoDB数据库的数据是持久化在磁盘上的,磁盘的IO速度很慢。如果每次数据库访问都直接访问磁盘,显然会严重影响数据库的性能。为了提高数据库的访问性能,InnoDB在数据库数据中加入了内存缓存区(BufferPool),避免每次访问数据库时都进行磁盘IO。缓存区BufferPool缓存区并不是Innodb独有的概念。操作系统中也有缓存区的概念。当用户第一次从磁盘读取文件时,它会将文件缓存在内存中。后续对该文件的读操作可以直接从内存中读取,从而减少磁盘IO次数。缓存只是内存中一块连续的空间。InnoDB如何合理利用缓存中的空间呢?本文将从以下几个方面介绍InnoDB的缓存区:Buffer概述:InnoDB缓存结构及状态查询;BufferPoolInstance:缓冲池可以分为多个实例;BufferChunk:实例数据块中的缓存区;控制块和数据页:InnoDB在数据库中以什么形式缓存数据;自由空间管理;缓存中的空闲空间管理逻辑;用户数据管理:管理缓存在缓存中的数据库数据和索引;适配哈希索引:针对热点数据等价查询优化哈希索引;ChangeBuffer介绍:ChangeBuffer提高数据库更新效率;锁信息管理:InnoDB中的行锁信息也存储在缓存区;缓存区概述InnoDB的缓存区叫做innodb_buffer_pool。在读取数据时,它会先检查缓存中是否存在数据页(page)。同理,插入、修改、删除也是先对缓存中的数据进行操作,然后以一定的频率更新到磁盘中。这种刷新机制称为检查点。如下图所示,InnoDB中的数据主要包括数据页、索引页、插入缓存、自适应哈希索引、锁信息和数据字典信息。我们经常听到的RedoLog不在缓存中。MySQL默认的innodb_buffer_pool大小是128M。我们可以通过以下命令查看innodb_buffer_pool的参数。执行结果如下图所示:showvariableslike'innodb_buffer_pool%';在使用MySQL的过程中,我们可能需要查看缓冲区的状态,比如已用空间的状态、脏页大小等,我们可以通过以下命令查看innodb_buffer_pool的状态。执行结果如下图所示。在图中的执行结果中,共有8192页数据。显示全局状态,如“%innodb_buffer_pool%”;缓存区实例缓存区本身就是一块内存空间。在多线程并发访问缓存的情况下,为了保证缓存页数据的正确性,缓存区的单实例锁可能会互斥访问,如果缓存区很大且多线程并发访问很高,单实例缓存区可能会影响请求的处理速度。如下图,数据库缓存大小为3G,并发访问QPS为3000,如果缓存中只有一个实例,这3000个请求可能需要竞争同一个mutex。MySQL5.5引入缓冲区实例作为减少内部锁争用和提高MySQL吞吐量的方法。用户可以通过设置innodb_buffer_pool_instances参数来指定InnoDB缓冲区实例的数量。缓冲池实例的默认数量是1。缓冲实例的大小是`innodb_buffer_pool_size/innodb_buffer_pool_instances。如下图,数据库缓存大小为3G,并发访问QPS为3000。如果缓存中有3个实例,理想情况下最多每1000个请求都会竞争同一个互斥量。如果缓存区的总大小小于1G,innodb_buffer_pool_instances会被重置为1,因为小空间内的多个缓存区实例会影响查询性能。缓冲区实例具有以下特点:缓冲区实例有自己的锁/信号量/物理块/逻辑链表,缓冲区实例之间不存在锁竞争关系;所有buffer实例的空间在数据库启动时分配,数据库关闭后释放;缓存页根据哈希函数随机分布到不同的缓存实例;一个缓存实例的BufferChunk我们知道一个缓存可以包含多个缓存实例,每个缓存实例包含一个连续的内存空间,而InnoDB就是将这个空间划分为多个BufferChunk。BufferChunk是InnoDB中的底层物理块。BufferChunck包含两部分:数据页和控制块。BufferChunk是最底层的物理块,在启动阶段向操作系统请求,直到数据库关闭才释放。几乎所有的数据页都可以通过遍历chunk访问,除了两种状态的数据页:未解压的压缩页(BUF_BLOCK_ZIP_PAGE);已修改的压缩页面和已驱逐的解压缩页面(BUF_BLOCK_ZIP_DIRTY);BufferChunck包含数据页两部分存储的数据如下:控制块:数据块控制信息,如页管理信息/互斥锁/页状态;数据页:数据库数据/锁数据/自适应哈希数据,数据页默认大小为16K;BufferChunck数据块的大小是可配置的。MySQL配置中默认的BufferChunck数据块大小如下。用户可以通过修改配置文件或在启动参数中指定启动MySQL实例块大小目的来自定义BufferChunck数据。$>mysqld--innodb-buffer-pool-chunk-size=134217728[mysqld]innodb_buffer_pool_chunk_size=134217728用户定义的innodb_buffer_pool_chunk_size参数的大小应该小于单个buffer实例的空间大小。如果innodb_buffer_pool_chunk_size乘以innodb_buffer_pool_instances的值大于初始化缓冲池的总大小,则innodb_buffer_pool_chunk_size被截断为innodb_buffer_pool_size/innodb_buffer_pool_instances。控制块和数据页通过上面我们知道InnoDB中底层的物理块是BufferChunk,它包含了控制块和数据页。本节将介绍数据页和控制块中包含哪些数据。控制块InnoDB中的每个数据页都有一个对应的控制块,用于存储数据页的管理信息,但这些信息不需要记录到磁盘,而是根据读取数据的状态动态生成的块在内存中。在查找或修改数据页时,数据块的操作总是通过控制块进行的。控制块主要包含以下数据:页面管理/互斥锁/页面状态等通用信息;freelinkedlist/LRUlinkedlist/FLUlinkedlist等其他链表根据一定的hash函数快速定位到数据页的位置;在数据页InnoDB中,数据管理的最小单位是页,默认为16KB。页面除了可以存储用户数据外,还可以存储控制信息数据。InnoDBIO子系统读写的最小单位也是page。如果表被压缩,则对应的数据页称为压缩页。如果需要从压缩页面中读取数据,则需要先对压缩页面进行解压,形成解压页面。解压后的页面为16KB。压缩页的大小在创建表时指定,目前支持16K、8K、4K、2K、1K。即使压缩页大小设置为16K,blob/varchar/text类型也有一定好处。假设指定的压缩页大小为4K,如果一个数据页不能压缩到4K以下,就需要进行B-tree的分裂操作,这是一个耗时的操作。数据页可以用来存储以下几类数据,下面我们将详细介绍这几类数据结构:用户数据、聚簇索引和非聚簇索引对应的节点数据;行锁信息,InnoDB锁过多异常当我们第一次启动服务器时,我们需要完成初始化过程,即分配内存空间并将其分成一对对的控制块和缓存页。但是此时并没有缓存真正的磁盘页面(因为还没有被使用),而随着程序的运行,磁盘上的页面会继续被缓存,那么问题来了,从磁盘读取哪个缓存页面应该获取页面时放置?或者如何区分哪些缓存页是空闲的,哪些已经被使用了?我们最好记录下某处有哪些可用页面,我们可以将所有空闲页面打包成一个节点,形成一个双向链表,这个链表也可以称为(或空闲链表)。如果InnoDB刚启动,缓存区的所有缓存页都是空闲的,每一个缓存页都会加入到空闲列表中。此时freelist的结构如下(这里省略数据页,freelist的指针指向数据块的控制块)。当需要将缓存页加载到BufferPool中时,如果空闲列表不为空,我们可以从空闲列表中获取一个空闲数据页,并将缓存放入空闲数据页中。以LRU(后面详细介绍)为例。InnoDB启动,LRU加载第一个缓存页后,BufferPool中的数据如下。用户数据管理用户数据管理是BufferPool中最重要的数据,包括表数据和索引数据。用户数据将根据数据的状态进行管理,主要包括以下数据管理。下面将一一介绍这几种链表:RecentlyLeastRecentlyUsed(LRU):InnoDB中最重要的链表,包含所有读入的数据页;FlushLRUList:以LRU方式管理脏页,后台线程定时写入磁盘;UnzipLRUList:管理LRU中解压后的页面数据。解压页面数据是从压缩页面中解压出来的;压缩页面链表(ZipList):顾名思义,它是由压缩页面数据组成的链表;recentlyLeastusedlinkedlistLRU最近最少使用的链表LRU用于缓存表数据和索引数据。由于内存大小通常远小于磁盘大小,无法将所有数据库数据都缓存在内存中,因此缓存通常需要一定的淘汰策略。经常使用的数据页。InnoDB的BufferPool使用了改进版的LRU淘汰策略。如下图所示,LRU链表的结构与自由链表类似。它是一个双向链表。链表中的节点包含指向数据页控制块的指针,通过控制块可以访问数据页中的数据。当需要向缓冲池中添加新的数据页时,可以将最近最少使用的数据页从LRU列表中剔除,将新的数据页添加到LRU列表的中间。该插入点将列LRU链表分为两个子链表:头部的5/8区域,最近访问的热点数据列表;尾部的3/8区域,最近访问较少的冷数据列表;LRU算法将经常使用的数据页保存在热数据链表中,冷数据链表包含不经常访问的数据页。这些数据页是LRU链表满后最先淘汰的数据。默认情况下,算法流程如下:LRU链表的最后3/8区域用于存放冷数据;LRU链表的中点是热数据尾部和冷数据头的交界线;数据链表移动到热点数据链表;热数据链表中的数据如果长时间没有被访问,会逐渐移动到冷数据链表中;如果冷数据长时间未被访问,LRU链表已满,则将末尾的冷数据从LRU链表中剔除;预读数据只会插入到LRU链表,不会移动到热点数据链表;LRU算法还有一个问题。当某条SQL语句需要批量扫描大量数据时,由于会访问到这些页面,可能会导致替换缓冲池中的所有页面,导致大量热数据被换出,性能MySQL急剧下降。这种情况称为缓冲池污染。MySQL缓冲池增加了冷数据驻留时间窗口机制:假设T=冷数据驻留时间窗口;插入冷数据头的数据页即使立即访问也不会立即放入新生代头中;只有满足Onlypages被访问并且在冷数据区停留时间超过T的页面才会被放在新生代的头部;加入冷数据驻留时间窗口策略后,短时间内大量加载的页面不会立即插入到新生代的头部。相反,优先淘汰那些在短时间内只访问过一次的页面。MySQL中LRU链表相关参数:innodb_old_blocks_pct:冷数据占整个LRU链长度的比例,默认为3/8,即整个LRU链中热数据长度与冷数据长度的比值LRU是5:3。innodb_old_blocks_time:冷数据保留时间窗口机制中的冷数据保留时间;当脏数据链表FLU需要更新一个数据页时,如果该数据页在内存中,它会直接更新内存中的数据,但是回写到磁盘的成本比较高,所以InnoDB不会立即将修改后的数据写回磁盘。此时缓存数据页和磁盘数据页中的数据不一致。这种情况下缓存数据页称为脏页,管理所有脏页的链表称为脏数据链表。下面是脏数据链表的示例图:脏数据链表是LRU链表的一个子集,LRU链表包含了所有的脏页数据。脏页中的数据最终会被写回磁盘。将内存数据页刷新到磁盘的操作称为刷新。下面是几种会触发InnoDBflush的情况。InnoDB的RedoLog满了,此时系统会停止。对于所有更新操作,Checkpoint向前推,RedoLog留出空间继续写入;当系统内存不足,需要将脏页从LRU链表中剔除时,必须先将脏页写回磁盘;当MySQL空闲时,一些脏页会自动写回磁盘;当MySQL正常关闭时,所有的脏页都会被写回磁盘;在InnoDB中,可以通过一些参数来设置脏页:innodb_io_capacity:MySQL数据文件所在磁盘的IO容量,innodb_io_capacity参数会影响MySQL刷新脏页的速度。可以通过FIO工具测试磁盘的IOPS。测试命令如下:fio-filename=$filename-direct=1-iodepth1-thread-rw=randrw-ioengine=psync-bs=16k-size=500M-numjobs=10-runtime=10-group_reporting-name=mytestinnodb_io_capacity参数如果不能正确设置,可能会导致数据库性能问题。举个例子:如果MySQL主机磁盘使用的是SSD,但是innodb_io_capacity的值设置的比较低,只有300。此时InnoDB认为系统的IO能力只有300,所以脏页刷新很慢,甚至比脏页的产生还要慢,造成脏页堆积,影响查询和更新性能。innodb_flush_neighbors:当准备刷新一个脏页时,如果数据页旁边的数据页恰好是脏页,则“邻居”将随之刷新;而把“邻居”拖下水的逻辑是可以继续扩散的,即对于每一个相邻的数据页,如果与其相邻的数据页还是脏页,也会一起被冲掉。innodb_flush_neighbors参数用于控制此行为。当值为1时,就会有上述的“坐在一起”机制。当值为0时,表示不找邻居,冲自己的。对于SSD等IOPS比较高的设备,IOPS往往不是瓶颈,innodb_flush_neighbors应该设置为0。在MySQL8.0中,innodb_flush_neighbors参数默认值已经是0。innodb_max_dirty_pages_pct:脏页比例超过innodb_max_dirty_pages_pct后,InnoDB会尽量刷脏页。如果没有超过这个比例,那么刷脏页的速度=max(当前脏页比例/innodb_max_dirty_pages_pct*innodb_io_capacity,RedoLog的缓存大小计算刷脏页的速度);压缩页链表(ZipList)Mysql允许用户压缩表以节省磁盘空间。这些压缩页的数据只有进入内存后解压后才能使用。我们可以通过如下SQL语句创建InnoDB数据表:createtableuser_info(idintprimarykey,ageintnotnull,namevarchar(16),sexbool)engine=InnoDB;对于建立的InnoDB数据表,我们可以通过如下SQL语句对表进行压缩,压缩后表占用的磁盘空间会减少:altertableuser_inforow_format=compressed;InnoDB中的表压缩是针对表数据页的,不仅可以压缩表数据,还可以压缩表索引。压缩页面大小可以是1k/2k/4k/8k。压缩页面链表存储这些压缩页面。压缩后的页面加载到内存后,并不会马上解压,而是需要用到的时候再解压。压缩页大小有1k/2k/4k/8k,InnoDB使用伙伴管理算法来管理压缩页。ZipFree链表有5个,分别管理1k/2k/4k/8k/16K内存碎片。8K链表存储了所有的8K分片。如果读取一个新的8K页面,先从这个链表中查找。如果有则直接返回,如果没有则从16K链表中拆分出两个8K块,一个使用,一个放入8K链表。UnzipLRUList压缩页面链表中的数据是压缩过的,不能直接增删改查。使用前需要解压。将解压后的数据存储在解压后的页面链表中,并将解压后的页面链表中的数据写回磁盘。需要压缩。自适应哈希索引我们知道B+树默认的索引数据结构是B+树,B+树更好地支持范围查询或者LIKE语法。如果数据库中存在大量等价查询,使用哈希索引可以显着提高查询效率。Innodb存储引擎将监视对表的二级索引的查找。如果发现某个二级索引被频繁访问,二级索引成为热点数据,就会为热点数据建立内存哈希索引。该索引称为自适应哈希希腊索引。默认情况下启用自适应哈希索引。您可以通过设置innodb_adaptive_hash_index变量或在启动MySQL时添加--skip-innodb-adaptive-hash-index变量来启用自适应哈希索引。在InnoDB中,可以查看哈希索引的使用情况。命令和输出如下:mysql>showengineinnodbstatus\G...Hashtablesize34673,nodeheaphas0buffer(s)0.00hashsearches/s,0.00non-hashsearches/sChangeBuffer修改数据库数据时,如果对应的数据页刚好在缓存区,可以修改缓存区的数据页,将数据页标记为脏页。如果数据被修改,如果对应的数据页不在缓存中,则需要将数据页从磁盘加载到缓存中,然后进行修改。对于写多读少的场景,会产生大量的磁盘IO,影响数据库的性能。ChangeBuffer可以加速数据更新过程。如果数据页不在内存中,更新操作会缓存在ChangeBuffer中,这样就不需要从磁盘读取数据页,减少IO操作,提高性能。更新操作首先记录在ChangeBuffer中,然后进行merge,真正更新数据。InnoDBChangeBuffer比较复杂,后面我会单独章节介绍。行锁信息管理InnoDB支持行锁,可以锁定数据库中的数据。这些锁信息也存储在BufferPool中。具体的存储格式这里不做详细说明。由于BufferPool中保存了锁信息,所以锁的数量必然受到缓冲区大小的影响。如果InnoDB中锁占用的空间超过BufferPool总大小的70%,添加新锁时会报如下错误:[FATAL]InnoDB:Over95%ofthebufferpoolisoccupiedbylockheapsortheadaptive哈希索引!检查你的事务没有设置太多的行锁。您的缓冲池大小为8MB。也许你应该让缓冲池更大?我们故意生成段错误以在Linux上打印堆栈跟踪!有关详细信息,请参阅位于http://www.mysql.com的帮助和支持中心。我是沉玉虎,欢迎大家关注我的微信公众号:wzm2zsd参考文档Mysql8.0参考手册/InnoDB存储引擎/InnoDBArchitectureChunkChange:InnoDBBufferPoolResizing玩转MySQL十InnoDBBufferPoolInnoDBBufferPoolIntroduction详解对Mysql的Innodb存储引擎缓冲池InnoDB的关键特性的个人理解自适应哈希索引InnoDB页面压缩技术本文首发于微信公众号,版权所有,禁止转载!