# innodb体系结构 ## innodb体系结构 innodb体系结构由如下部分构成 - innodb存储引擎内存池 - 后台线程 - 磁盘上的文件数据 innodb内存池由多个内存块构成,内存快负责如下功能: - 维护进程、线程访问的内部数据结构 - 对磁盘上的文件数据进行缓存(加快读取速度),同时缓存对磁盘数据的修改 - redo log缓冲 在innodb存储引擎中,后台线程主要负责刷新内存池中的数据,保证内存缓存为最新状态。此时后台线程还负责将内存池中的修改刷新到磁盘中。 ## 后台线程 innodb采用多线程模型,存在多个后台线程,每种后台线程负责不同的后台任务。 ### Master Thread Master Thread主要负责将内存池中的缓存数据异步刷新到磁盘中,包括脏页的刷新、合并`插入缓冲`(insert buffer),undo页的回收等。 ### IO Thread innodb中使用AIO来处理IO请求,IO Thread主要则用来处理异步IO的回调,其中`innodb_read_io_threads`和`innodb_write_io_threads`默认均为4个。 ```sql 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个: ```sql 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. ```sql 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将会被自动调整到10G,10G是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`。 ```sql 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_mark`为`1.5G`,`sync_water_mark`为`1.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`。