关于后端:MySQL持久化不为人知的一面⭐️卡顿现象的根源与对策

36次阅读

共计 5769 个字符,预计需要花费 15 分钟才能阅读完成。

MySQL 长久化鲜为人知的一面⭐️卡顿景象的本源与对策

2024 新年新气象,小菜同学又踏上了求职之路,但求职路艰苦,新年第一次面试又被面试官给问住了

面试官:你有没有遇到过因为长久化,把线程的查问、批改申请卡住的状况?

小菜(得意的笑,还想给我挖坑):长久化时写 redo log 的,利用写 redo log 的程序性来晋升性能,防止随机 IO,因而不会卡住其余线程的申请的

面试官:好,那咱们明天的面试就到这里吧

经验本次面试,小菜同学又重新整理缓冲池、长久化相干的知识点终于搞懂卡顿的本源和对策

文章导图如下:

缓冲池

缓冲池的组成

缓冲池是一块内存区域,用于将磁盘中的页加载到内存,放慢访问速度

当拜访数据页时须要先判断页是否在缓冲池中,如果不在则须要从磁盘加载到缓冲池(内存)中

那如何判断某个页是否存在于缓冲池中呢?难度去遍历吗?(遍历是不可能遍历的,工夫复杂度太高)

理论是通过 Key:表空间 + 页号 Value:页 的形式建设散列表,达到 O(1) 的查找速度

数据页被加载到缓冲池后称为缓存页,每个缓存页对应一个管制块,管制块上记录数据页的相干信息

缓存页和对应的管制块组成 chunk,chunk 是申请间断空间的根本单位(当这片空间被缓存页、管制块没占满时还会剩下碎片)

为了防止扩容时从新分配内存,还要将数据从旧的空间迁徙到新的空间,应用 chunk 进行扩容

拜访缓冲池的线程会加锁,如果并发量大且只有一个缓冲池,开销会很大

应用分段锁的思维:将一个缓冲池分为多个实例,每个实例相当于有一把锁(页 hash 到实例),每个实例存在数个 chunk

调整缓冲池参数如下:

  1. 应用 innodb_buffer_pool_instances 调整缓冲池实例的数量
  2. 应用 innodb_buffer_pool_chunk_size 设置每个实例中的 chunk 数量
  3. 应用 innodb_buffer_pool_size 规定缓冲池大小,并且其值必须是 innodb_buffer_pool_instanesinnodb_buffer_pool_chunk_size 的倍数

链表治理

缓存页有三种状态:

  1. 闲暇:当还未从磁盘加载数据页时,缓存页是闲暇的
  2. 已应用页洁净:当从磁盘加载数据页到缓冲池时,对应缓存页被占用,但未在页上进行写操作(页不脏)
  3. 已应用脏页:当有写操作对页中某些记录进行批改时,页并不会立马写回磁盘(这样开销太大),而是通过写 redo log 的模式保障长久化(后文再说),这种被批改但未写回磁盘的页称为脏页

应用不同的链表管理控制块(对应缓存页):

  1. 闲暇链表:治理闲暇缓存页的管制块
  2. 脏页链表:治理脏页缓存页的管制块

留神:链表管理控制块相当于治理对应的缓存页

缓冲池的容量始终是无限的,当缓冲池满时须要将命中不高的页换出,将须要的页换进缓冲池

因而为了晋升缓存命中率,应用 LRU 链表(LRU 算法)治理缓存页

当一个页被查问时,LRU 算法无论页是否存在都会放到链表头,如果链表已满,则将最初一个节点移除

这种场景下,如果进行范畴扫描(页数量多),将会把大量页移除链表,全表扫描场景下状况会更蹩脚,这会大大降低缓存命中率

为了防止以上场景产生,MySQL 对 LRU 算法进行优化:

  1. 将链表分为冷 (old) 热(young)数据区,首次拜访的页只放到 old 区的头部

    应用 innodb_old_blocks_pct 规定 old 区占比 (默认 37%)

  2. 全表扫描可能屡次拜访同一页,所以在规定工夫内屡次拜访某页,不会把它对应管制块放到 young 区头部

    应用 innodb_old_blocks_time 规定该工夫 ms (默认 1000ms)

  3. 如果页对应管制块就在 young 头部左近就不挪动(规定在 1 /4)

留神:LRU 链表中也可能存储脏页

长久化

redo log

在聊脏页刷新前须要先搞懂 innodb 如何长久化

redo log 是 Innodb 存储引擎用于长久化、奔溃复原的重要日志

前文说过,当数据页遇到写操作变成脏页时须要写入磁盘进行长久化

如果对每一条记录都这么做,遇到一个写操作就写入磁盘,而且写回磁盘时,因为页的无序此时会是随机 IO,开销十分大

如果想要存一段时间,等该页的脏记录多了再同时刷盘性价比会高一些,然而如果该期间宕机了,那岂不是会产生数据失落?(因为此时还没刷入磁盘)

