Files
rikako-note/mysql/mysql文档/innodb体系结构.md
2024-12-16 12:46:16 +08:00

26 KiB
Raw Blame History

innodb体系结构

innodb体系结构

innodb体系结构由如下部分构成

  • innodb存储引擎内存池
  • 后台线程
  • 磁盘上的文件数据

innodb内存池由多个内存块构成内存快负责如下功能

  • 维护进程、线程访问的内部数据结构
  • 对磁盘上的文件数据进行缓存(加快读取速度),同时缓存对磁盘数据的修改
  • redo log缓冲

在innodb存储引擎中后台线程主要负责刷新内存池中的数据保证内存缓存为最新状态。此时后台线程还负责将内存池中的修改刷新到磁盘中。

mysql存储结构

mysql存储结构如图所示 mysql storage structure

mysql存储结构主要分为两部分

  • 内存结构
  • 磁盘存储结构

内存结构

mysql内存结构主要是为了管理和优化数据存储和数据检索mysql内存结构包含

  • 缓冲区Buffer Pool
  • Change Buffer
  • 自适应哈希索引Adaptive hash index
  • Log Buffer

Buffer Pool

buffer pool主要用于缓存经常访问的数据

change Buffer

change buffer主要用于缓存针对辅助索引页面的修改。当被修改的辅助索引页面不存在于内存中时将修改缓存在change buffer中当后续辅助索引页面被加载到内存中时会将对该页面的修改进行合并。

Adaptive hash index

自适应哈希索引是一个内存结构,用于优化部分读取操作的性能。其提供了一个快速内存检索机制,用于加速对索引页的频繁访问。

log Buffer

log buffer是一个内存区域用于存储将要被写入到transactiion log中的数据。

磁盘存储结构

innodb存储引擎将数据持久化存储到磁盘中。磁盘存储的结构如下所示

  • system tablespace
  • file-per-table tablespaces
  • general tablespaces
  • undo tablespaces
  • temporary tablespaces
  • double write buffer
  • redo log
  • undo log

system tablespace

system tablespace作为change buffer的存储区域。

innodb用一个或多个文件来存储system tablespace默认情况下mysql会创建名为ibdata1的文件。

innodb_data_file_path决定了system tablespace files的大小和数量。

后台线程

innodb采用多线程模型存在多个后台线程每种后台线程负责不同的后台任务。

Master Thread

Master Thread主要负责将内存池中的缓存数据异步刷新到磁盘中包括脏页的刷新、合并插入缓冲insert bufferundo页的回收等。

IO Thread

innodb中使用AIO来处理IO请求IO Thread主要则用来处理异步IO的回调其中innodb_read_io_threadsinnodb_write_io_threads默认均为4个。

show variables like 'innodb_%_io_threads'
Variable_name Value
innodb_read_io_threads 4
innodb_write_io_threads 4

Purge Thread

当事务被提交之后其对应的undo log不再会被需要需要Purge Thread来回收已经使用的undo页。

可以设置多个purge threads默认情况下mysql 8的purge threads为4个

show variables like 'innodb_purge_threads'
Variable_name Value
innodb_purge_threads 4

Page Cleaner Thread

page cleaner thread将脏页刷新的任务放到单线程中来完成从而减轻原Master Thread的工作以及减少对用户查询线程的阻塞。

内存

缓冲池

innodb存储引擎是基于磁盘存储的并将记录基于页的方式进行管理。为了提升数据库系统的读写性能通常采用缓冲池来提升数据库的整体性能。

缓冲池原理

读缓冲

在数据库读取磁盘上的页面时,会将丛磁盘上读取到的页存放到缓冲池中,后续再读取相同的页数据时,先丛缓冲池中查找。如果缓冲池中存在该页,直接从缓冲池中读取。

写缓冲

在数据库针对磁盘上的页数据进行修改时,首先会尝试修改缓冲池中的页数据,并且,缓冲池中的页数据会定期刷新到磁盘中。

缓冲池刷新页到磁盘中的操作由checkpoint机制进行触发并不会在每次更新缓冲池中的页数据后立马触发。

缓冲池参数配置

