MYSQL体系结构
MYSQL由一下几部分组成:
- 连接池组件
- 管理服务和工具组件
- SQL接口组件
- 查询分析器组件
- 优化器组件
- 缓冲组件
- 插件式储存引擎
- 物理文件
需要特别注意的是:存储引擎是基于表的,而不是数据库。
MYSQL存储引擎
InnoDB存储引擎
- 支持事务:主要面向在线事务处理(OLTP)应用。特点是
- 行锁设计
- 支持外键
- 支持类似于Oracle的非锁定读,即默认读取操作不会产生锁。
- 数据放在一个逻辑的表空间,这个表空间像黑盒一样由INNODB存储引擎自身进行管理。它可以将每个INNODB存储引擎的表单独存放到一个独立的ibd文件中。
- 通过使用多版本并发控制(MVCC)来获得高并发性
- 实现了SQL标准的四种隔离级别。默认为REPEATABLE级别。使用一种被称为next-keylocking的策略来避免幻读现象的产生。
- 提供插入缓冲、二次写、自适应哈希索引、预读等高性能和高可用功能。
- 表中数据采用聚集的方式存放,因此每场表的储存都是按主键的顺序进行存放,如果没有显式指定主键,则会为每一行生成一个6字节的ROWID,并作为主键。
MyISAM存储引擎
- 不支持事务,支持全文索引,主要面向一些OLAP数据库应用。
- 缓冲池只缓冲索引文件,不缓冲数据文件。
- MYD用来存放数据文件,MYI用来存放索引文件。
InnoDB 存储引擎
InnoDB 存储架构
InnoDB 存储引擎有多个内存块,可以认为这些内存块组成了一个大的内存池,负责如下工作:
- 维护所有进程 / 线程需要访问的多个内部数据结构
- 缓存磁盘上的数据,方便快速的读取,同时在对磁盘文件的数据修改之前在这里缓存。
- 重做日志 (redo log) 缓冲
后台线程的主要作用
- 刷新内存池中的数据,保证缓冲池中的内存缓存是最新的数据
- 将已经修改的数据文件刷新到磁盘文件,同时保障在数据库发生异常的情况下 InnoDB 能恢复到正常运行状态。
后台线程
-
Master Thread
核心线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新,合并插入缓冲,UNDO 页的回收等。
-
IO Thread
- 在 INNODB 存储引擎中大量使用了 AIO(Async IO)来处理写 IO 请求,这样可以极大提高数据库的性能。
- Sync IO,即每进行一次IO操作,需要等待此操作结束才能继续接下来的操作
- 异步IO,用户可以在发出一个IO请求后立即再发出另一个IO请求,当全部IO请求发送完毕后,等待所有IO操作的完成
- 有四种 IO Thread 分别是(1.0.x 开始):
- wrtie thread:4 个
- read thread:4 个
- insert buffer thread:1 个
- buffer log IO:1 个
- 可以使用 innodb_read_io_threads 和 innodb_write_io_threads 参数进行设置。
- 读线程一定小于写线程
- 在 INNODB 存储引擎中大量使用了 AIO(Async IO)来处理写 IO 请求,这样可以极大提高数据库的性能。
# INNODB版本查询
SHOW VARIABLES LIKE 'INNODB_VERSION'\G;
# INNODB THREAD 数量查询
SHOW VARIABLES LIKE 'innodb_%io_threads'\G;
# 查询IO THREAD
SHOW ENGINE INNODB STATUS\G;
-
Purge Thread
事务被提交后,其所使用的 undolog 可能不再需要,因此需要 PurgeThread 来回收已经使用并分配的 undo 页。从 INNODB 1.1 版本开始,可以将 purge 操作从 MASTER THREAD 中抽离出来,来减轻 MASTER THEAD 的工作。使用配置开始
[mysqld]
innodb_purge_threads=1
从 INNODB 1.2 版本开始,可以支持多个 purge Thread,这样做的目的是为了进行加快 undo 页的回收。例如可以设置 4 个
SELECT VERSION() \G;
SHOW VARIABLES LIKE 'innodb_purge_threads'\G;
-
Page Cleaner Thread
是在 INNODB 1.2.x 版本中引入的。起作用是将之前版本中脏页的刷新操作都放入到单独的线程中来完成。其目的是为简称 MASTER THREAD 的工作以及对用于查询线程的堵塞,进一步提高性能。
内存
-
缓冲池
InnoDB 存储引擎是在磁盘按照页的方式进行管理,是基于磁盘的数据库系统。需要使用缓冲池技术提高数据库的整体性能。
缓冲池就是一块内存区域,读取先再缓存找,找不到去磁盘加载。写入的是后续通过 Checkpoint 的机制刷新回磁盘。
SHOW VARIABLES LIKE 'innodb_buffer_pool_size'\G;
从 1.0.X 版本开始,允许有多个缓冲池实例。每个页根据哈希值平均分配到不同缓冲池实例中。好处是减少数据库内部的资源竞争,增加数据库的并发处理能力。
## 查询缓冲池信息、状态
SHOW VARIABLES LIKE 'innodb_buffer_pool_instances'\G;
SHOW ENGINE INNODB STATUS\G;
SELECT POOL_ID, POOL_SIZE, FREE_BUFFERS, DATABASES_PAGES
FROM INNODB_BUFFER_POOL_STATUS\G;
-
INNODB 内存管理模式——LRU List、Free List(空闲页列表) 和 Flush List(脏页列表)
InnoDB 存储引擎使用 LRU 算法进行内存管理,不同的是,他最新读取的页不是放在列表的首部,而是放在 midpoint 的位置。midpoint 位置可由参数
innodb_old_blocks_pct
控制,例如
SHOW VARIABLES LIKE 'innodb_old_blocks_pct'\G;
为什么不是用最基础的 LRU 算法呢?
这是因为某些 SQL(例如索引或者数据的扫描操作)可能会将缓冲池中的页被刷新出,影响缓冲池的效率。但是这类 SQL 的数据使用的页并不是活跃数据。
为了更进一步优化这个问题,引入一个新的参数innodb_old_blocks_time
,表示页读取到 mid 位置后需要等待多久才会被加入到 LRU 列表的热端。所以在执行上述类型的 SQL 时候,可以先设置这个参数保证原来的 LRU 列表热点数据不被刷出。
SET GLOBAL innodb_old_blocks_time = 1000;
# 一些操作
.....
SET GLOBAL innodb_old_blocks_time = 0;
如果用户预估自己热点数据不止 63%,可以在执行 SQL 前改变innodb_old_blocks_pct
参数
SET GLOBAL innodb_old_blocks_pct=20;
InnoDB 开始启动时,LRU 加载过程:
- 数据库刚启动时,LRU 列表是空的,即没有任何的页。所有页都放在 Free 列表中。
- 当需要从缓冲池分页时,首先从 Free 列表中查找是否有可用的空闲页,若有则将该页从 Free 列表中删除,放入到 LRU 列表中。
- 页从 LRU 列表的 old 部分加入到 new 部分时,称此时发生的操作为 page made young
- 因为
innodb_old_blocks_time
的设置而导致页没有从 old 部分移动到 new 部分的操作称为 page not made young。 - 通过命令 SHOW ENGINE INNODB STATUS 观察 LRU 列表以及 FREE 列表的使用情况和运行状态。
SHOW ENGINE INNODB STATUS\G
# INNODB1.2版本开始,还可以通过表INNODB_BUFFER_POOL_STATUS来观察缓冲池的运行状态
SELECT POOL_ID, HIT_RATE, PAGES_MADE_YOUNG, PAGES_NOT_MADE_YOUNG
FROM information_schema.INNODB_BUFFER_POOL_STATUS\G;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BmlWc47Y-1586955325442)(https://images2018.cnblogs.com/blog/657755/201807/657755-20180710151616439-1442496539.png)]
# 通过表INNODB_BUFFER_PAGE_LRU来观察每个LRU列表中每个页的具体信息
SELECT TABLE_NAME, SPACE, PAGE_NUMBER, PAGE_TYPE
FROM INNODB_BUFFER_PAGE_LRU WHERE SPACE = 1;
INNODB 存储引擎从 1.0.X 版本开始支持压缩页的功能,即将原来 16KB 的页压缩为 1KB、2KB、4KB 和 8KB。由于页的大小发生了变化,所以 LRU 列表也有了些许的变化,对于非 16KB 的页,是通过 unzip_LRU 列表进行管理的。通过命令观察得到:
SHOW ENGINE INNODB STATUS\G;
这里需要注意的是 LRU 列表长度包括 unzip_LRU 列表长度。
unzip_LRU 是怎样从缓冲池中分配内存的呢?
首先,在 unzip_LRU 列表中对不同压缩页大小的页进行分别管理。其次通过伙伴算法进行内存的分配。来如对需要从缓冲池中申请页为 4KB 的大小的过程如下:
- 检查 4KB 的 unzip_LRU 的列表,检查是否有可用的空闲页;
- 若有,则直接使用
- 否则,检查 8KB 的 unzip_LRU 列表
- 若能够得到空闲页,将页分成 2 个 4KB 的页,存放到 4KB 的 unzip_LRU 列表汇总;
- 若不能得到空闲页,从 LRU 列表中申请一个 16KB 的页,分为 1 个 8K 的页还有 2 个 4KB 的页,分别存放到对应的 unzip_LRU 列表中。
# 观察unzip_LRU列表中的页
SELECT
TABLE_NAME, SPACE, PAGE_NUMBER, COMPERSSID_SIZE
FROM INNODB_BUFFER_PAGE_LRU
WHERE COMPRESSED_SIZE <> 0;
在 LRU 列表中的页被修改后,该页成为脏页,即缓冲池中的页和磁盘上的页的数据产生了不一致。这时数据库会通过 CHECKPOINT 机制将脏页刷新会磁盘。
Flush 列表中的页即为脏页列表。
脏页既存在于 LRU 列表中,也存在与 Flush 列表中。LRU 列表用来管理缓冲池中的页的可用性,Flush 列表用来管理将页刷新回磁盘,二者互不影响。
# modified db pages显示脏页的数量
SHOW ENGINE INNODB STATUS
# 可以通过源数据库表INNODB_BUFFER_PAGE_LRU来查看
SELECT TABLE_NAME, SPACE, PAGE_NUMBER, PAGE_TYPE
FROM INNODB_BUFFER_PAGE_LRU
WHERE OLDEST_MODIFICATION > 0;
-
重做日志缓冲
INNODB 存储引擎首先将重做日志信息放在这个缓冲区中,然后按照一定的频率刷新到重做日志文件。一般不用设置的很大。因为刷新速度很快。可通过
innodb_log_buffer_size
控制,默认 8MB
SHOW VARIABLES LIKE 'innodb_log_buffer_size'\G;
刷新到磁盘的策略
- MASTER THREAD 每一秒将重做日志缓冲刷新到重做日志文件中
- 每个事务提交时会将重做日志缓冲刷新到重做日志文件中
- 当重做日志缓冲池剩余空间小于一半时,重做日志缓冲刷新到重做日志文件中。
-
额外的内存池
在 INNODB 存储引擎中,对内存的管理是通过一种称为内存堆 (heap) 的方式进行的。在对一些数据结构本身的内存进行分配时,需要从额外的内存池中进行申请,当该区域的内存不够时,也会从缓冲池中进行申请。所以在申请了很大的 INNODB 缓冲池时,也要相应增加这个数值。
Checkpoint 技术
一条 DML 语句会使得产生脏页(内存的数据比磁盘新),那就需要刷新到磁盘中。基本上是采用Write Ahead Log
策略。即当事务提交时,先写重做日志,再修改页。当由于发生宕机而导致数据丢失时,通过重做日志来完成数据的恢复。
而 Checkpoint 技术是为了解决这个过程的痛点:
-
缩短数据库恢复的时间
由于 checkpoint 之前的数据都刷回去磁盘了,所以只需要对 checkpoint 后的重做日志进行恢复。大大缩短了恢复时间。
-
缓冲池不够用时,将脏页刷新到磁盘
LRU 算法会溢出最近最少使用的页,如果是脏页,强制 Checkpoint。
-
重做日志不可用时,刷新脏页。
重做日志的空间是循环使用的,当要被重用的时候,被重用的部分必须进行 checkpoint。
在 InnoDB 存储引擎中,通过 LSN(Log Sequence Number) 来标记版本的。LSN 是 8 字节的数字。每个页、重做日志、Checkpoint 都有 LSN。可以通过命令来查看
SHOW ENGINE INNODB STATUS\G;
在 InnoDB 存储引擎内部,有两种 Checkpoint,分别为:
-
Sharp Checkpoint
是在数据库关闭时刷新全部脏页到磁盘。参数是
innodb_fast_shutdown=1
-
Fuzzy Checkpoint
运行时使用,指刷新一部分脏页。
-
Master Thread Checkpoint
每秒或者每十秒刷新从缓冲池的脏页列表中刷新一定比例的页回磁盘。
-
FLUSH_LRU_LIST Checkpoint
是因为 InnoDB 存储引擎需要保障 LRU 列表中需要有足够多的空闲页可使用。
在 InnoDB 1.1.x 版本之前,检查 LRU 列表空间是否足够是在用户查询线程中,会堵塞用户的查询操作。而且如果查询空间不足,会将尾端的页移除,如果有脏页就进行 Checkpoint。
在 InnoDB1.2.x 版本开始,这个操作会放在 Page Cleaner 线程中进行。
可以通过参数进行设置预留的空间大小,设置 LRU 列表需要保留多少个空闲页的空间
-
SHOW VARIABLES LIKE 'innodb_lru_sacn_depth'\G;
-
Async/Sync Flush Checkpoint
是指重做日志文件不可用(空间快用完了)的情况,这时需要强制将一些页刷新会磁盘,而此时脏页是从脏页列表中选取的。
在 INNODB1.2.x 版本后,放入到了单独的 page Cleaner Thread 中。可以通过命令来观察状态
SHOW ENGINE INNODB STATUS\G;
- Dirty Page too much Checkpoint
脏页的数量太多,导致 InnoDB 强制进行 CheckPoint。可以由参数来配置,表示缓冲中脏页的数量占据百分比为多少后,进行脏页的刷新。
```
SHOW VARIABLES LIKE 'innodb_max_dirty_pages_pct'\G;
```
Master Thread 工作方式
InnoDB 1.0.x
版本之前的 Master Thread
Master Thread 具有最高的线程优先级别,内部由多个循环 (loop) 组成,会根据运行状态在不同的循环中切换。
- 主循环
loop
- 后台循环
backgroup loop
- 刷新循环
flush loop
- 暂停循环
suspend loop
主循环
包括每秒操作和每十秒操作。
每秒操作包括:
-
日志缓冲刷新到磁盘,即使这个事务还没提交(总是)
-
合并插入缓冲(可能)
判断前一秒发生 IO 次数是否小于 5 次,才会执行
-
至多刷新 100 个 InnoDB 的缓冲池中的脏页到磁盘(可能)
// 当前脏页比例
if buf_get_modified_ratio_pct > innodb_max_drity_pages_pct
then
刷新100个脏页到磁盘
- 如果当前没有用户活动,则切换到 background loop(可能)
每十秒操作包括:
- 刷新 100 个脏页到磁盘(可能的情况下)
- 合并至多 5 个插入缓冲(总是)
- 将日志缓冲刷新到磁盘(总是)
- 删除无用的 Undo 页(总是)
- 刷新 100 个或者 10 个脏页到磁盘(总是)
background loop
以上的操作,都是基于过去 10 秒内 IO 次数小于 200 才会进行。
当前没有用户活动或者数据库关闭,就会切换到background loop
循环
- 删除无用的 Undo 页(总是)
- 合并 20 个插入缓冲(总是)
- 跳回到主循环(总是)
- 不断刷新 100 个页知道符合条件(可能,跳转到 flush loop 中完成)
suspend_loop
如果 flush loop 事情完成了,就会切到 suspend_loop,将 master_thread 挂起,等待事件发生
InnoDB 1.2.x 版本之前的 Master Thread
可以看出之前版本的代码,做了很多的硬编码,很大程度上限制了 InnoDB 存储引擎对 IO 的性能(SSD 盘使用后)。所以有时候数量上去后,其实是代码中未充分使用资源,导致性能瓶颈。所以抽出参数供用户来设置调节。
- 参数:innodb_io_capacity,用来表示磁盘 IO 的吞吐量,默认值为 200.
- 在合并插入缓冲时,合并插入缓冲的数量为 Innodb_io_capacity 值的 5%;
- 在从缓冲区刷新脏页时,刷新脏页的数量 innodb_ip_capacity。
- 参数:innodb_adaptive_flushing ,自适应刷新,影响每秒刷新脏页的数量
原来的规则是:脏页在缓冲池所占的比例小于innodb_max_dirty_pages_pct
时,不刷新脏页,大于时,刷新 100 个脏页。
现在的规则是:引擎会通过一个名为buf_flush_get_desired_flush_rate
的函数来获取刷新脏页合适的数量。
粗略翻阅源代码后发现buf_flush_get_desired_flush_rate
通过重做日志的产生速度来决定最合适的刷新脏页的数量。
- 参数:innodb_purge_batch_size,该参数控制每次 full purge 回收的 undo 页的数量。
SHOW VARIABLES LIKE 'innodb_purge_batch_size'\G;
通过命令可以查看当前 master thread 的状态信息
SHOW ENGINE INNODB STATUS\G;
InnoDB1.2.x 版本的 Master Thread
对于脏页的刷新操作,分离到一个单独的 Page Cleaner Thread,从而减轻了 Master Thread 的工作。进一步提升了系统的并发性。
InnoDB 关键特性
插入缓冲 (Insert buffer)
-
Insert Buffer
- 插入自增主键时,只需要顺序读取,不需要随机访问。并非所有主键插入都是顺序的:
主键是 UUID 时,是随机。
主键是自增类型,但插入时指定了值,而非 NULL,则依旧是随机的。
如果表上有非聚集的辅助索引( secondary index),则由于不是聚集的,依旧需要随机访问索引页
随机读取导致插入操作性能下降,因此使用插入缓冲。
-
插入时,如果索引页在缓冲池中,则直接插入
-
如果不在,则先放到 Insert Buffer 中,假装插入成功,然后定期将 Insert Buffer 和索引页进行合并操作
-
使用时需要满足条件
- 索引是辅助索引 (secondary index)
- 索引不唯一
-
存在问题
- 宕机恢复时间长
- 写密集时占用过多缓冲区
- 插入自增主键时,只需要顺序读取,不需要随机访问。并非所有主键插入都是顺序的:
Change Buffer:Insert Buffer 的升级,可缓冲 DML 操作
Insert Buffer 内部实现
-
全局的一棵 B+ 树
-
非叶节点存放 search key (space, marker, offset)
search key 共占用9个字节,其中space表示待插入记录所在表的表空间id,在 InnoDB存储引擎中,每个表有一个唯一的space id,可以通过space id查询得知是哪张表。
space占用4字节。marker 占用1字节,它是用来兼容老版本的 Insert Buffer。offset 表示页所在的偏移量,占用4字节。
-
叶子节点:从第四列(metadata) 开始, 第五列之后存放字段
Metadata:记录顺序类型等
两次写 (Double Write)
数据只有写到磁盘才安全,如果脏页未刷回,如何保证 crash safe?使用了 WAL,先写入 log 再刷回磁盘。由于 log 是顺序写入,性能过关,写入页的途中可能 crash,此时使用 double write 恢复,相当于存档
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-noqN2s5A-1586955325455)(https://s1.ax1x.com/2020/04/15/JPvWSf.png)]
- 作用:带来数据页的可靠性,解决在将页写入磁盘时发生宕机
- 由两部分缓存:内在中的 buffer,和磁盘上共享表空间的连续 128 个页,大小都是 2MB
- 先将页复制到 double write buffer,再分两次,每次 1MB 写入磁盘,后马上调用 fsync 同步磁盘
- double write 结束后再写入表空间的文件中
自适应哈希索引 (Adaptive Hash Index)
- 会自动根据对索引页的访问频率和模式创建哈希索引
- 要求
- 要求对页的连续访问模式必须一样
- 以该模式访问了 100 次
- 页通过该模式访问了 N 次,N = 页中记录 * 1/16
异步 IO (Async IO)
- 增加 IO 请求的吞吐
- 底层实现可以进行 IO merge
刷新邻接页 (Flush Neighbor Page)
刷新脏页时,会检测该页所在区的所有页,如果是脏页则一起刷新,好处是可以合并 IO 操作