为了避免数据失落,在宕机时可能进行数据恢复,应用 redo log 记录页中批改的数据并以程序写入的形式进行 IO(程序 IO)

当脏页被真正刷入磁盘后,对应的 redo log 就没有用了,因而redo log 被设计成环形文件,以笼罩的形式进行追加日志

redo log 通常以 ib\_logfile 0…x 命名(开端为 0 -x)

应用 innodb_log_group_home_dir 查看 redo log 文件所在位置

应用 innodb_log_files_in_group 设置 redo log 文件数,多个文件数串联造成环形文件

应用 innodb_log_file_size 规定每个 redo log 文件大小

通过这两个参数能够设置 redo log 文件的大小

当数据页变成脏页时,会往 redo log buffer(缓冲区)上写 redo log

因为每个事务中存在 SQL,每个 SQL 都可能对应多个 redo log,在往 redo log buffer 写 redo log 时,可能波及到多个事务的 redo log 交替进行写

在进行 redo log 的刷盘时,会先将数据写入 OS 的 page cache(write),而后依据参数配置 innodb_flush_log_at_trx_commit 不同机会刷入磁盘(fsync)

默认下参数为 1,事务提交时会进行 fsync 将 redo log 刷入磁盘

当参数为 0 时,由后盾线程进行 write 再 fsync,吞吐量最高,宕机时会丢数据

当参数为 2 时,事务提交时只进行 write 写到 OS 的 page cache,吞吐量也不错,但 OS 宕机时也会丢数据

bin log

redo log 是 Innodb(物理)奔溃复原的日志,MySQL 还存在逻辑复原的日志 binlog,binlog 还用于主从复制

bin log 的刷盘与 redo log 也是相似的,先进行 write 写到 OS 的 page cache(过程快),再进行 fsync 刷入磁盘(慢)

能够应用 sync_binlog 管制 binlog 刷盘机会,相似 redo log 的 innodb_flush_log_at_trx_commit

默认下参数为 1,事务每次提交后进行 fsync 刷入磁盘

当参数为 0 时,只 write 不 fsync,由 OS 接管刷盘工夫,吞吐量大,可能失落数据

当参数为 X 时,经验 X 事务提交后进行 fsync 刷盘

在刷盘的过程中为了保证数据的一致性,在 redo log 刷盘的同时会对 bin log 一起刷盘

应用 XA 事务的两阶段提交:

  1. redo log prepare(write):redo log 从缓冲区写入 OS page cache
  2. bin log write:bin log 从缓冲区写入 OS page cache
  3. redo log prepare(fsync):redo log 从 page cache 刷入磁盘
  4. bin log fsync:bin log 从 page cache 刷入磁盘
  5. redo log commit:刷盘实现,长久化实现

留神:每个事务的 redo log 是交替写入 buffer 的,每次提交事务时能够把其余事务的 redo log 刷入磁盘(组提交)

解体回复时的判断:

  1. 如果 redo log 是 commit(已实现第五步)那么间接复原数据
  2. 如果 redo log 是 prepare(未实现第五步),查看 binlog 是否残缺;如果 binlog 残缺(已实现第四步:bin log fsync)阐明 redo log、bin log 都实现刷盘能够复原数据,否则不复原

为啥要设计成这样呢?

如果先写完 redo log 宕机没写 bin log,那么主机会通过 redo log 复原数据,而从机须要通过 binlog 复原数据,此时 binlog 不存在就会导致数据不统一

如果先写完 bin log 宕机没写 redo log,那么主机就无奈通过 redo log 复原数据,从而导致数据不统一

double write

在长久化的过程中还存在 double write 两次写

如果你了解 redo log 长久化的过程,是不是想说:两次写就是先写 redo log 再写数据页,分两次刷入磁盘

其实不是的,这里的两次写代表着数据页会分为两次写入磁盘,应用 redo log 复原数据须要基于页的完整性,那在页还未刷入磁盘时如何保障页的完整性呢?

思维与 redo log 相似,通过先程序写数据页的形式保障 \~(程序 IO 代替随机 IO)

checkpoint

将 redo log 刷入磁盘后,期待后续线程将对应的脏页刷入磁盘后,该 redo log 就能够被笼罩了

然而如何判断环形 redo log 可被笼罩呢?

在 redo log 上记录一些 lsn(Log Sequeue Number),lsn 是自增的(文件环形达到最大后又从终点开始)

lsn:标识写 redo log 序列号地位

flushed\_to\_disk\_lsn:标识 redo log 刷入磁盘序列号地位

checkpoint\_lsn:标识 checkpoint 推动到序列号的地位(可笼罩的地位)

后盾线程会定期 checkpoint 推动可笼罩 redo log 的标记,每次进行 checkpoint 更新 checkpoint\_lsn 的地位(更新可笼罩的 redo log)

lsn 与 flushed\_to\_disk\_lsn 之间的 redo log 是没有刷入磁盘的