对于innodb其缓冲池大小通过innodb_buffer_pool_size来配置。默认情况下,innodb_buffer_pool_size大小为128M.

show variables like 'innodb_buffer_pool_size'
Variable_name Value
innodb_buffer_pool_size 134217728

缓冲池中缓存的数据页类型如下:

  • 索引页
  • 数据页
  • 插入缓冲insert buffer
  • 自适应哈希索引
  • innodb存储的锁信息
  • 数据字典信息

在mysql 8中innodb可以支持多个缓冲池实例每个页根据hash值不同被散列到不同缓冲池实例中这样可以提高应用的并发能力。

缓冲池实例数量可以通过innodb_buffer_pool_instances变量来进行设置该变量默认值为1.

innodb_buffer_pool_size

当修改innodb_buffer_pool_size操作将会在chunk上执行。chunk size通过innodb_buffer_pool_chunk_size来配置。

M = innodb_buffer_pool_chunk size * innodb_buffer_pool_instances

innodb_buffer_pool_size必须等于M或是M的整数倍。如果innodb_buffer_pool_size不等于M且不是M的整数倍那么innodb_buffer_pool_size将会被自动调整到等于M或是M的整数倍。

设置innodb_buffer_pool_size示例

innodb_buffer_pool_chunk_size其默认大小为128M如果将innodb_buffer_pool_instances调整为16那么M值为128M * 16 = 2G.

将innodb_buffer_pool_size设置为8G

由于8G = 2G * 4那么8G是2G的整数倍此时该innodb_buffer_pool_size有效

将innodb_buffer_pool_size设置为9G

由于9G不是2G的整数倍那么innodb_buffer_pool_size将会被自动调整到10G10G是2G的整数倍

LRU List, Free List, Flush List

缓冲池是一块由页构成的内存区域。

innodb中缓冲池通过LRU算法来进行管理LRU中最频繁使用的页放在最前端而较少使用的页放在最尾端。当缓冲池中内存已满不能存放新读取到的页时会释放LRU尾端较少使用的页。

innodb中页大小默认为16KB.

midpoint

在LRU中新读取的页并不放在LRU的首部而是放在midpoint的位置该算法被称为midpoint insertion strategy

默认情况下midpoint位于5/8的位置离首部5/8 离尾部3/8。在innodb中首部 -> midpoint部分的页称之为new列表midpoint -> 尾部部分称之为old列表。

midpoint位置可以通过innodb_old_blocks_pct来进行控制,默认情况下该值为37

show variables like 'innodb_old_blocks_pct'

结果为

Variable_name Value
innodb_old_blocks_pct 37

引入midpoint的原因是防止在进行数据扫描等操作时热点数据被淘汰。

此外innodb还引入了innodb_old_blocks_time变量来管理LRU列表代表LRU列表在被读取到midpoint位置后需要经过多久时间才能被加入到LRU的new部分

默认情况下,innodb_old_blocks_time该值为1000单位为ms如果增加该值会使新页面更快的从缓冲区中淘汰。

page made young

在innodb LRU中使用了midpoint insertion的方法来对LRU列表进行管理。当需要将一个新的页添加到缓冲池时最近最少被使用的页将会从缓冲池中淘汰并且新的页将会被插入到midpoint位置。

midpoint将LRU列表分为了两部分

  • young部分该部分为head到midpoint的部分默认占列表长度的5/8用于存放访问频繁的页
  • old部分用于存放访问频率较少的页

默认情况下LRU通过如下算法管理缓冲池

  • 3/8部分属于old sublist部分
  • 当innodb读取页到缓冲池中时新读取的页将会被插入到midpoint位置。页可能因为如下原因被读取
    • 由用户发起的操作例如sql查询
    • 由innodb执行的read ahead操作
  • 当访问old部分的页时made young即将页从old部分移动到young部分的头部。
    • 如果old部分的页是因为用户发起的查询被读取到缓冲池那么该页马上会被访问并且该页会被made young
    • 如果该页是被因为innodb的read ahead被读取到缓冲池中那么该页可能不会马上被访问甚至知道该页被淘汰也不会被访问
  • 随着数据库的运行LRU中的young部分和old部分页面都会向LRU列表尾部移动这被称为老化。当其他页面触发made young操作时:
    • 如果发生made young操作那么young部分和old部分的节点都会向后移动发生老化
    • 如果由新页面被插入到midpoint那么只有old部分的节点会发生老化

table scan导致的问题

默认情况下因用户发起的查询操作而被读取的页面挥别马上移动到LRU young部分。例如mysqldump操作或不带where条件的select都会发起table scan而table scan会读取大量数据到缓冲区中此时会对缓冲区中旧的页进行淘汰。通常table scan读取到缓冲区的新数据后续可能并不会被用到

table scan会快速的将原本处于young部分的页面向后推动到old部分通常这些页都是被频繁使用的。

如上所述table scan会将大量页读取到缓冲池中但是这些页只会在短期内被很快的访问过几次访问会导致该页被made young移动到young部分只会就不会再用到这样会导致大量原本被访问的页被淘汰。

为了解决该问题innodb引入了innodb_old_blocks_time在第一次访问了位于old区域的页后的innodb_old_blocks_time时间范围内再次访问该页并不会导致该页被移动到LRU的young部分。innodb_old_blocks_time的默认值为1000增加该值将会使old部分的页触发made young的条件变得更苛刻old部分老化和淘汰的速度也会更快。

youngs/s & non-youngs/s

  • youngs/s: youngs/s该指标代表每秒平均的因访问old页从而导致made young的访问次数,如果相同的页发生多次访问,那么所有的访问将都会被计入
  • non-youngs/s: 该指标代表访问old页且没有导致made young的访问次数,如果相同页发生多次访问,那么所有的访问都会被计入

如果在没有大量扫描发生的情况下youngs/s的指标值仍然很小那么可以考虑适当降低innodb_old_blocks_time的值让更多的页更快进入young部分。同样可以适当增加old部分的百分比从而可以令old页更慢移动到LRU尾端更有可能被made young。

如果在发生大量扫描的情况下non-youngs/s的指标值仍然不高那么可以考虑增加innodb_old_blocks_time的值延长old页不触发made young的时间窗口

buffer pool hit rate

buffer pool hit rate代表缓冲池中页的命中率如果命中率高则代表该缓冲池运行良好。如果命中率较低则需要考虑是否应增加innodb_old_blocks_time的值避免table scan导致缓冲池被污染。

FreeList

当数据库实例刚启动时LRU里列表中并没有任何页此时页都存放在Free List中。当要从缓冲池中获取页时首先查看Free List中是否有空闲的页如果有则从FreeList中获取并将该页添加到LRU的midpoint位置若Free List中没有空闲的页那么将根据LRU算法淘汰LRU尾部的页将淘汰页的内存空间分配给新的页。

Flush List

LRU中被修改的页称其为脏页在缓冲池中的数据被修改之后并不会马上就刷新到磁盘中而是会通过checkpoint将脏页刷回到磁盘中。

FlushList即是脏页的列表需要注意的是脏页既存在于LRU中又存在于FlushList中LRU和FlushList都管理的是指向该内存页的指针LRU管理缓冲而FlushList管理脏页回刷二者互不影响。

redo log buffer

innodb存储引擎的内存区域中除了有缓冲池之外还存在redo log buffer。innodb首先会将redo log放入到这个缓冲区然后会按照一定频率将其刷新到redo log文件。

redo log缓冲区大小并不需要很大通常每隔1s会将redo log buffer中的内容刷新到文件中。innodb_log_buffer_size负责控制该缓冲区域大小,该参数默认值为8M。(mysql 8中实际测试为16M)

redo log buffer在如下场景下会被刷新到文件中

  • master thread每秒将缓冲刷新到磁盘文件中
  • 每个事务提交时都会将redo log buffer刷新到磁盘文件中
  • 当redo log buffer剩余空间小于一般时redo log buffer刷新到磁盘文件中

checkpoint

在innodb中对数据页的修改都是在缓冲池中完成的在对内存页进行修改后内存页和磁盘上的页内容不一致此时被称为脏页