flushed\_to\_disk\_lsn 与 checkpoint\_lsn 之间的 redo log 是刷入磁盘的(然而它们对应的数据页可能有的被刷盘,有的没刷盘)

checkpoint\_lsn 前的 redo log 示意可笼罩的(对应数据页曾经刷盘)

脏页刷新

晓得 MySQL 的长久化机制后,再来看长久化时为啥会卡顿?

写操作太多,很多页没有刷盘,导致 redo log 占满,此时触发 checkpoint 将脏页刷入磁盘,空出可笼罩的 redo log

又或者是 缓冲池已满,要换进新的页时,会将 old 区开端的页换出,如果该页是脏页,则又要进行刷盘

除了这种场景外还会有线程定时刷新、敞开前把脏页刷入磁盘等

当产生这种场景时,会暂停用户线程去进行刷盘操作从而造成阻塞(相似于 JVM 中的 GC)

因而咱们应该减低这种场景的产生,能够通过调整参数或降级磁盘等多方面实现

以后参数最好通过测试让 DBA 去调整,总结一下对应的参数

  • 缓冲池

    • 应用 innodb_buffer_pool_instances 调整缓冲池实例的数量
    • 应用 innodb_buffer_pool_chunk_size 设置每个实例中的 chunk 数量
    • 应用 innodb_buffer_pool_size 规定缓冲池大小,并且其值必须是 innodb_buffer_pool_instanesinnodb_buffer_pool_chunk_size 的倍数
  • LRU 算法

    • 应用 innodb_old_blocks_pct 规定 old 区占比 (默认 37%)
    • 应用 innodb_old_blocks_time 规定该工夫 ms (默认 1000ms)
  • redo log

    • 应用 innodb_log_group_home_dir 查看 redo log 文件所在位置
    • 应用 innodb_log_files_in_group 设置 redo log 文件数,多个文件数串联造成环形文件
    • 应用 innodb_log_file_size 规定每个 redo log 文件大小
  • bin log、redo log 刷盘策略

    • sync_binlog
    • innodb_flush_log_at_trx_commit
  • 调整 io

    • 应用 innodb_io_capacity 调整 IO 能力(应用磁盘 IOPS)
    • 应用 innodb_flush_neighbore(是否刷脏页的街坊页到磁盘,默认是,应用 SSD 能够敞开)

总结

本篇文章从 MySQL 的缓冲池开始,总结 Innodb 中进行长久化的实现原理

缓冲池由数个实例组成,实例由数个 chunk 组成,chunk 由管制块、缓存页组成,每一个缓存页都有一个对应的管制块(缓冲池 -> 实例 -> chunk -> 管制块、缓存页)

缓存页分为闲暇、已应用页洁净、已应用脏页三种状态,应用闲暇链表、脏页链表、LRU 链表对缓存页的管制块进行治理

将 LRU 链表分为冷热数据区,从磁盘加载的页先放到冷数据区,通过一段时间屡次读取后再放入热数据区头部,如果在短时间内屡次拜访一页则不会放入热数据区(避免范畴、全表扫描导致缓存命中率升高),如果页就在热数据区头部左近则不会挪动到头部(1/4)

应用先写 redo log 再将脏页刷盘的形式,用程序 IO 代替随机 IO

redo log 记录数据页批改的数据,用于实现物理上的数据恢复,因为 redo log 对应的页刷盘后,该 redo log 相当于有效,因而被设计成环形文件(可笼罩)

在生成 redo log 时,会将 redo log 写在 redo log buffer 缓冲池,因为每个事务可能对应多条 redo log,redo log 在缓冲池中是被交替写入的

redo log 在进行刷盘时,会先从缓冲池写入操作系统的文件缓存 page cache(write 快),再刷入磁盘(fsync 慢)

binlog 是 MySQL 逻辑上的数据恢复日志,在 redo log 进行刷盘时,为了保证数据一致性,bin log 与 redo log 基于 XA 协定应用两阶段提交

redo log 复原数据基于页的完整性,double write 先让页程序写到磁盘(保障页的可用),后续脏页再刷入磁盘

checkpoint 将脏页刷入磁盘,更新 redo log 上的 checkpoint lsn(更新 redo log 可覆盖范围)

当 redo log 被写满或缓冲池已满冷数据区开端是脏页的场景,都会去让脏页刷新,导致用户线程阻塞,对于这种场景应该让 DBA 调整参数,降级 IO 能力解决

最初(不要白嫖,一键三连求求拉~)

本篇文章被支出专栏 MySQL 进阶之路,感兴趣的同学能够继续关注喔

本篇文章笔记以及案例被支出 gitee-StudyJava、github-StudyJava 感兴趣的同学能够 stat 下继续关注喔~

有什么问题能够在评论区交换,如果感觉菜菜写的不错,能够点赞、关注、珍藏反对一下~

关注菜菜,分享更多干货,公众号:菜菜的后端私房菜

本文由博客一文多发平台 OpenWrite 公布!

正文完
 0