在存在脏页时如果在脏页被刷新到磁盘时数据库发生宕机那么将会发生数据修改的丢失。为了解决该问题innodb采用了write ahead log策略即当事务提交时先写redo log再修改页。当发生数据丢失时可以通过redo log来完成数据的修复。

checkpoint解决了如下问题

  • 缩短数据库恢复时间
  • 缓冲池不够用时,将脏页刷新到磁盘
  • redo log不可用时刷新脏页

缩短数据库恢复时间

通过checkpoint数据库并不需要在宕机重启之后对所有日志执行redo操作checkpoint之前的页都已经刷新到磁盘。故而数据库只需要对checkpoint之后的数据执行redo操作即可。

缓冲池不够用

当缓冲池不够用时LRU会进行页面淘汰此时被淘汰的页如果是脏页需要强制执行checkpoint将脏页刷新到磁盘中。

redo log不可用

redo log类似循环队列checkpoint之前的位置都已经被刷新到磁盘中可以被覆盖使用。如果当redo log文件中所有的内容都未被刷新到磁盘中那么此时会强制触发checkpoint。

LSN

checkpoint代表的是最后被写入到磁盘文件中的变更通过LSN来进行表示。

LSN为Log Sequence Number该值不断递增代表了和redo log中操作记录相关的时间点。LSN代表的时间点和事务的开始和结束时间并不相关LSN可以处于一个或多个事务的中间

LSN在innodb内部被使用用于crash recovery和管理缓冲池。LSN长度为8字节

位于checkpoint之前的变更都已经被写入到磁盘中

Fuzzy Checkpoint

innodb中实现了fuzzy checkpoint机制会基于小批量small batches来将buffer pool中的页刷新到磁盘中。并不需要在一次batch中将buffer pool中的页都刷新到磁盘中否则checkpoint过程会中断用户的sql语句处理

crash recovery的过程中innodb会查找已经写入到log file中的checkpoint位于checkpoint之前的内容已经被全部写入到数据库的磁盘文件中innodb会扫描checkpoint之后的内容并且将log file中的修改都应用到数据库中。

如果数据库事务发生宕机且缓冲池中存在部分变更尚未被写入到磁盘那么数据库实例重启之后会查看redo log日志中位于checkpoint之后的内容,并且将checkpoint之后的变更写入到磁盘中。

checkpoint种类

innodb内部使用Fuzzy Checkpoint进行页的刷新只会将一部分的脏页刷新到磁盘。

innodb中存在如下集中类型的fuzzy checkpoint

  • Master Thread Checkpoint
  • FLUSH_LRU_LIST_CHECKPOINT
  • Async/Sync Checkpoint
  • Dirty Page Too Much Checkpoint

Master Thread Checkpoint

在Master Thread大约以固定的间隔将一定比例的脏页刷新到磁盘中。Master Thread刷新脏页的操作是异步的用户线程并不会因之阻塞。

FLUSH_LRU_LIST_CHECKPOINT

在innodb中需要保证LRU中有一定数量的空闲页如果空闲页少于该数量那么innodb会将位于LRU尾端的页面淘汰。如果被淘汰页中存在脏页那么对这些脏页需要执行checkpoint操作。

flush_lru_list_checkpoint操作在单独的page cleaner线程中被执行可以通过innodb_lru_scan_depth来控制lru中可用页的数量该值默认为1024

Async/Sync Flush Checkpoint

定义如下变量:

checkpoint_age = redo_lsn - checkpoint_lsn
async_water_mark = 0.75 * total_redo_log_file_size
sync_water_mark = 0.9 * total_redo_log_file_size

假设定义了2个redo log文件并且每个文件大小为1G那么total_redo_log_file_size的大小为2G.

那么,async_water_mark1.5Gsync_water_mark1.8G.

  • checkpoint_age < async_water_mark时,不需要触发任何刷新操作
  • async_water_mark < checkpoint_age < sync_water_mark时,触发async flush从flush列表中刷新足够的脏页回磁盘

    刷新足够脏页回磁盘,是指刷新后满足checkpoint_age < sync_water_mark

  • checkpoint_age > sync_water_mark,会触发sync_water_mark,刷新足够的脏页回磁盘

sync/async flush checkpoint操作同样放入到了page cleaner线程中不会阻塞用户操作

redo异步刷新的水位线为0.75,同步刷新的水位线为0.9

redo_lsn - checkpoint的大小超过异步或同步水位线时,会把足够的脏页刷新到磁盘中,刷新后满足redo_lsn - checkpoint < async_water_mark

Dirty Page too Much

如果buffer中存在的脏页数量过多那么会触发innodb强制进行脏页刷新将脏页刷新到磁盘。

Buffer pool刷新

innodb会在后台将脏页刷新到磁盘中。在innodb中buffer pool刷新由page cleaner thread来执行。

page cleaner threads

page cleaner threads的数量由innodb_page_cleaners变量来控制,该变量存在默认值,默认值为innodb_buffer_pool_instances的值。

innodb_max_dirty_pages_pct_lwm

当buffer pool中的脏页比例达到innodb_max_dirty_pages_pct的百分比时将会触发buffer pool刷新操作。

innodb_max_dirty_pages_pct_lwm的默认值为10%如果将该变量的值设置为0那么将会禁用该刷新行为。

当配置innodb_max_dirty_pages_pct_lwm变量时,应该确保该变量的值小于innodb_max_dirty_pages_pct的值。

innodb_lru_scan_depth

innodb_lru_scan_depth该变量制定了每个缓冲池实例中page cleaner在扫描lru列表时待刷新脏页的深度。该后台操作由page cleaner thread每秒执行一次。

若增加innodb_lru_scan_depth的值在用户线程IO的基础上会额外增加IO的负载。只有在工作负载之外存在空闲IO容量时才考虑增加该变量的值。

如果工作负载已经令IO容量饱和那么可以考虑减少innodb_lru_scan_depth的大小。

该变量默认值大小为1024

自适应刷新

innodb采用了自适应的缓冲区刷新算法根据redo log的生成速率和当前刷新频率动态调整刷新速率。通过该算法可以令刷新活动和当前的工作负载保持平衡。

通过自适应刷新,可以避免因buffer flush产生的IO活动挤占普通读写活动的IO容量造成的吞吐量下降。

自适应刷新根据buffer pool中的脏页数量以及redo log日志的生成速率来帮助避免buffer flush造成的io活动突然增加避免出现工作负载的突然变化。

innodb_adaptive_flushing_lwm

innodb_adaptive_flushing_lwm变量针对redo log容量定义了一个low water mark,当超过该阈值时,自适应花心将会被启用,即使innodb_adaptive_flushing变量被禁用

innodb_adaptive_flushing

通过innodb_adaptive_flushing变量,可以控制自使用刷新是否被启用,默认情况下其被启用。

Log Buffer

log buffer为内存中的区域用于保存将要写入到磁盘log文件中的数据。log buffer的大小通过innodb_log_buffer_size来进行配置,默认其值为64MB

log buffer中的内容将会被定期刷新到磁盘中当log buffer足够大时事务在提交之前无需将redo log内容刷到磁盘中

innodb_flush_log_at_trx_commit

innodb_flush_log_at_trx_commit用于控制如何将log buffer中的内容刷新到磁盘中。

默认情况下,该变量的值为1代表在事务提交时会将log buffer中的redo log内容刷新到磁盘为文件中。

innodb_flush_log_at_timeout

innodb_flush_log_at_timeout控制刷新频率。

Master Thread

master thread中的操作主要分为两部分

  • 每秒钟执行一次的操作
  • 每10s执行一次的操作

每秒钟执行一次的操作

master thread中每秒一次的操作包括如下内容

  • 将log buffer中的内容刷新到磁盘即使对应事务尚未提交总是
  • 合并插入缓冲(可能)

对于redo log即使事务当前尚未提交innodb仍然会每秒将log buffer中的内容刷新到缓冲区中

合并插入缓冲并不是每秒都会发生innodb会根据前1s内发生的IO次数是否小于5来判断是否合并插入缓冲。如果小于5次那么当前IO负载较小可以执行合并缓冲操作。

每10秒执行的操作

  • 合并至多5个插入缓冲总是
  • 将日志缓冲刷新到磁盘(总是)
  • 删除无用的undo log页总是

change buffer

change buffer为一个特殊的数据结构当想要修改的辅助索引page不在缓冲区中时会将针对辅助索引page的修改缓冲到change buffer中。

对于辅助索引页面的修改可能是由insert, update, delete操作造成的当后续待修改的页面被读入到缓冲区中时会对change buffer中缓冲的修改操作进行合并。

聚簇索引和辅助索引插入顺序

和聚簇索引不同,辅助索引通常不是唯一的。并且,当使用auto_increment的聚簇索引时新数据的插入通常都是顺序的插入的多个数据唯一同一page中

但是当插入新数据时新数据对应辅助索引记录的插入则相对是无序的多条辅助索引记录的插入可能都分布在不同的page。同样的针对辅助索引记录的修改和删除其影响的辅助索引记录页可能分布在不同的索引页中。

通过change buffer将针对辅助索引页面的修改缓冲在buffer中并且当需要修改的辅助索引页面读取到缓冲区中时再对change buffer中的修改操作进行合并这样能很大程度上避免多次随机读取辅助索引页到缓冲区中带来的开销。

定期执行的purge操作会将被更新的索引页写入到磁盘中。相比于马上将被修改的索引页写入到磁盘中purge操作更高效。

当存在很多被修改的行数据以及辅助索引数据时change buffer merging操作可能花费数个小时来执行。在change buffer merging期间磁盘IO将会增加可能会导致磁盘相关查询的性能下降。

在内存中change bufer占用的缓冲区的一部分而在磁盘中change buffer是system tablespace的一部分。当数据库实例未启动时针对辅助索引的修改存储在磁盘的change buffer文件中。

当辅助索引中包含descending index column或主键中包含descending index columnchange buffer不适用。

降序索引示例如下:

CREATE TABLE t (
  c1 INT, c2 INT,
  INDEX idx1 (c1, c2 DESC),
  INDEX idx2 (c1 DESC, c2),
  INDEX idx3 (c1 DESC, c2 DESC)
);

change buffer配置

当对于table执行insert、update、delete一些列操作时这些操作集合中辅助索引列的值通常是无序的分散在多个辅助索引页中如果大量更新多个索引页。当待更新的辅助索引页不位于buffer pool中时针对辅助索引页的修改将会被缓存。当待更新的索引页面被加载到buffer pool中时被缓存的修改操作将会被合并被更新的页面后续会刷新到磁盘中

innodb在数据库实例空闲或slow shutdown时会进行change buffer的合并。

优势

change buffer可以带来更少的磁盘读取和磁盘写入故而在工作负载收到io限制时change buffer能发挥重大作用。例如在存在大量插入操作时change buffer能够显著提升性能。

弊端

由于change buffer会占用buffer pool的内存空间故而当change buffer较大时会减少数据缓存的可用空间。但是如果work set的大小和buffer pool几乎相当时或是数据库表中几乎没有辅助索引时此时则可以禁用change buffer

innodb_change_buffering

innodb_change_buffering变量控制chnage buffer的行为通过innodb_change_buffering,可以对如下操作进行启用和禁用:

  • insert operations
  • delete operations:当index record被标记为删除
  • purge operations: 当index被物理删除

update操作是insert操作和delete操作的组合

innodb_change_buffering取值如下:

value numeric value description
none 0 默认,不针对任何操作进行缓存
inserts 1 针对insert操作进行缓存
deletes 2 针对delete marking操作进行缓存
changes 3 针对inserts和delete-marking操作进行缓存
purges 4 针对后台发生的物理删除操作进行缓存
all 5 针对inserts, deletes, purges操作进行缓存

innodb_change_buffer_max_size

innodb_change_buffer_max_size允许按照百分比的方式来配置change buffer占用buffer pool的最大大小默认为25,最大可为50

当mysql server存在大量insert、update、delete操作时change buffer merging操作的速率无法跟上change buffer entries的新增速率此时可以考虑增加innodb_change_buffer_max_size

当change buffer占用了过多buffer pool中内存导致buffer pool中页面老化速度超出预期时此时可以尝试将innodb_change_buffer_max_size适当调小。