diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/404.html b/404.html new file mode 100644 index 000000000..55b23821b --- /dev/null +++ b/404.html @@ -0,0 +1 @@ +
SQLite 现在已经是全球用户最多的数据库产品。它非常小巧以及单文件无单独操作系统进程,就像病毒一样依附在宿主程序的进程里运行。你看不到它,但它却无处不在。
汽车,手机,浏览器,以及各类 app 里都能见到 .db 结尾的 SQLite 数据库文件。 假如 SQLite 出现重大 bug,或者像平常的数据库那样无法连接,整个地球都会乱套。你身边用的几乎所有电子产品(手机,电脑,iPad,笔记本)和嵌入式设备全部都会出问题。它的诞生到大范围全球流行的过程和一般软件有着不太一样的发展历程。
SQLite 诞生的契机就是典型的程序员开发的故事剧本。作者 Richard 最开始在一艘军舰上做 contractor(就是我们说的外包)。他们程序跑在军舰安装的电脑上,电脑上装的是 informix。Richard 的工作就是把 informix 的数据拿出来进行计算然后展示到电脑屏幕上(这和我们今天的 CRUD 工作类似)。比较令人恼火的是 informix 很不稳定,经常崩溃连不上。部队里的铁拳长官可不懂啥 TCP/IP 或者数据库系统知识。他们只看到软件的报错 dialog(对话框) 经常弹出来,而这个 dialog 又是 SQLite 的作者 Richard 写的软件画出来的,锅自然从天而降。于是 Richard 决定自己从头写一个无需外部连接的数据库来解决这个问题。
SQLite 是一个 C 实现的 SQL 数据库引擎,它的特点是小型、快速、自包含、高可靠性和功能齐全。SQLite 嵌入在所有手机和大多数计算机中,也捆绑在为数众多的其它应用中,是世界上使用量最大的数据库引擎。SQLite是一个进程内的轻量级嵌入式数据库,它的数据库就是一个文件,实现了自给自足、无服务器、零配置的、事务性的SQL数据库引擎。
它是一个零配置的数据库,这就体现出来SQLite与其他数据库的最大的区别:SQLite不需要在系统中配置,直接可以使用。且SQLite不是一个独立的进程,可以按应用程序需求进行静态或动态连接。SQLite可直接访问其存储文件。
SQLite 经过 20 多年发展,支持几乎所有数据库功能,并且能跑在几乎所有小型的嵌入式环境里。对这些小型环境需要做的优化比普通数据库要更多,实现起来要更复杂。想学习数据库的实现从 SQLite 入手并不是一个好的选择。它虽然运行环境和代码量小巧,但在工程层面实际是个大项目,背后还有无数的单元测试。它做到了软件开发的一个非常难做到的事情:100% 测试覆盖。这意味着后续的任何开发迭代都能做到机器自动化测试,自动找出新加入代码产生的兼容性问题,不得不说这是程序员梦想中的代码迭代方式。SQLite 一开始就诞生在工业界巨头公司的夹缝里面,从最开始它就是一个对运行质量要求极高的工业软件。它看起来像一个精致的小软件,但绝不是一个小玩具,当年他合作的是摩托罗拉和诺基亚这种巨头放在今天也是 facebook,google 这种体量。从它打败诺基亚内部 9 个同类产品这段经历来看,这是很典型的一个头部软件赢家通吃的案例。它是纯粹的商业驱动项目,并不是开源社区的项目,开源只是它商业模式的一部分。
1.SQlite 通过文件来保存数据库,一个文件就是一个数据库。 2.数据库里又包含数个表格; 3.每个表格里面包含了多个记录; 4.每个记录由多个字段组成; 5.每个字段都有其对应的值; 6.每个值都可以指定类型,并且指定约束。
许多SQL数据库引擎(除SQLite之外的各种SQL数据库引擎)使用静态、严格的数据类型。对于静态类型,一个值的数据类型由它的容器,即存储这个值的列来决定。
SQLite则使用更加通用的动态类型系统。在SQLite
中,一个值的数据类型被关联到这个值本身,而不是它的容器。
SQLite的动态类型系统向后兼容一般静态类型系统的数据库引擎。在某种意义上,工作在静态类型数据库上的SQL声明也同样能工作在SQLite上。
但是SQLite动态类型还允许做一些在传统严格类型的数据库中不能做的事情。
当我们查询数据的时候,会先去Buffer Pool中查询。如果Buffer Pool中不存在,存储引擎会先将数据从磁盘加载到Buffer Pool中,然后将数据返回给客户端;
同理,当我们更新某个数据的时候,如果这个数据不存在于Buffer Pool,同样会先数据加载进来,然后修改修改内存的数据。被修改过的数据会在之后统一刷入磁盘。
假设我们修改Buffer Pool中的数据成功,但是还没来得及将数据刷入磁盘MySQL就挂了怎么办?按照上图的逻辑,此时更新之后的数据只存在于Buffer Pool中,如果此时MySQL宕机了,这部分数据将会永久的丢失;
MySQL能够实现崩溃恢复的**事实**来看,MySQL必定实现了某些骚操作。没错,这就是接下来我们要介绍的另外的两个关键功能,Redo Log**和**Undo Log。
这两种日志是属于InnoDB存储引擎的日志,和MySQL Server的Binlog不是一个维度的日志。
这两种日志有明显的区别。
更新数据还是会判断数据是否存在于Buffer Pool中,不存在则加载进来。上面我们提到了回滚的问题,在更新Buffer Pool中的数据之前,我们需要先将该数据事务开始之前的状态写入Undo Log中。假设更新到一半出错了,我们就可以通过Undo Log来回滚到事务开始前。
然后执行器会更新Buffer Pool中的数据,成功更新后会将数据最新状态写入Redo Log Buffer中。因为一个事务中可能涉及到多次读写操作,写入Buffer中分组写入,比起一条条的写入磁盘文件,效率会高很多。
undo log 是mysql中比较重要的事务日志之一undo log是一种用于撤销回退的日志。
在一个事务没提交之前,MySQL会先记录更新前的数据到 undo log日志文件里面,当事务回滚时或者数据库崩溃时,可以利用 undo log来进行回退。
在MySQL中,undo log日志的作用主要有两个:
1、提供回滚操作【undo log实现事务的原子性】
在设计DB时,我们假设数据库可能在任何时刻,由于如硬件故障,软件Bug,运维操作等原因突然崩溃。
这个时候尚未完成提交的事务可能已经有部分数据写入了磁盘,如果不加处理,会违反数据库对Atomic的保证,也就是任何事务的修改要么全部提交,要么全部取消。
针对这个问题,直观的想法是等到事务真正提交时,才能允许这个事务的任何修改落盘,也就是No-Steal策略。显而易见,这种做法一方面造成很大的内存空间压力,另一方面提交时的大量随机IO会极大的影响性能。
因此,数据库实现中通常会在正常事务进行中,就不断的连续写入Undo Log,来记录本次修改之前的历史值。
当Crash真正发生时,可以在Recovery过程中通过回放Undo Log将未提交事务的修改抹掉。InnoDB采用的就是这种方式。
我们在进行数据更新操作的时候,不仅会记录redo log,还会记录undo log,如果因为某些原因导致事务回滚,那么这个时候MySQL就要执行回滚(rollback)操作,利用undo log将数据恢复到事务开始之前的状态。
例如如我们执行下面一条删除语句:
delete from user where id = 1;
+
那么此时undo log会记录一条对应的insert 语句【反向操作的语句】,以保证在事务回滚时,将数据还原回去。
如果这个修改出现异常,可以使用undo log日志来实现回滚操作,以保证事务的一致性。
2、MVCC,即多版本控制。在MySQL数据库InnoDB存储引擎中,用undo Log来实现多版本并发控制(MVCC)。当读取的某一行被其他事务锁定时,它可以从undo log中分析出该行记录以前的数据版本是怎样的,从而让用户能够读取到当前事务操作之前的数据【快照读】。
下面解释一下什么是快照读,与之对应的还有一个是---当前读。
快照读:
SQL读取的数据是快照版本【可见版本】,也就是历史版本,不用加锁,普通的SELECT就是快照读。
当前读:
SQL读取的数据是最新版本。通过锁机制来保证读取的数据无法通过其他事务进行修改UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE都是当前读。
所谓表空间其实是真实存在于磁盘上的数据文件。而这里的所说的 undolog表空间 其实就是磁盘上专门存放 undolog 的文件。
在MySQL5.5以及之前,InnoDB的undo log也是存放在 ibdata1 里面的。一旦出现大事务,这个大事务所使用的undolog占用的空间就会一直在ibdata1里面存在,即使这个事务已经关闭。
MySQL从8.0开始undo 表空间管理已经发生了改变,在5.7版本中一旦MySQL初始化以后,就不能再改变undo表空间了。
所以我们在5.7版本中都是在初始化的时候对undo表空间进行一些设置,类似这样:在my.cnf文件中加入innodb_undo_directory= /data/mysql/undologs 和 innodb_undo_tablespaces=5 这两个参数。
之所以这么改,是因为我们想把undo表空单独从系统表空间idbdata中分离出来,这样就可以消除因undo的问题造成对ibdata系统表空间的影响。
所以上面的参数配置在5.7版本中是我们对MySQL初始化做的一个常规的最佳实践设置,如果不设置,那么在5.7版本中,undo还是默认会放在ibdata中。
从MySQL8.0版本开始,MySQL默认对undo进行了分离操作,使用默认配置初始化时,就会在datadir目录下生成两个10MiB大小的undo表空间文件undo_001 和 undo002
默认至少初始化2个Undo表空间,最大支持127个Undo表空间,默认表空间名称为undo_001,undo_002
因为大事务可能会导致单个undo文件变的很大,创建额外的表空间文件可以避免这个文件,8.0.14 之后UNDO表空间支持**在线扩缩容**
CREATE UNDO TABLESPACE tablespace_name ADD DATAFILE 'file_name.ibu';
+-- 不支持指定相对路径,只支持绝对路径,且必须是innodb_directories参数定义可识别的路径或默认的数据目录下
+
innodb_directories
参数定义可识别的路径或默认的数据目录下# 8.0.23之前,undo初始化大小依赖于inoodb_page_size,对于默认的16KB的页,undo默认是10MiB
+
#
+
+mysql> show VARIABLES like '%undo%';
++--------------------------+------------+
+| Variable_name | Value |
++--------------------------+------------+
+| innodb_max_undo_log_size | 1073741824 |
+| innodb_undo_directory | ./ |
+| innodb_undo_log_encrypt | OFF |
+| innodb_undo_log_truncate | ON |
+| innodb_undo_tablespaces | 2 |
++--------------------------+------------+
+5 rows in set (0.01 sec)
+
+mysql>
+
+
+show variables like '%undo%';
++--------------------------+------------+
+| Variable_name | Value |
++--------------------------+------------+
+| innodb_max_undo_log_size | 8589934592 |
+| innodb_undo_directory | ./ |
+| innodb_undo_log_encrypt | OFF |
+| innodb_undo_log_truncate | ON |
+| innodb_undo_tablespaces | 2 |
++--------------------------+------------+
+
+show variables like '%truncate%';
++--------------------------------------+-------+
+| Variable_name | Value |
++--------------------------------------+-------+
+| innodb_purge_rseg_truncate_frequency | 128 |
+| innodb_undo_log_truncate | ON |
++--------------------------------------+-------+
+
+show variables like '%segment%';
++-------------------------------+-----------+
+| Variable_name | Value |
++-------------------------------+-----------+
+| innodb_rollback_segments | 128 |
+| innodb_segment_reserve_factor | 12.500000 |
++-------------------------------+-----------+
+
+innodb_undo_log_truncate --控制是否自动做UNDO的truncate收缩操作,默认为ON,只有为ON时,下面2个参数才生效
+innodb_max_undo_log_size --控制UNDO做truncate收缩操作的阈值,当UNDO达到该值时才出发收缩操作
+innodb_purge_rseg_truncate_frequency
+ -- Batch UNDO清理的次数,默认最大值128,也就是128次后才会触发一次UNDO的truncate,而每次清理的undo page由innodb_purge_batch_size参数决定,innodb_purge_batch_size默认为300,也就是300*128个UNDO小批次清理后才会触发UNDO表空间的truncate(也就是UNDO表空间的收缩)操作
+
+innodb_undo_tablespaces
+-- 控制生成的UNDO表空间的数量,默认2个,在8.0对该参数做了废弃,但并未提供其他参数控制UNDO数量,当前依旧可以使用该参数做UNDO表空间数量配置,通常建议配置为3(手工收缩UNDO时需要至少3个UNDO表空间)
+
+innodb_rollback_segments -- UNDO表空间回滚段的数量,默认为最大值128
+
-- 可以查看到undo的表空间名称/文件路径/初始大小/扩展大小/磁盘文件大小/可用空间及是否启用的状态等
+SELECT T1.SPACE AS SPACE_ID,
+ T1.NAME AS TABLESPACE_NAME,
+ T2.FILE_NAME,
+ ROUND(T2.INITIAL_SIZE / 1024 / 1024, 2) AS "INITIAL_SIZE(M)",
+ ROUND(T2.AUTOEXTEND_SIZE / 1024 / 1024, 2) AS "AUTOEXTEND_SIZE(M)",
+ ROUND(T1.FILE_SIZE / 1024 / 1024, 2) AS "FILE_SIZE_DISK(M)",
+ ROUND(T2.DATA_FREE / 1024 / 1024, 2) AS "DATA_FREE(M)",
+ T2.STATUS,
+ T1.STATE
+ FROM INFORMATION_SCHEMA.INNODB_TABLESPACES T1,
+ INFORMATION_SCHEMA.FILES T2
+ WHERE T1.SPACE = T2.FILE_ID
+ AND T1.ROW_FORMAT = 'Undo';
+
+
+
+
+
+-- 创建一个新的UNDO表空间
+CREATE UNDO TABLESPACE undo_004 ADD DATAFILE 'undo_004.ibu';
+
+-- 可以用前面的命令查看创建后的状态
+
+-- 可以将已有的UNDO表示为inactive(也可理解为UNDO表空间收缩)
+-- PS:设置为INACTIVE的表空间的STATE为empty,表示这个表空间不包含任何事务回滚数据,且表空间也收缩为默认大小
+ALTER UNDO TABLESPACE undo_003 SET INACTIVE;
+
+-- 可以将inactive的UNDO转为active
+ALTER UNDO TABLESPACE innodb_undo_001 SET ACTIVE;
+
+-- 可以将inactive的UNDO表空间进行删除
+-- PS:默认以innodb_开头初始化的undo表空间不可被删除
+DROP UNDO TABLESPACE innodb_undo_001;
+ERROR: 3119 (42000): InnoDB: Tablespace names starting with `innodb_` are reserved.
+
+-- 非系统默认的UNDO在inactive后可被删除
+ALTER UNDO TABLESPACE undo_003 SET ACTIVE;
+Query OK, 0 rows affected (0.0030 sec)
+
PS:通常对表空间做收缩前最简单避免性能的方式是提前创建一个UNDO表空间,收缩完后再删除或一直保留均可
Doublewrite Buffer被MySQL放在官方文档目录的 "InnoDB On-Disk Structures" 中了,但真实情况是Doublewrite Buffer是内存+磁盘的结构。
操作系统可以看成是一个程序,作为程序而言,都有最小处理单位的说法,我们常见的服务器一般都是Linux操作系统,对应文件系统的页(OS Page)就可以看成是Linux操作系统与文件系统交互的最小单位。
一般情况下,除了操作系统的页(OS Page)为4KB之外,其余程序的页(Page)都会大于等于操作系统的页大小,比如,Oracle的Page大小为8KB。
MySQL的Page大小也可以通过上面innodb_page_size参数指定,具体情况如下:
说了这么多,其实大多数情况一般都不用修改,使用默认值即可。
有点扯远了。我们知道操作系统的页大小和MySQL的页大小了,而且MySQL程序是跑在Linux操作系统上的,所以可以得出如下结论:
MySQL将Buffer Pool中一页数据刷入磁盘,要写4个文件系统里的页(也可以说成一个MySQL数据页映射4个系统页)
为了解决**部分页面写入**问题(Partial Page Write)。
MySQL写入修改时刷新整个页面(默认16KB),而不仅仅是刷新页面中已更改的记录。而系统的单次io,一般是512byte为单位的,在断电,OS crash(操作系统崩溃)情况下可能会丢失数据。
写入顺序:先写doublewrite buffer,写**成功后**再写到数据文件。
# 指定df文件的存储路径,默认是跟innodb_data_home_dir,一般就是数据目录datadir
+innodb_doublewrite_dir=innodb_data_home_dir
+
+# 指定db文件的数量,默认为每个buffer_pool_instance创建2个,最小是2,最大是256
+innodb_doublewrite_files=
+
由以下几个参数决定:
8.0.20之前,Doublewrite buffer是系统表空间中连续的128个页(每个页16k),总共2M
8.0.20开始的doublewrite buffer由单独文件保存:
# Doubliewrite文件命名
+# #_页大小_文件编号.dblwr
+# 页大小一般默认就是16KB,由innodb_page_size控制
+-rwxrwxrwx 1 root root 192K 7月 6 23:15 '#ib_16384_0.dblwr'
+-rwxrwxrwx 1 root root 8.2M 7月 6 23:16 '#ib_16384_1.dblwr'
+
清洗线程数由innodb_page_cleaners配置(8.0.20版本默认为4),单个线程最多每次写入doublewrite buffer 的页面数由innodb_doublewrite_pages变量控制, 在默认情况下为innodb_write_io_threads的值(默认4), 每秒写入次数还受到innodb_io_capacity (定义了每秒I / O操作数,默认值200)和innodb_io_capacity_max(InnoDB在由后台任务每秒执行的最大IOPS数 )控制。
一般都认为,redo提供了崩溃恢复功能,
数据库是数据的集合,数据库管理系统(DBMS)是操作和管理数据库的应用程序。数据库应用主要有两类:OLAP(联机分析处理)和OLTP(联机事务处理)。
OLAP的主要特点是:
OLTP的主要特点是:
mysql是一种DBMS,其体系架构如下图所示:
mysql中集成的是插件式的存储引擎,InnoDB引擎是其中之一。存储引擎基于表而不是数据库:同一个数据库中根据不同表的访问操作需求可以选择不同的存储引擎。
InnoDB引擎主要面对OLTP类应用。
InnoDB引擎在mysql中处于文件和文件系统的上层,管理着对InnoDB引擎表的访问和更新。
MySQL5.7版本中的架构图:
MySQL8.0 版本中的架构图:
图片来源与官网:
https://dev.mysql.com/doc/refman/5.7/en/innodb-architecture.html
以下内容全部引自: InnoDB存储引擎——Master Thread工作方式
(当做备份)
Master Thread是InnoDB存储引擎非常核心的一个后台线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲、UNDO页的回收等。
Master Thread具有 最高的线程优先级别。
其内部由多个循环组成:主循环(loop)、后台循环(backgroup loop)、刷新循环(flush loop)、暂停循环(suspend loop)。
Master Thread会 根据数据库运行的状态 在loop、backgroup loop、flush loop和suspend loop中 进行切换。
loop是主循环,大多数的操作都在这个循环中,主要有两大部分的操作——每秒钟的操作和每10秒钟的操作。伪代码如下:
void master_thread()
+{
+ loop:
+ for(int i = 0; i < 10; ++i){
+ do thing once per second;
+ sleep 1 second if necessary;
+ }
+ do things once per ten seconds;
+ goto loop;
+}
+
每秒一次的操作包括:
即使某个事务还没有提交,InnoDB存储引擎仍然每秒会将重做日志缓冲中的内容刷新到重做日志文件。这也解释了为什么再大的事务提交的时间也是很短的。
合并插入缓冲并不是每秒都会发生的。InnoDB存储引擎会判断当前一秒内发生的IO次数是否小于5次,如果小于5次,InnoDB存储引擎认为当前的IO压力很小,可以执行合并插入缓冲的操作;
至多刷新100个InnoDB的缓冲池中的脏页到磁盘(可能); 刷新100个脏页也不是每秒都会发生的,InnoDB存储引擎通过判断当前缓冲池中脏页的比例(buf_get_modified_ratio_pct)是否超过了配置文件中 innodb_max_dirty_pages_pct这个参数(默认是75,代表75%),如果超过了这个值,InnoDB存储引擎则认为需要做磁盘同步的操作,将100个脏页写入磁盘中。
如果当前没有用户活动,则切换到background loop(可能);
综上所述,伪代码可以进一步具体化。
void master_thread()
+{
+ loop:
+ for(int i = 0; i < 10; ++i){
+ thread_sleep(1);
+ do log buffer flush to disk;
+ if(last_one_second_ios < 5)
+ do merge at most 5 insert buffer;
+ if(buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct)
+ do buffer pool flush 100 dirty page;
+ if(no user activity)
+ goto backgroud loop;
+ }
+ do things once per ten seconds;
+ backgroud loop;
+ do something;
+ goto loop;
+}
+
每10秒的操作主要是下面几个方面:
刷新100个脏页到磁盘(可能) InnoDB存储引擎会先判断过去10秒之内磁盘的IO操作是否小于200次,如果是,InnoDB存储引擎认为当前有足够的磁盘IO能力,因此将100个脏页刷新到磁盘。
合并至多5个插入缓冲(总是)
将日志缓冲刷新到磁盘(总是)
删除无用的Undo页(总是)
刷新100个或者10个脏页到磁盘(总是) InnoDB存储引擎会执行full purge操作,即删除无用的Undo页。对表进行update,delete这类的操作时,原先的行被标记为删除,但是因为一致性读的关系,需要保留这些行版本的信息。但是在full purge过程中,InnoDB存储引擎会判断当前事务系统中已被删除的行是否可以删除,比如有时候可能还有查询操作需要读取之前版本的undo信息,如果可以删除,InnoDB存储引擎会立即将其删除。从源代码中可以看出,InnoDB存储引擎在执行full purge 操作时,每次最多尝试回收20个undo页。
然后,InnoDB存储引擎会判断缓冲池中脏页的比例(buf_get_modified_ratio_pct),如果有超过70%的脏页,则刷新100个脏页到磁盘,如果脏页的比例小于70%,则只需刷新10%的脏页到磁盘。
伪代码进一步细化:
void master_thread()
+{
+ loop:
+ for(int i = 0; i < 10; ++i){
+ thread_sleep(1);
+ do log buffer flush to disk;
+ if(last_one_second_ios < 5)
+ do merge at most 5 insert buffer;
+ if(buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct)
+ do buffer pool flush 100 dirty page;
+ if(no user activity)
+ goto backgroud loop;
+ }
+ if(last_ten_second_ios < 200)
+ do buffer pool flush 100 dirty page;
+
+ do merge at most 5 insert buffer;
+ do log buffer flush to disk;
+ do full purge;
+ if(buf_get_modified_ratio_pct > 70%)
+ do buffer pool flush 100 dirty page;
+ else
+ buffer pool flush 10 dirty page;
+ goto loop;
+ backgroud loop;
+ do something;
+ goto loop;
+}
+
如果当前没有用户活动(数据库空闲)或者数据库关系,就会切换到backgroud loop这个循环。 backgroud loop会执行以下操作:
如果flush loop中也没有什么事情可以做了,InnoDB存储引擎会切换到suspend_loop,将Master Thread挂起,等待事件的发生。若用户启用了InnoDB存储引擎,却没有使用任何InnoDB存储引擎的表,那么Master Thread总是处于挂起的状态。
最后,Master Thread完整的伪代码如下:
void master_thread()
+{
+ loop:
+ for(int i = 0; i < 10; ++i){
+ thread_sleep(1); // sleep 1秒
+ do log buffer flush to disk;
+ if(last_one_second_ios < 5)
+ do merge at most 5 insert buffer;
+ if(buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct)
+ // 如果缓冲池中的脏页比例大于innodb_max_dirty_pages_pct(默认是75时)
+ do buffer pool flush 100 dirty page; // 刷新100脏页到磁盘
+ if(no user activity)
+ goto backgroud loop;
+ }
+ if(last_ten_second_ios < 200) // 如果过去10内磁盘IO次数小于设置的innodb_io_capacity的值(默认是200)
+ do buffer pool flush 100 dirty page;
+
+ do merge at most 5 insert buffer; // 合并插入缓冲是innodb_io_capacity的5%(10)(总是)
+ do log buffer flush to disk;
+ do full purge;
+ if(buf_get_modified_ratio_pct > 70%)
+ do buffer pool flush 100 dirty page;
+ else
+ buffer pool flush 10 dirty page;
+
+ backgroud loop: // 后台循环
+ do full purge // 删除无用的undo页 (总是)
+ do merge 20 insert buffer; // 合并插入缓冲是innodb_io_capacity的5%(10)(总是)
+ if not idle // 如果不空闲,就跳回主循环,如果空闲就跳入flush loop
+ goto loop: // 跳到主循环
+ else
+ goto flush loop
+
+ flush loop: // 刷新循环
+ do buffer pool flush 100 dirty page;
+ if(buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct)
+ // 如果缓冲池中的脏页比例大于innodb_max_dirty_pages_pct的值(默认75%)
+ goto flush loop; // 跳到刷新循环,不断刷新脏页,直到符合条件
+
+ goto suspend loop; // 完成刷新脏页的任务后,跳入suspend loop
+
+ suspend loop:
+ suspend_thread(); //master线程挂起,等待事件发生
+ waiting event;
+ goto loop;
+}
+
1.0.x版本中,InnoDB存储引擎最多只会刷新100个脏页到磁盘,合并20个插入缓冲。如果是在写入密集的应用程序中,每秒可能会产生大于100个的脏页,如果是产生大于20个插入缓冲的情况,那么可能会来不及刷新所有的脏页以及合并插入缓冲。
后来,InnoDB存储引擎提供了参数innodb_io_capacity,用来表示磁盘IO的吞吐量,默认值为200。
mysql> show variables like 'innodb_io_capacity';
++--------------------+-------+
+| Variable_name | Value |
++--------------------+-------+
+| innodb_io_capacity | 200 |
++--------------------+-------+
+1 row in set (0.00 sec)
+
对于刷新到磁盘的页的数量,会按照innodb_io_capacity的百分比来进行控制。规则如下:
如果用户使用的是SSD类的磁盘,可以将innodb_io_capacity的值调高,直到符合磁盘IO的吞吐量为止;
另一个问题是参数innodb_max_dirty_pages_pct的默认值,在1.0.x版本之前,该值的默认值是90,意味着脏页占缓冲池的90%。InnoDB存储引擎在每秒刷新缓冲池和flush loop时会判断这个值,如果该值大于innodb_max_dirty_pages_pct,才会刷新100个脏页,如果有很大的内存,或者数据库服务器的压力很大,这时刷新脏页的速度反而会降低。 后来将innodb_max_dirty_pages_pct的默认值改为了75。这样既可以加快刷新脏页的频率,又能够保证磁盘IO的负载。
mysql> show variables like 'innodb_max_dirty_pages_pct';
++----------------------------+-------+
+| Variable_name | Value |
++----------------------------+-------+
+| innodb_max_dirty_pages_pct | 75 |
++----------------------------+-------+
+1 row in set (0.00 sec)
+
还有一个新的参数是innodb_adaptive_flushing(自适应地刷新),该值影响每秒刷新脏页的数量。原来的刷新规则是:脏页在缓冲池所占的比例小于innodb_max_dirty_pages_pct时,不刷新脏页;大于innodb_max_dirty_pages_pct时,刷新100个脏页。随着innodb_adaptive_flushing参数的引入,InnoDB通过一个名为buf_flush_get_desired_flush_rate的函数来判断需要刷新脏页最合适的数量。buf_flush_get_desired_flush_rate函数通过判断产生重做日志的速率来决定最合适的刷新脏页数量。
之前每次进行full purge 操作时,最多回收20个Undo页,从InnoDB 1.0.x版本开始引入了参数innodb_purge_batch_size,该参数可以控制每次full purge回收的Undo页的数量。该参数的默认值为20,并可以动态地对其进行修改。
mysql> show variables like 'innodb_purge_batch_size';
++-------------------------+-------+
+| Variable_name | Value |
++-------------------------+-------+
+| innodb_purge_batch_size | 20 |
++-------------------------+-------+
+1 row in set (0.00 sec)
+
Master Thread的伪代码变为了下面的形式:
void master_thread()
+{
+ loop:
+ for(int i = 0; i < 10; ++i){
+ thread_sleep(1);
+ do log buffer flush to disk;
+ if(last_one_second_ios < 5%innodb_io_capacity)
+ do merge 5%innodb_io_capacity insert buffer;
+ if(buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct)
+ do buffer pool flush 100%innodb_io_capacity dirty page;
+ else if enable adaptive flush
+ do buffer pool flush desired amount dirty page;
+ if(no user activity)
+ goto backgroud loop;
+ }
+ if(last_ten_second_ios < innodb_io_capacity)
+ do buffer pool flush 100%innodb_io_capacity dirty page;
+
+ do merge 5%innodb_io_capacity insert buffer;
+ do log buffer flush to disk;
+ do full purge;
+ if(buf_get_modified_ratio_pct > 70%)
+ do buffer pool flush 100%innodb_io_capacity dirty page;
+ else
+ do buffer pool flush 10%innodb_io_capacity dirty page;
+
+ goto loop;
+ backgroud loop:
+ do full purge
+ do merge 100%innodb_io_capacity insert buffer;
+ if not idle
+ goto loop:
+ else
+ goto flush loop
+
+ flush loop:
+ do buffer pool flush 100%innodb_io_capacity dirty page;
+ if(buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct)
+ goto flush loop;
+
+ goto suspend loop;
+
+ suspend loop:
+ suspend_thread();
+ waiting event;
+ goto loop;
+}
+
这个版本的性能得到了提高。
mysql> show engine innodb status\G
+*************************** 1. row ***************************
+ Type: InnoDB
+ Name:
+Status:
+=====================================
+170312 20:14:04 INNODB MONITOR OUTPUT
+=====================================
+Per second averages calculated from the last 38 seconds
+-----------------
+BACKGROUND THREAD
+-----------------
+srv_master_thread loops: 1 1_second, 1 sleeps, 0 10_second, 1 background, 1 flush
+srv_master_thread log flush and writes: 1
+
可以看到主循环进行了1次,每秒的操作进行了1次,10秒一次的操作进行了0次,backgound loop进行了1次,flush loop进行了1次。
1.2.x版本中再次对Master Thread进行了优化。 Master Thread的伪代码如下:
if InnoDB is idle
+ srv_master_do_idle_tasks();
+else
+ srv_master_do_active_tasks();
+
其中srv_master_do_idle_tasks()就是之前版本中每10秒的操作,srv_master_do_active_tasks()处理的是之前每秒中的操作。同时,对于刷新脏页的操作,从Master Thread线程分离到一个单独的Page Cleaner Thread,从而减轻了Master Thread的工作,同时进一步提高了系统的并发性。
InnoDB中大量使用**AIO (Async IO)** 来处理IO请求。
IO Thread的作用,是负责这些 IO 请求的回调(call back)
可使用 show engine innodb status
看到以下类型
事务被提交后,其所使用的undo log可能不在需要。因此,需要purge thread来回收已经使用并分配的undo页。
以前Master Thread来完成释放undo log,InnoDB1.1独立出来,分担主线程压力
mysql> show variables like 'innodb_purge_threads';
++----------------------+-------+
+| Variable_name | Value |
++----------------------+-------+
+| innodb_purge_threads | 4 |
++----------------------+-------+
+1 row in set (0.02 sec)
+
**脏页**刷新到磁盘
以前Master Thread来刷新脏页,InnoDB1.2独立出来,分担主线程压力
如果您的服务器由于硬件或软件问题而意外退出,无论当时数据库中发生了什么,重新启动数据库后都不需要执行任何特殊操作。InnoDB崩溃恢复
会自动提交 崩溃之前已提交 的所有事务并落盘持久化,并撤消所有 正在处理但尚未提交 的事务。只需重新启动并从上次中断的地方继续即可。
InnoDB存储引擎有自己的缓冲池
,在访问数据时可以在主存中缓存 表 和 索引数据 。经常使用的数据直接从 内存 中处理。此缓存适用于许多类型的信息,并加快处理速度。在专用数据库服务器上,通常会将高达 80% 的物理内存分配给缓冲池。
如果将相关数据分割到不同的表中,则可以设置执行参照完整性的外键
。更新或删除数据,其他表中的相关数据将 自动 更新或删除。如果尝试将数据插入到一个辅助表中,而主表中没有相应的数据,那么 错误的数据将自动被踢出 。
如果数据在磁盘或内存中损坏,则校验机制
会在使用前提醒您注意虚假数据。
当您为每个表使用适当的主键
列设计数据库时,涉及这些列的操作将 自动优化 。在 WHERE 子句、 ORDER BY 子句、 GROUP BY 子句和 join操作 中引用主键列非常快。
插入、更新和删除由一种称为change buffering
的自动机制进行优化。InnoDB不仅允许对同一个表并发读和写访问,它还 缓存修改后的数据 以简化磁盘I/O。
自适应哈希索引
。当从一个表中一遍又一遍访问相同的数据时,InnoDB会自动对这些数据建立自适应哈希索引,就像从哈希表中查出来一样。
您可以压缩表和关联的索引。
您可以创建和删除索引,而对性能和可用性的影响要小得多。
截断表空间文件非常快,可以释放磁盘空间供操作系统
重用,而不是释放只有InnoDB才能重用的系统表空间
内的空间。
对于BLOB
和长TEXT
字段,使用动态行格式
的表数据的存储布局更有效。
您可以通过查询INFORMATION_SCHEMA
表来监视存储引擎的内部工作。
您可以通过查询性能模式
表来监视存储引擎的性能细节。
您可以自由地将InnoDB
表与其他MySQL存储引擎的表混合使用,即使在同一条语句中也是如此。例如,您可以使用JOIN操作在单个查询中合并来自InnoDB
和 MEMORY
表的数据。
InnoDB是为处理大数据量时的CPU效率和最高性能而设计的。
InnoDB表可以处理大量数据,即使是在文件大小限制在2GB的操作系统上。
为每个表中查询最频繁的一列或几列指定一个主键,如果没有明显的主键,则指定一个 自动递增的值 。
使用join连接从多个表中根据相同的ID值提取数据时,为了提高连接性能,可以在连接列上定义外键
,并在每个表中使用 相同的数据类型 声明这些列。 添加外键可以确保引用的列被索引,这可以提高性能。外键还将 删除 或 更新 传播到所有受影响的表,并防止在父表中没有对应id的情况下在子表中插入数据。
关闭自动提交
。每秒提交数百次会限制性能(受存储设备的写入速度限制)。
通过使用START TRANSACTION
和COMMIT
语句将相关的DML操作sql分组到事务中。不建议过于频繁地提交,也不建议发出一个批次中含有大量的INSERT、UPDATE或DELETE语句,这些语句可能运行几个小时。
不建议使用LOCK TABLES
语句。InnoDB
可以同时处理对同一个表进行读写的多个会话,而无需牺牲可靠性或高性能。要获得对一组行的排他性写访问权限,请使用 SELECT ... FOR UPDATE
语法仅锁定要更新的行。
启用innodb_file_per_table
选项,或者使用常规表空间
将表的数据
和索引
放在 单独的文件 中,而不是系统表空间
中。 默认情况下innodb_file_per_table
选项是启用的。
评估您的数据和访问模式是否可以从InnoDB
表或页面压缩功能中受益。您可以在InnoDB
不牺牲读/写功能的情况下压缩表。
使用选项sql_mode=NO_ENGINE_SUBSTITUTION
运行服务器,以防止在CREATE TABLE的engine =子句中指定的引擎出现问题时使用不同的存储引擎创建表。
可以通过如下SQL语句来查询MySQL支持的各种存储引擎
show engines;
+
+SELECT * FROM INFORMATION_SCHEMA.ENGINES;
+
Engine | Support | Comment | Transactions | XA | Savepoints |
---|---|---|---|---|---|
InnoDB | DEFAULT | Supports transactions, row-level locking, and foreign keys | YES | YES | YES |
MRG_MYISAM | YES | Collection of identical MyISAM tables | NO | NO | NO |
MEMORY | YES | Hash based, stored in memory, useful for temporary tables | NO | NO | NO |
BLACKHOLE | YES | /dev/null storage engine (anything you write to it disappears) | NO | NO | NO |
MyISAM | YES | MyISAM storage engine | NO | NO | NO |
CSV | YES | CSV storage engine | NO | NO | NO |
ARCHIVE | YES | Archive storage engine | NO | NO | NO |
PERFORMANCE_SCHEMA | YES | Performance Schema | NO | NO | NO |
FEDERATED | NO | Federated MySQL storage engine | NULL | NULL | NULL |
以下内容引自 InnoDB的关键特性
回顾一下,InnoDB是索引组织表,主键是唯一标识。
在数据量较小的系统中经常使用的自增ID做为主键,利用数据库的自增ID,从1开始,基本可以做到连续递增。
但是对于有多个二级索引的表,二级索引的叶子节点的数据插入,就不是顺序的了。
插入缓冲是为了 提高插入数据效率 的,在往非聚簇索引中插入数据时,首先会看缓冲池中,是否有要插入的非聚簇索引页。如果有则直接插入,那个页变成脏页。如果没有,就把**多次插入的数据先缓冲到插入缓冲中**,然后**合并多次操作**,即把非聚集索引在一起的数据合并为一次IO(减少IO),再以一定的频率刷新到磁盘(将Insert Buffer和辅助索引页字节点进行merge操作),但是插入缓冲只是针对**非聚集索引没有非唯一约束的索引**的插入有效。
(类似于MRR,随机IO变顺序IO)
InnoDB的数据是按主键的索引的顺序来存放数据的,这种索引被称为聚集索引,一个表有且只有一个聚集索引。但我们可以为一个表创建多个索引,这些索引被称为非聚集索引,或者辅助索引。这些索引是存放在另外的索引页的,跟实际数据的存放是没有关系的。非聚集索引记录的是索引列的数据,不是全部的列。
如果不了解聚集索引,有可能会被绕晕,但是,至少可以得出一个结论,就是聚集索引是按顺序存放的,而非聚集索引是没有必然的顺序的。这就导致一个问题,当插入一条数据时,更新非聚集索引时,必须到处去找非聚集索引所在的页(也称为离散写),这样的插入效率就比较低了。
如果该非聚集索引有唯一约束,那么为了保证唯一性,必须每次插入前都去查询是否存在相同的数据,这时就必须到磁盘到处找是否存在该值(也称为离散读),这就导致插入缓冲失效了。
插入缓冲是为了解决非聚集索引随机写导致的效率低的问题,但是对于有唯一约束的非聚集索引也无能为力。
在机械硬盘里,离散读必须要移动磁头,所以速度真的很慢,但是随着固态硬盘的发展,也许这个就不是问题了。固态硬盘的随机读写能力很强,而且现在固态硬盘的价格也低了不少。不过要让mysql支持固态硬盘,可能还有配置不少参数,直接上固态硬盘似乎效果不明显呢。
在写密集的情况下,插入缓冲会占用过多的缓冲池内存,默认最大可以占用½的缓冲池内存,修正这个问题可以修改默认值对插入缓冲的大小进行控制。
mysql> show variables like 'innodb_change_buffering';
++-------------------------+-------+
+| Variable_name | Value |
++-------------------------+-------+
+| innodb_change_buffering | all |
++-------------------------+-------+
+1 row in set, 1 warning (0.00 sec)
+
mysql> show variables like 'innodb_change_buffer_max_size';
++-------------------------------+-------+
+| Variable_name | Value |
++-------------------------------+-------+
+| innodb_change_buffer_max_size | 25 |
++-------------------------------+-------+
+1 row in set, 1 warning (0.00 sec)
+
insert buffer内部是一颗B+树,mysql4.1之前是每张表有一颗insert buffer B+树,而现在的版本是全局只有一颗B+树,负责对所有的表的辅助索引进行insert buffer, 这颗B+树存放在共享表空间中,即ibdata1中。
Insert Buffer中B+树的非叶子节点中存放的是查询的serarch key(键值),结构如下图所示
+-------+--------+--------+
+| space | marker | offset |
++-------+--------+--------+
+
search key一共占有9个字节:
当一个辅助索引要插入到页(space,offset)时,如果这个页不在缓冲池中,则按照上述结构构造一个search key,然后查询Insert Buffer的B+树,将这条记录插入到Insert BufferB+树中。
对于Insert Buffer的B+树的叶子节点,并非只有记录,而是如下所示:
+-------+--------+--------+----------+-----------+-----------+
+| space | marker | offset | metadata | 辅助索引值 | ... |
++-------+--------+--------+----------+-----------+-----------+
+
前面和非叶子结点的含义相同,一共占用9个字节,metadata占用4个字节,从第5列开始,就是实际插入记录的各个字段了,所以与原插入记录相比较,B+树的叶子节点记录需要额外记录13字节的开销
metadata中存储的内容
名称 字节 含义 IBUF_REC_OFFSET_COUONT 2 对每个进入Insert Buffer的记录进行排序 IBUF_REC_OFFSET_TYPE 1 每个进入Insert Buffer的记录类型 IBUF_REC_OFFSET_FLAGS 1 每个进入Insert Buffer的记录标记
因为启用Insert Buffer之后,辅助索引页(space,offset)可能被记录到Insert Buffer的B+树中,所以为了保证每次Merge Insert Buffer必须成功,需要一个特殊的页来标记每个辅助索引页(space,offset)的可用空间。
每个Insert Buffer BitMap页用来追踪16384个辅助索引,也就是256个区(Extent)。每个Insert Buffer BitMap在16384个页的第二页中。
相关知识点:
- 每个区64页,一页16KB,一个区所占大小为: 16 * 64 = 1024KB = 1M
- 256个区,一个区64页,则一个BiMap可以追踪的页为:256 * 64 = 16384
每个辅助索引页在Insert Buffer BitMap中,占用4位(bit),由下表三部分组成
名称 大小 说明 IBF_BITMAP_FREE 2 表示该辅助索引页可用空间数量,可取值为
0表示无可用空间
1表示剩余空间 > 1/32 页(512字节)
2表示剩余空间 > 1/16 页
3表示剩余空间 > ⅛ 页IBF_BITMAP_BUFFERED 1 1表示该辅助索引页有记录被缓存在Insert Buffer B+树中 IBF_BITMAP_IBUF 1 1表示该页为Insert Buffer B+树的索引页 相关知识点:
- 为什么一个页中,至少要有 1/32 的剩余空间? 按照一般情况来说,一页为16K,16 * 1024 / 32 = 512 B。从磁盘的物理结构来看存取信息的最小单位是扇区,一个扇区大小为 512 B.
merge insert buffer 的操作可能会发生在以下几种情况:
Doublewrite Buffer被MySQL放在官方文档目录的 "InnoDB On-Disk Structures" 中了,但真实情况是Doublewrite Buffer是内存+磁盘的结构。
操作系统可以看成是一个程序,作为程序而言,都有最小处理单位的说法,我们常见的服务器一般都是Linux操作系统,对应文件系统的页(OS Page)就可以看成是Linux操作系统与文件系统交互的最小单位。
一般情况下,除了操作系统的页(OS Page)为4KB之外,其余程序的页(Page)都会大于等于操作系统的页大小,比如,Oracle的Page大小为8KB。
MySQL的Page大小也可以通过上面innodb_page_size参数指定,具体情况如下:
说了这么多,其实大多数情况一般都不用修改,使用默认值即可。
有点扯远了。我们知道操作系统的页大小和MySQL的页大小了,而且MySQL程序是跑在Linux操作系统上的,所以可以得出如下结论:
MySQL将Buffer Pool中一页数据刷入磁盘,要写4个文件系统里的页(也可以说成一个MySQL数据页映射4个系统页)
为了解决**部分页面写入**问题(Partial Page Write)。
当前所有数据库普遍采用 Write Ahead Log
策略,即先写日志在修改磁盘数据,这样可以保证内存中丢失的数据可以通过Log恢复。
而日志记录的内容是以数据页为单位的(即数据库最小操作单元)
在PostgreSQL中默认page大小为8KB,在MySQL中默认page大小为16KB,而操作系统的单次IO一般以4KB为单位进行读写,所以写完WAL日志(or redo log)后,在往磁盘写数据页的过程中如果一个page写到一半出现了问题,那么下次启动在根据 WAL 日志恢复时,再基于这个corrupted page
进行恢复肯定是不行的。
一个数据页的大小是16K,假设在把内存中的脏页写到数据库的时候,写了2K突然掉电,也就是说前2K数据是新的,后14K是旧的,那么磁盘数据库这个数据页就是不完整的,是一个坏掉的数据页。redo只能加上旧、校检完整的数据页恢复一个脏块,不能修复坏掉的数据页,所以这个数据就丢失了,可能会造成数据不一致。
写入顺序:先写doublewrite buffer,写**成功后**再写到数据文件。
# Doubliewrite文件命名
+# #_页大小_文件编号.dblwr
+# 页大小一般默认就是16KB,由innodb_page_size控制
+-rwxrwxrwx 1 root root 192K 7月 6 23:15 '#ib_16384_0.dblwr'
+-rwxrwxrwx 1 root root 8.2M 7月 6 23:16 '#ib_16384_1.dblwr'
+
# 控制是否启用双写缓冲区,默认启用。生产环境禁止关闭
+innodb_doublewrite=on
+
+
+# 指定doublewrite文件的存储路径,默认是跟innodb_data_home_dir一起,一般就是数据目录datadir
+innodb_doublewrite_dir=innodb_data_home_dir
+
+# 指定doublewrite文件的数量,默认为每个buffer_pool_instance创建2个dbw文件,最小是2,最大是256
+innodb_doublewrite_files=
+
+# innodb_doublewrite_pages 变量(在 MySQL 8.0.20 中引入)控制每个线程的最大双写页数。如果未指定值,将设置为 innodb_write_io_threads 值。该变量用于高级性能调整。默认值适合大多数用户。
+innodb_doublewrite_pages=innodb_write_io_threads
+
由以下几个参数决定:
8.0.20之前,Doublewrite buffer是系统表空间中连续的128个页(每个页16k),总共2M
清洗线程数由innodb_page_cleaners配置(8.0.20版本默认为4),单个线程最多每次写入doublewrite buffer 的页面数由innodb_doublewrite_pages变量控制, 在默认情况下为innodb_write_io_threads的值(默认4), 每秒写入次数还受到innodb_io_capacity (定义了每秒I / O操作数,默认值200)和innodb_io_capacity_max(InnoDB在由后台任务每秒执行的最大IOPS数 )控制。
一般都认为,redo提供了崩溃恢复功能,
两次写带给innodb存储引擎的是数据页的可靠性
当innodb存储引擎正在写入某个页到表中,而这个页只写了一部分就发生了宕机,称为部分写失效,会导致数据丢失,可以通过重做日志恢复,可是重做日志中记录的是对页的物理操作,如偏移量80,写ddd
操作。
如果这个页本身已经损坏,则重做也没意义,因此,可以在应用重做之前,用户需要一个页的副本,当发生写失效时,通过副本还原该页,再进行重做,这就是doublewrite。
在数据库中,数据是被分成一块一块的。在操作系统中,数据也是被分成一块一块的。 一般情况下,数据库的块要比操作系统的快大,且数据库块的大小是操作系统块的大小的整数倍。 所以,数据库的块没法保证原子地持久化。
Double write 要解决的是 inplace update 的 partial write 的问题。什么叫 partial write?数据库 flush 脏页的时候,系统可能宕机,这个时候,数据库的一个脏页可能只刷了一部分。 而 InnoDB 的 redo log 没有记录整个 page 的内容。因为如果每次修改都记录整个 page,那日志就太大了。 也就是说, old_page + redo_log => new_page。如果 old_page 的内容被写坏了,数据就没法恢复了。 Double write 的做法就是先将 old_page + redo_log 得到的 new_page 先持久化到磁盘上的“另一个地方”。然后再进行 inplace update,如果中途发生宕机,可以从“另一个地方”恢复这个 page 的数据。
partial write的问题是很多数据库设计中都需要考虑到这么一个临界点的问题。MySQL中的页是16k,数据的校验是按照这个为单位进行的,而操作系统层面的数据单位肯定达不到16k(比如是4k),那么一旦发生断电时,只保留了部分写入,如果是Oracle DBA一般对此都会很淡定,说用redo来恢复嘛。但可能我们被屏蔽了一些细节,MySQL在恢复的过程中一个基准是检查page的checksum,也就是page的最后事务号,发生这种partial page write 的问题时,因为page已经损坏,所以就无法定位到page中的事务号,这个时候redo就无法直接恢复。
一部分是内存的doublewrite buffer,大小为2MB,另一部分是物理磁盘上共享空间中的连续128页,两个区,大小也为2MB。其中120个用于批量刷脏数据,另外8个用于Single Page Flush。根据阿里翟卫祥同学分析,之所以这样做是因为批量刷脏是后台线程做的,这样不影响前台线程。而Single Page Flush是用户线程发起的,需要尽快地刷脏并替换出一个空闲页出来。所以不是一个严格的64+64的拆分,最后也给出了这篇文章的链接。(https://yq.aliyun.com/articles/50627)
2MB的 Double Write 区,可以存储 2 * 1024 / 16 = 128 页
因此在操作系统将页写入磁盘的过程中发生了崩溃,innodb可以从共享表空间中的doublewrite中找到该页的副本,将其复制到表空间文件,再应用重做日志恢复
参数skip_innodb_doublewrite可以禁止使用两次写功能,这时可能会发生写失效问题,
有些文件系统本身就提供了部分写失效的防范机制,比如ZFS文件系统,因此用户就可以不必启用两次写功能。
如果操作系统在将页写入磁盘的过程中发生崩溃,在恢复过程中,innodb存储引擎可以从共享表空间的doublewrite中找到该页的一个最近的副本,将其复制到表空间文件,再应用redo log,就完成了恢复过程。
对于文件校验来说,一个中心词就是checksum。如果出现了partial write的时候,比如断电,那么两次写的过程中,很可能page是不一致的,这样checksum校验就很可能出现问题。而出现问题时,因为有了前期写入共享表空间的页信息,所以就可以重构出页的信息重新写入。
因为有副本所以也不担心表空间中数据页是否损坏。
但是,doublewrite buffer写入磁盘共享表空间这个过程是连续存储,是顺序写,性能非常高,(约占写的%10),牺牲一点写性能来保证数据页的完整还是很有必要的。
mysql> show global status like '%dblwr%';
++----------------------------+-------+
+| Variable_name | Value |
++----------------------------+-------+
+| Innodb_dblwr_pages_written | 7 |
+| Innodb_dblwr_writes | 3 |
++----------------------------+-------+
+2 rows in set (0.00 sec)
+
关注点:Innodb_dblwr_pages_written / Innodb_dblwr_writes
开启doublewrite后,每次脏页刷新必须要先写doublewrite,而doublewrite存在于磁盘上的是两个连续的区,每个区由连续的页组成,一般情况下一个区最多有**64个页**,所以一次IO写入应该可以最多写64个页。
而根据以上系统Innodb_dblwr_pages_written与Innodb_dblwr_writes的比例来看,大概在3左右,远远还没到64(如果约等于64,那么说明系统的写压力非常大,有大量的脏页要往磁盘上写),所以从这个角度也可以看出,系统写入压力并不高。
关闭double write适合的场景
海量DML
不惧怕数据损坏和丢失
系统写负载成为主要负载
mysql> show variables like '%double%';
++--------------------+-------+
+| Variable_name | Value |
++--------------------+-------+
+| innodb_doublewrite | ON |
++--------------------+-------+
+1 row in set (0.04 sec)
+
作为InnoDB的一个关键特性,doublewrite功能默认是开启的,但是在上述特殊的一些场景也可以视情况关闭,来提高数据库写性能。静态参数,配置文件修改,重启数据库。
还可参考这篇文章:页断裂(partial write)与doublewrite技术
https://www.modb.pro/db/114783
innodb存储引擎会监控对表上的各索引页的查询,如果观察到建立哈希索引可以带来速度提升,则建立哈希索引,称之为自适应哈希索引(Adaptive hash index,AHI).AHI是通过**缓冲池的B+树**构造而来,因此建立速度很快,innodb会自动根据访问的频率和模式自动的为某些热点页建立哈希索引。
哈希索引只能用来搜索等值的查询,如select * from table where index_col="xxx";而对于其他查找类型,如范围查找不能使用哈希索引,通过参数innodb_adptive_hash_index来禁止或者开启此特性,默认AHI为开启状态。
为了提高磁盘操作性能,使用异步io(asynchronous io,AIO)的方式来处理磁盘操作,
与AIO相对应的是Sync IO,即每次进行一次IO操作,需要等待此次操作结束后,才能进行下一步操作。
用户可以在发出一个io请求后立即再发出另一个io请求,当全部io请求发送完毕后,等待所有的io操作完成,称为AIO,AIO还可以进行io merge操作,也就是将多个io合并为一个io.这样可以提高IPOS性能。
例如,用户查询的页为
(3,5)(3,6)(3,7)
每个页的大小为16KB,那么,Sync IO需要三次IO 操作。而AIO会判断这三个页是连续的,(通过space,offset就可以看出来)因此,AIO会发出一个IO请求,从(3,5)开始,读取48KB的页
在innodb1.1.x之前,Aio是通过innodb存储引擎的代码模拟实现,而从innodb1.1.x开始,提供了内核级别AIO的支持,即为native AIO,
read ahead方式的读取,脏页的刷新等都是AIO完成的
当刷新一个脏页时,innodb会检测**该页所在区的所有页**,如果是脏页,那么一起刷新
通过AIO,将多个IO写入操作合并到一个IO操作中,对于机械硬盘来说,性能提升很明显。固态硬盘建议关闭
参数innodb_flush_neighbors来开启或关闭该特性,为0则关闭
InnoDB在I/O的优化上有个比较重要的特性为预读,预读请求是一个i/o请求,它会异步地在缓冲池中预先回迁多个页面,预计很快就会需要这些页面,这些请求在一个范围内引入所有页面。InnoDB以64个page为一个extent,那么InnoDB的预读是以page为单位还是以extent?
数据库请求数据的时候,会将读请求交给文件系统,放入请求队列中;相关进程从请求队列中将读请求取出,根据需求到相关数据区(内存、磁盘)读取数据;取出的数据,放入响应队列中,最后数据库就会从响应队列中将数据取走,完成一次数据读操作过程。
接着进程继续处理请求队列,(如果数据库是全表扫描的话,数据读请求将会占满请求队列),判断后面几个数据读请求的数据是否相邻,再根据自身系统IO带宽处理量,进行预读,进行读请求的合并处理,一次性读取多块数据放入响应队列中,再被数据库取走。(如此,一次物理读操作,实现多页数据读取,rrqm>0(# iostat -x),假设是4个读请求合并,则rrqm参数显示的就是4)
预读请求是一个I/O请求,用于异步预取缓冲池中的多个页面,以预期这些页面将很快被需要。请求在一个区段中引入所有页面。InnoDB使用两种预读算法来提高I/O性能:
线性预读(默认方式),它根据按顺序访问的缓冲池中的页面来预测可能很快需要哪些页面。通过使用配置参数innodb_read_ahead_threshold来调整触发异步读取请求所需的顺序页面访问次数,可以控制InnoDB何时执行预读操作。在添加这个参数之前,InnoDB只会在读取当前区段的最后一页时计算是否要对整个下一个区段发出异步预取请求。
配置参数innodb_read_ahead_threshold控制着InnoDB检测顺序页面访问模式的敏感度。如果从一个区段连续读取的页面数大于或等于innodb_read_ahead_threshold, InnoDB会对整个后续区段启动一个异步预读操作。innodb_read_ahead_threshold可以设置为0-64之间的任何值。默认值是56。值越高,访问模式检查越严格。例如,如果你将这个值设置为48,InnoDB只会在48个页面被顺序访问的情况下触发一个线性提前读取请求。如果值是8,InnoDB会触发异步提前读取,即使只有8个页面被顺序访问。您可以在MySQL配置文件中设置该参数的值,或者使用set GLOBAL语句动态地更改它,这需要足够的权限来设置全局系统变量。
mysql> show variables like 'innodb_read_ahead_threshold';
++-----------------------------+-------+
+| Variable_name | Value |
++-----------------------------+-------+
+| innodb_read_ahead_threshold | 56 |
++-----------------------------+-------+
+
mysql> show variables like 'innodb_random_read_ahead';
++--------------------------+-------+
+| Variable_name | Value |
++--------------------------+-------+
+| innodb_random_read_ahead | OFF |
++--------------------------+-------+
+
SHOW ENGINE INNODB STATUS
命令显示统计信息,帮助您评估预读算法的有效性。统计信息包括以下全局状态变量的计数器信息:
在微调innodb_random_read_ahead设置时,这些信息可能很有用。
以上内容来源于
传统的 DBMS 架构都属于 disk-oriented architecture,即假设数据主要存储在非易失的磁盘(non-volatile disk)上。
于是 DBMS 中一般都有磁盘管理模块(disk manager),它主要负责数据在非易失与易失(volatile)的存储器之间的移动。
这里需要理解两点:
从 InnoDB 逻辑存储结构来看,所有的数据都被逻辑的存放在一个空间中,这个空间就叫做表空间(tablespace)。
****表空间由 段(segment)、区(extent)、页(page)****组成。
当我们创建一个表之后,在磁盘上会有对应的表名称.ibd
的磁盘文件。
表空间的磁盘文件里面有很多的数据页,一个数据页最多 16kb,因为不可能一个数据页一个磁盘文件,所以数据区的概念引入了。
InnoDB 存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进管理,因此可以将其视为基于磁盘的数据库系统。
在数据库系统中,由于 CPU 和磁盘交换速度的差距,基于磁盘的数据库系统通常使用缓冲池技术来提高数据库的整体性能。
内存缓冲池简单说就是 MySQL 进程向操作系统申请一块内存区域,通过内存的速度来弥补磁盘的速度,在数据库中读取页时,首先将磁盘读到的页放到缓冲池中,这个过程称为将页 fix 到缓冲池。
buffer pool
一般称为内存缓冲池
,在 MySQL 也有人简称bp
。
下次再读取相关的页时,下次再读取相同的页时,先判断是否在缓冲池中,若在,则称为该页在缓冲池被命中。
对于修改数据(增删改),同样首先修改缓冲池中的页,然后在以一定的频率刷新到磁盘。通过一种被称作 checkpoint 的机制刷回磁盘。
在 MySQL 中,innodb 表以 tablename.ibd 格式的文件存放在磁盘中。
数据页是 MySQL 抽象出来的数据单位,磁盘文件中就是存放了很多数据页,每个数据页里存放了很多行数据。默认情况下,数据页的大小是 16KB。
数据库采用数据页的形式组织数据。MySQL 默认的非压缩数据页为 16KB。在 ibd 中间中,0-16KB 偏移量即为 0 号数据页,16KB-32KB 的为 1 号数据页,依次类推。
数据页的头尾除了一些元信息外,还有 Checksum 校验值,这些校验值在写入磁盘前计算得到,当从磁盘中读取时,重新计算校验值并与数据页中存储的对比,如果发现不同,则会导致 MySQL crash。遇到这种情况,往往需要从备份集中恢复数据,如果备份不可用,只能使用 innodb_force_recovery 强行启动,然后尽可能多的导出数据。
严格来讲,InnoDB 的数据页有很多种,比如,索引页,Undo 页,Inode 页,系统页,BloB 页等,一共有 10 多种。
对应的,在 Buffer Pool
中,也是以数据页为数据单位,存放着很多数据。
但是通常被叫做缓存页,因为 Buffer Pool
是一个缓冲池,并且里面的数据都是从磁盘文件中缓存到内存中。
所以,默认情况下缓存页的大小也是 16KB,因为它和磁盘文件中数据页是一一对应的。
缓冲池和磁盘之间的数据交换的单位是数据页,包括从磁盘中读取数据到缓冲池和缓冲池中数据刷回磁盘中。
缓冲池中基本概念
缓冲池是 MySQL 向操作系统申请的一块内存区域,操作系统是以页为单位对内存进行管理。
缓冲池是 InnoDB 存储引擎中最重要的组件。为了提高 MySQL 的并发性能,使用到的数据都会缓存在缓冲池中,然后所有的增删改查操作都将在缓冲池中执行。
对于每个更新请求,尽量就是 只更新内存,然后往磁盘顺序写日志文件。
更新内存的性能是极高的,然后顺序写磁盘上的日志文件的性能也是比较高的,因为顺序写磁盘文件,他的性能要远高于随机读写磁盘文件。
具体来看,缓冲池中的页类型有:
不能简单的认为,缓冲池只是缓冲索引和数据页。它们只是占内存缓冲池很大的一部分而已
在 Linux 中,操作系统以页为单位管理内存,无论是将磁盘中的数据加载到内存中,还是将内存中的数据写回磁盘,操作系统都会以页面为单位进行操作。 哪怕我们只向磁盘中写入一个字节的数据,我们也需要将整个页面中的全部数据刷入磁盘中。 在操作系统层面,每个进程都有自己独立的地址空间,看到的都是操作系统虚拟出来的地址空间,虚拟地址最终还是要落在实际内存的物理地址上进行操作的。 操作系统就会通过页表的机制来实现进程的虚拟地址到物理地址。其中每一页的大小都是固定的。
####X86: +[root@ens8 ~]# getconf PAGESIZE +4096 +####ARM: +root@ens8ARM:~# getconf PAGESIZE +65536 +
Linux 同时支持正常大小的内存页和大内存页(Huge Page)
绝大多数处理器上的内存页(page)的默认大小都是 4KB,虽然部分处理器会使用 8KB、16KB 或者 64KB 作为默认的页面大小,但是 4KB 的页面仍然是操作系统默认内存页配置的主流;
总结一下:
innodb_page_size 作为 innodb 和 OS 交互单位。文件系统对文件的 buffer IO,也是 page 为单位进行处理的。
InnoDB 缓冲池中的数据访问是以 Page 为单位的,每个 Page 的大小默认为 16KB,Buffer Pool 是用来管理和缓存这些 Page 的。
# 内存缓冲池总大小,默认是128M,应当适当设置调大buffer_pool_size,一般设置为服务器内存60%。通常实际占用的内存会比配置的还要大10%
+# MySQL5.7.5之后可以动态在线调整。在调整innodb_buffer_pool_size 期间,用户的请求将会阻塞,直到调整完毕,所以请勿在白天调整,尽量在凌晨3-4点业务低峰期调整。
+innodb_buffer_pool_size=8G
+
+# 内存缓冲池实例数,默认是1,通过将buffer pool分成多个区,每个区用独立的锁保护,这样就减少了访问buffer_pool时需要上锁的粒度,以提高并发能力和性能。
+innodb_buffer_pool_instances=16
+
+# innodb页大小,默认是16KB,一般设置为16KB或64KB
+innodb_page_size=16KB
+
+# 在调整内存缓冲池总大小时,内部把数据页移动到一个新的位置,单位是块。如果想增加移动的速度,需要调整innodb_buffer_pool_chunk_size参数的大小,默认是128M。缓冲池配置时的基本单位,以块的形式配置,innodb_buffer_pool_chunk_size参数指明块大小。
+# innodb_buffer_pool_size=innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances * n
+innodb_buffer_pool_chunk_size=128MB
+
LRU 就是一种很常见的缓存淘汰策略。按照英文的直接原义就是 Least Recently Used,最近最久未使用。
利用好 LRU 算法,我们能够提高对热点数据的缓存效率,进而提升缓存服务的内存使用率。
一般计算机内存容量有限,操作系统分配给 MySQL 的内存缓存池容量自然也有限,如果缓存池满了就要删除一些内容,给新内容腾位置。
但问题是,删除哪些内容呢?我们肯定希望删掉哪些没什么用的缓存,而把有用的数据继续留在缓存里,方便之后继续使用。
那么,什么样的数据,我们判定为「有用的」的数据呢?LRU 缓存淘汰算法就是一种常用策略。
LRU 的全称是 Least Recently Used,这个算法认为最近使用过的数据应该是是「有用的」,很久都没用过的数据应该是无用的,内存满了就优先删那些很久没用过的数据。
它是按照一个非常著名的计算机操作系统基础理论得来的:
最近使用的页面数据会在未来一段时期内仍然被使用,已经很久没有使用的页面很有可能在未来较长的一段时间内仍然不会被使用。
基于这个思想,会存在一种缓存淘汰机制,每次从内存中找到 最久未使用的数据然后置换出来 ,从而存入新的数据!
LRU 的主要衡量指标是 使用的时间 。附加指标是 使用的次数。
在计算机中大量使用了这个机制,它的合理性在于 优先筛选热点数据 ,所谓热点数据,就是 最近最多使用的数据 !
在一些大厂面试中,经常会要求手写 LRU 算法。
通常,数据库中的缓冲池是通过 LRU (Latest Recent Used) 算法来管理的,即最频繁使用的页在 LRU 最前端。
但是 MySQL InnoDB 对传统的 LRU 算法做了一些优化。在 buffer pool 中的的数据页可以认为是一个 LIST 列表,分为两个子列表 (New Sublist) ( Old Sublist)
# 这个参数控制着 New Sublist 和 Old Sublist 的比例 ,New Sublist占5/8,Old Sublist占3/8
+innodb_old_blocks_pct=37
+
可以简单理解为 New Sublist 中的页都是最活跃的热点数据页。
当有数据页要加载到内存中,就插入到 Old Sublist 的头部,并且从 Old Sublist 尾部移除不再使用的页。
可以看到,这是一个先进先出的队列。
很明显不用一个队列来管理这些,可以避免一次大表的全表扫描,就把缓冲池中的所有数据都刷出。
MySQL 默认在 InnoDB 缓冲池(而不是整个缓冲池)中仅保留最频繁访问页的 25% 。
在多数使用场景下,合理的选择是:保留最有用的数据页,比加载所有的页(很多页可能在后续的工作中并没有访问到)在缓冲池中要更快。
# INFORMATION_SCHEMA中有几个缓冲池表提供有关InnoDB缓冲池中页面的缓冲池状态信息和元数据。
+
+############ 查询INNODB_BUFFER_PAGE表可能会影响性能。 除非您了解性能影响并确定其可接受,否则请勿在生产系统上查询此表。 为避免影响生产系统的性能,请重现要调查的问题并在测试实例上查询缓冲池统计信息。
+
+mysql> SHOW TABLES FROM INFORMATION_SCHEMA LIKE 'INNODB_BUFFER%';
++-----------------------------------------------+
+| Tables_in_INFORMATION_SCHEMA (INNODB_BUFFER%) |
++-----------------------------------------------+
+| INNODB_BUFFER_PAGE_LRU |
+| INNODB_BUFFER_PAGE |
+| INNODB_BUFFER_POOL_STATS |
++-----------------------------------------------+
+
+# INNODB_BUFFER_PAGE:保存InnoDB缓冲池中每个页面的信息。
+
+# INNODB_BUFFER_PAGE_LRU:保存有关InnoDB缓冲池中页面的信息,特别是它们在LRU列表中的排序方式,确定哪些页面在缓冲池变满时从缓冲池中逐出。 INNODB_BUFFER_PAGE_LRU表与INNODB_BUFFER_PAGE表具有相同的列。
+# 但INNODB_BUFFER_PAGE_LRU表具有LRU_POSITION列而不是BLOCK_ID列。
+# INNODB_BUFFER_POOL_STATS:提供缓冲池状态信息。许多相同的信息由SHOW ENGINE INNODB STATUS输出提供,或者可以使用InnoDB缓冲池服务器状态变量获得。
+
InnoDB
可以使用访问的标准监视器输出, SHOW ENGINE INNODB STATUS
提供有关缓冲池操作的度量。
缓冲池度量标准位于BUFFER POOL AND MEMORY
“ InnoDB
标准监视器”输出中的部分,其输出类似于以下内容:
----------------------
+BUFFER POOL AND MEMORY
+----------------------
+Total large memory allocated 2198863872
+Dictionary memory allocated 776332
+Buffer pool size 131072
+Free buffers 124908
+Database pages 5720
+Old database pages 2071
+Modified db pages 910
+Pending reads 0
+Pending writes: LRU 0, flush list 0, single page 0
+Pages made young 4, not young 0
+0.10 youngs/s, 0.00 non-youngs/s
+Pages read 197, created 5523, written 5060
+0.00 reads/s, 190.89 creates/s, 244.94 writes/s
+Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not
+0 / 1000
+Pages read ahead 0.00/s, evicted without access 0.00/s, Random read
+ahead 0.00/s
+LRU len: 5720, unzip_LRU len: 0
+I/O sum[0]:cur[0], unzip sum[0]:cur[0]
+
下表描述了InnoDB
标准监视器报告的缓冲池度量标准
名称 | 描述 |
---|---|
Total memory allocated | 为缓冲池分配的总内存(以字节为单位) |
Dictionary memory allocated | 为InnoDB 数据字典分配的总内存,以字节为单位 |
Buffer pool size | 分配给缓冲池的页面总大小 |
Free buffers | 缓冲池空闲列表的页面总大小 |
Database pages | 缓冲池 LRU 列表的页面总大小 |
Old database pages | 缓冲池旧 LRU 子列表的页面总大小 |
Modified db pages | 缓冲池中当前修改的页面数 |
Pending reads | 等待读入缓冲池的缓冲池页面数 |
Pending writes LRU | 从 LRU 列表的底部开始写入的缓冲池中的旧脏页数 |
Pending writes flush list | 检查点期间要刷新的缓冲池页面数 |
Pending writes single page | 缓冲池中暂挂的独立页面写入数 |
Pages made young | 缓冲池 LRU 列表中变年轻的页面总数(已移至“ new ”页面的子列表的开头) |
Pages made not young | 缓冲池 LRU 列表中没有年轻的页面总数(保留在“ old ”子列表中但没有年轻的页面) |
youngs/s | 每秒平均访问缓冲池 LRU 列表中的旧页面所导致的页面年轻 |
non-youngs/s | 每秒平均访问缓冲池 LRU 列表中的旧页面导致的页面不年轻 |
Pages read | 从缓冲池读取的页面总数 |
Pages created | 在缓冲池中创建的页面总数 |
Pages written | 从缓冲池写入的页面总数 |
reads/s | 每秒平均每秒读取的缓冲池页面数 |
creates/s | 每秒平均创建的缓冲池页面的每秒数量 |
writes/s | 每秒平均缓冲池页面写入数 |
Buffer pool hit rate | 从缓冲池内存与磁盘存储读取的页面的缓冲池页面命中率 |
young-making rate | 页面访问的平均命中率使页面更年轻 |
not (young-making rate) | 页面访问未使页面变年轻的平均命中率 |
Pages read ahead | 预读操作的每秒平均数 |
Pages evicted without access | 每秒从缓冲池访问而未访问的页面的平均值 |
Random read ahead | 随机预读操作的每秒平均数 |
LRU len | 缓冲池 LRU 列表的页面总大小 |
unzip_LRU len | 缓冲池 unzip_LRU 列表的页面总大小 |
I/O sum | 最近 50 秒内访问的缓冲池 LRU 列表页面的总数 |
I/O cur | 已访问的缓冲池 LRU 列表页面的总数 |
I/O unzip sum | 已访问的缓冲池 unzip_LRU 列表页面的总数 |
I/O unzip cur | 已访问的缓冲池 unzip_LRU 列表页面的总数 |
https://dev.mysql.com/doc/refman/8.0/en/innodb-preload-buffer-pool.html
在生产中,重启 MySQL 后,会发现一段时间内 SQL 性能变差,然后最终恢复到原有性能。
MySQL 进程重启后,它在内存中的数据自然就释放了。通过业务的访问才会逐步将热点数据从磁盘缓存到 InnoDB Buffer Pool 中,从磁盘读取数据自然没有从内存读取数据快。随着业务的访问,MySQL 会逐步经常操作的热点数据都已经缓存到 InnoDB Buffer Pool 缓冲池中。最后趋于原有情况。
MySQL 重启后,将热点数据从磁盘逐渐缓存到 InnoDB Buffer Pool
的过程称为预热(官方文档称之为 warmup)。
让应用系统自身慢慢通过 SQL 给InnoDB Buffer Pool
预热成本很高,如果遇到高峰期极有可能带来一场性能灾难,导致业务卡顿不能顺利运营。
为了避免这种情况发生,MySQL 5.6 引入了数据预热机制,在停止数据库的时候,把内存中的热点数据 dump 到磁盘文件中,启动时,直接把热点数据从磁盘加载回内存中。
需要注意的是,对于较大内存的数据库来说,配置这种预热机制,会让关闭数据库的时间非常长。同样启动过程也会延长。
# 关闭数据库时是否保留当前的缓冲池的状态到磁盘中,MySQL5.7之后默认开启
+innodb_buffer_pool_dump_at_shutdown=on
+
+# 保留内存缓冲池中数据的比例,默认是25%
+innodb_buffer_pool_dump_pct=25
+
+# 缓冲池数据dump到磁盘中的文件名称,默认是ib_buffer_pool,一般放在磁盘的datadir/ib_buffer_pool中
+innodb_buffer_pool_filename=ib_buffer_pool
+
+# 预热开关:启动时自动从磁盘文件读取热点数据到内存的innodb_buffer_pool中
+innodb_buffer_pool_load_at_startup=on
+
脏页
当修改数据的事务提交后,数据刷到磁盘之前,此时内存中的数据页和磁盘中的数据是不一致的,我们把此时内存中的这些数据页成为脏页。
刷脏
刷脏,即把 buffer pool 中的内存脏页数据刷回磁盘落地。
InnoDB 会在后台执行某些任务,包括从缓冲池刷新脏页(那些已更改但尚未写入数据库文件的页)。
InnoDB 当缓冲池中脏页的百分比达到定义的低水位设置时,其实就是当缓冲池中的脏页占用比达到innodb_max_dirty_pages_pct_lwm
的设定值的时候,就会自动将脏页清出 buffer pool,这是为了保证buffer pool
当中脏页的占有率,也是为了防止脏页占有率超过innodb_max_dirty_pages_pct
的设定值,当脏页的占有率达到了 innodb_max_dirty_pages_pct 的设定值的时候,InnoDB 就会强制刷新 buffer pool pages。
InnoDB 采用一种基于 redo log 的最近生成量和最近刷新频率的算法来决定冲洗速度,这样的算法可以保证数据库的冲洗不会影响到数据库的性能,也能保证数据库 buffer pool 中的数据的脏数据的占用比。这种自动调整刷新速率有助于避免过多的缓冲池刷新限制了普通读写请求可用的 I/O 容量,从而避免吞吐量突然下降,但还是对正常 IO 有影响。
5.6 版本以前,脏页的清理工作交由 master thread 的;
Page cleaner thread 是 5.6.2 引入的一个新线程(单线程),从 master 线程中卸下 buffer pool 刷脏页的工作独立出来的线程(默认是启一个线程);
5.7 开始支持多线程刷脏页,线程数量通过innodb_page_cleaners
参数控制,默认是4
个,如果数量超过 buffer pool instance,那么会降级成跟它一致。
刷脏主要分以下几种场景:
1、数据库关闭,
内部 XA 事务主要指单节点实例内部,一个事务跨多个存储引擎进行读写,那么就会产生内部 XA 事务;这里需要指出的是,MySQL 内部每个事务都需要写 binlog,并且需要保证 binlog 与引擎修改的一致性,因此 binlog 是一个特殊的参与者,所以在打开 binlog 的情况下,即使事务修改只涉及一个引擎,内部也会启动 XA 事务。
外部 XA 事务与内部 XA 事务核心逻辑类似,提供给用户一套 XA 事务的操作命令,包括 XA start, XA end,XA prepre 和 XA commit 等,可以支持跨多个节点的 XA 事务。外部 XA 的协调者是用户的应用,参与者是 MySQL 节点,因此需要应用持久化协调信息,解决事务一致性问题。无论外部 XA 事务还是内部 XA 事务,存储引擎实现的 prepare 和 commit 接口都是同一条路径。
对于用户事务,是否使用二阶段提交,取决于是否开启了 binlog。
因为 MySQL 把 binlog 也看作一个存储引擎,开启 binlog,SQL 语句改变(插入、更新、删除)InnoDB 表的数据,这个 SQL 语句执行过程中,就涉及到两个存储引擎。
在 MySQL 中,两阶段提交有几个最重要的文件:
重做日志,又叫事务日志,是 InnoDB 存储引擎层的日志。一般也叫**物理日志**
在 MySQL 里,如果每一次的更新操作都需要写进磁盘持久化,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很高。
当缓冲池中的某页数据被修改后,该页就被标记为 ”脏页“,脏页的数据会被定期刷新到磁盘上。
倘若每次一个页发生变化,就将新页的版本刷新到磁盘,那么这个开销是非常大的。并且,如果热点数据都集中在某几个页中,那么数据库的性能将变得非常差。另外,如果在从缓冲池将页的新版本刷新到磁盘时发生了宕机,那么这个数据就不能恢复了。
所以,为了避免发生数据丢失的问题,当前事务数据库系统(并非 MySQL 所独有)普遍都采用了 WAL(Write Ahead Log,预写日志)策略:即当事务提交时,先写重做日志(redo log),再修改页(先修改缓冲池,再刷新到磁盘);当由于发生宕机而导致数据丢失时,通过 redo log 来完成数据的恢复。这也是事务 ACID 中 D(Durability 持久性)的要求。
有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。
举个简单的例子,假设你非常热心且 rich 的,借出去了很多钱,但是你非常 old school,不会使用电子设备并且记性不太好,所以你用一个小本本记下了所有欠你钱的人的名字和具体金额。这样,别人还你钱的时候,你就翻出你的小本本,一页页地找到他的名字然后把这次还的钱扣除掉。
但是呢,其实你平常是非常忙碌的,没办法随时随地翻小本本做记录,因此你就想出了一个主意:每当有人还你钱的时候,你就在一张白纸上记下来,然后挑个时间对照小本本把白纸上的账目都给清了。
这就是 WAL。白纸就是 redo log,小本本就是磁盘。
所以有一种技术叫 WAL ,全称是 Write-Ahead Logging,它的关键点就是先写日志,再写磁盘。(日志先行)
PostgreSQL 文档中有这么一句话 Write-Ahead Logging (WAL) is a standard method for ensuring data integrity
通过日志实现事务的原子性和持久性是当今的主流方案。
在磁盘中写入数据就不能像程序修改内存中变量值那样,直接改变某表某行某列的某个值,而是必须将修改数据这个操作所需的全部信息,包括修改什么数据、数据物理上位于哪个内存页和磁盘块中、从什么值改成什么值,等等,以日志的形式——即仅进行顺序追加的文件写入的形式(这是最高效的写入方式)先记录到磁盘中。只有在日志记录全部都安全落盘,数据库在日志中看到代表事务成功提交的“提交记录”(Commit Record)后,才会根据日志上的信息对真正的数据进行修改,修改完成后,再在日志中加入一条“结束记录”(End Record)表示事务已完成持久化,这种事务实现方法被称为“Commit Logging”(提交日志)。
WAL 的核心概念是数据文件(表和索引所在的位置)的更改必须仅在记录这些更改之后写入,即在描述更改的日志记录已刷新到永久存储之后。
如果我们遵循这个过程,我们不需要在每次事务提交时将数据页真正刷新到磁盘,因为我们知道在发生崩溃时我们将能够使用日志恢复数据库;
任何尚未应用的更改可以从日志记录中重做数据页。(这是前滚恢复,也称为 redo)
具体来说,当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log (磁盘中的物理文件)里面,并更新内存,这个时候更新就几乎算完成了。
(后面会讲,这时严格说还不算事务 commit 成功,客户端还看不到返回成功)
由于 redo-log 是顺序写的,所以速度比较快。redo-log 是物理日志,记录的是 “在某个数据页上做了什么修改”。
redo-log 是循环写的,当 redo-log 写完后,就要刷盘。把数据刷到磁盘中。(更严格地说,何时刷盘应该还是有参数控制的。)
由于内存缓冲的存在,对数据的增删改都先修改内存中的数据页,再定期 flush 落盘持久化。
在每次事务提交的时候,将该事务涉及修改的数据页全部刷回到磁盘中。但是这么做会有严重的性能问题,主要体现在两个方面:
因此 MySQL 设计了 redo log
。具体来说就是只记录事务对数据页做了哪些修改,这样就能完美地解决性能问题了(相对而言文件更小,并且是顺序 IO)。
redo log 包括两部分:
MySQL 每执行一条 DML 语句,先将记录写入 redo log buffer ,后续某个时间点再一次性将多个操作记录写到 redo log file 。
默认情况下,redo log 在磁盘上由名为 ib_logfile0
和 ib_logfile1
的两个物理文件。
### redolog 关键参数
+
+# redo-log 默认是在 datadir 目录下,名为 `ib_logfile1` 和 `ib_logfile2` 这样的两个文件。
+# 指定redolog的存放目录,默认是"./",即在datadir目录下,条件允许的话,一般不建议放在跟datadir同一块磁盘下,防止IO争用
+# 注意这个目录要提前创建好,并设置好正确的权限
+innodb_log_group_home_dir=/data/mysql_redo_log/
+
+# 单个redolog文件的大小,默认是48MB,最大值为512G,注意最大值指的所有redo-log文件之和
+# 如果数据库单个事务较大的话,redolog应该尽量设置的稍微大点
+innodb_log_file_size=48MB
+
+# regolog是以一组文件的形式出现。这个参数了指定了一组里面有多少个redo log文件
+innodb_log_files_in_group=2 # 默认值是2
+# regolog文件的总大小就是等于 innodb_log_file_size*innodb_log_files_in_group
+
+# redo_log_buffer 大小,默认为16M。延迟事务日志写入磁盘,把 redo log 放到该缓冲区
+# 然后根据 innodb_flush_log_at_trx_commit 参数的设置,再把日志从buffer中flush到磁盘
+# innodb_log_buffer_size是会话级的,所有整个redolog buffer占用的空间应该是innodb_log_buffer_size * connections
+# 一般默认值16MB是够用的,但如果事务之中含有blog/text等大字段,这个缓冲区会被很快填满会引起额外的IO负载。对于大事务操作。可以考虑设的大一些。
+innodb_log_buffer_size=16M
+
+## 修改redo_log文件大小必须要先关闭实例后再修改。
+## MySQL 8.0.30 版本中提供动态调整redo log 文件大小的功能。自此,可以在线调整无需重启。
+
+innodb_flush_log_at_trx_commit=1
+# innodb_flush_log_at_trx_commit 控制 redolog 从 redolog buffer刷新到磁盘的策略,具体含义如下:
+
+# 默认为1。值为1,每次 commit 都会把 redo log 从 redo log buffer 写入到 system ,并fsync刷新到磁盘文件中。
+
+# 值为2时,表示每次事务提交时 MySQL 会把日志从 redo log buffer 写入到 system ,但只写入到 file system buffer,由系统内部来 fsync 到磁盘文件。如果数据库实例 crash ,不会丢失 redo log,但是如果服务器 crash,由于 file system buffer 还来不及 fsync 到磁盘文件,所以会丢失这一部分的数据。
+
+# 值为0,表示事务提交时不进行写入redo log操作,这个操作仅在 master thread 中完成,而在 master thread 中每1秒进行一次重做日志的 fsync 操作,因此实例 crash 最多丢失1秒钟内的事务。
+
+
+# 这个参数是innodb的数据页大小单位,一般设置为
+innodb_page_size=16KB
+
+
+# https://blog.csdn.net/u010647035/article/details/104733939
+
+
+
+## MySQL8.0.30 版本中提供在线动态调整redo log文件大小的功能。自此,可以直接在线调整且无需重启
+## 自8.0.30后,redo文件总大小参数也变成了由 innodb_redo_log_capacity 参数直接控制,默认是100MB。
+## 一旦设置了这个参数,前面的组数量和大小都被忽略。
+## redo文件也存放在datadir的#innodb_redo目录中。分为两类:ordinary(已用过的,不带_tmp 后缀的) 和 spare(空闲的,带tmp后缀的) 。
+## 一般会将redo文件平均分成32份文件。以 #ib_redoN 文件命名,N为序号
+
+# 在线调整redo为8GB
+SET GLOBAL innodb_redo_log_capacity = 8589934592;
+
+# 在线直接调整redo为2GB
+set persist innodb_redo_log_capacity=2*1024*1024*1024;
+
+
+# 新增对应的状态变量innodb_redo_log_capacity_resized,方便在 MySQL 侧监控当前 REDO 日志文件大小
+show status like 'innodb_redo_log_capacity_resized';
+
+# 同时 performance_schema 库里新增表innodb_redo_log_files:获取当前使用的 REDO 日志文件 LSN 区间、实际写入大小、是否已满等统计数据。例如当前15个 REDO 日志文件的统计数据如下:一目了然!
+
在 MySQL 8.0.21 新版本发布中,支持了一个新特性 Redo Logging 动态开关。
借助这个功能,在新实例导数据的场景下,事务处理可以跳过记录 redolog 和 doublewrite buffer,从而加快数据的导入速度。
同时,付出的代价是短时间牺牲了数据库的 ACID 保障。所以主要使用场景就是向一个新实例导入数据。
注意事项
新增内容
ALTER INSTANCE {ENABLE | DISABLE} INNODB REDO_LOG
。用法
--先赋权
+GRANT INNODB_REDO_LOG_ENABLE ON *.* to 'data_load_admin';
+--然后关闭redo_log
+ALTER INSTANCE DISABLE INNODB REDO_LOG;
+--确认是否关闭成功
+SHOW GLOBAL STATUS LIKE 'Innodb_redo_log_enabled';
+
+--开始往新实例导入数据
+
+--重新开启redo_log
+ALTER INSTANCE ENABLE INNODB REDO_LOG;
+--确认是否开启成功
+SHOW GLOBAL STATUS LIKE 'Innodb_redo_log_enabled';
+
热备的原理都是要备份 redolog,由于 redolog 是循环写的。
如果备份期间还是有大量的事务写入,备份速度跟不上 redo log 生成的速度,结果导致 redo log 被覆盖了,然后备份就无法保证一致性。导致备份失败。
MySQL8.0.17 中引入了 redo log 的归档功能,如果我们开启归档功能,redo log 会持续不断的生成,而不会覆盖掉之前的 redo log。
试想这样一种情况,在对一个高并发的数据库进行热备份的时候,备份速度很慢而 redo log 生成的速度很快,备份的速度跟不上 redo log 的生成速度,导致 redo log 被覆盖了,此时备份的一致性就无法得到保证了。
有了 redo log 的归档功能,就可以在备份启动的时候同步启动 redo log 归档,而在备份结束的时候同步停止 redo log 归档,这样就可以避免这个备份的问题了。
备份结束之后,依旧可以利用这个期间产生的 redo log 进行数据恢复。
想要启用 redo log 归档功能,只需设置 innodb_redo_log_archive_dirs 选项即可,该选项可支持在线动态修改,例如:
https://blog.csdn.net/qq_35246620/article/details/79345359
binlog 是 MySQL 的 server 层很重要的一个文件,它的主要作用如下:
binlog 是逻辑上的日志,
# 相关参数
+
+# binlog_cache是session级别的,也就是说实际binlog cache占用内存数= connections * binlog_cache。
+binlog_cache_size # 默认值是32k,写binlog之前,会先写binlog_cache
+
+# 一般设置为row
+binlog_format
+
+# binlog是否加密,默认不加密
+binlog_encryption=off
+
+# binlog过期时间,默认是30天
+binlog_expire_logs_seconds=2592000
+
redo-log 和 binlog 是 两阶段提交的重点,
当**数据恢复(指事务已经提交成功,但是数据还没有刷回磁盘时重启的这种情况)**时:
所有已经 prepared 但是没有 commit 的事务则会通过 undo log 做回滚
开启 binlog 时,两阶段的流程:
InnoDB 的事务 Prepare 阶段,即 SQL 已经成功执行并生成 redo 和 undo 的内存日志;(写 redo_log_buffer)
Prepare 阶段:写 redo-log , 此时 redo log 处于 prepare 状态。注意这里可能只是写 innodb_log_buffer (这是内存中的重做日志缓冲区)
Commit 阶段:innodb 释放锁(释放锁住的资源),释放回滚段,设置提交状态,binlog 持久化到磁盘,然后存储引擎层提交
当关闭 binlog 时,业务你根本不需要 binlog 带给你的特性(比如数据备份恢复、搭建 MySQL 主从集群),那你根本就用不着让 MySQL 写 binlog,也用不着什么两阶段提交。
只用一个 redolog 就够了。无论你的数据库如何 crash,redolog 中记录的内容总能让你 MySQL 内存中的数据恢复成 crash 之前的状态。
所以说,两阶段提交的主要用意是:为了保证 redolog 和 binlog 数据的安全一致性。只有在这两个日志文件逻辑上高度一致了。
你才能放心地使用 redolog 帮你将数据库中的状态恢复成 crash 之前的状态,使用 binlog 实现数据备份、恢复、以及主从复制。
而两阶段提交的机制可以保证这两个日志文件的逻辑是高度一致的。没有错误、没有冲突。
undolog 是 mysql 中比较重要的事务日志之一,顾名思义,undolog 是一种用于撤销回退的日志,在事务没提交之前,MySQL 会先记录更新前的数据到 undolog 日志文件里面,当事务回滚时或者数据库崩溃时,可以利用 undolog 来进行回退。
为了方便事务回滚,数据库在修改数据时也必须记录数据的旧值,这就是 Undo Log。正因为有了 Redo Log,对于已经提交的事务的数据一定存在于磁盘,所以保证了数据库系统的持久性;而有了 Undo Log,数据库可以保证在故障恢复时对没有完成的事务进行回滚,也就保证的事务的原子性。
默认情况下初始化后,会创建两个 undo 表空间innodb_undo_001
和innodb_undo_002
。对应到磁盘就是innodb_undo_directory
下的两个文件undo_001
和undo_002
,如果没有设置该参数,默认就是在datadir
目录下。
MySQL 实例最多支持 127 个 undo 表空间,其中包括 MySQL 实例初始化时创建的两个默认 undo 表空间。undo 表空间的数量由 innodb_undo_tablespaces 控制。默认值 0,最大值 95
ALTER UNDO TABLESPACE tablespace_name SET INACTIVE;
+
+DROP UNDO TABLESPACE tablespace_name;
+
+SELECT NAME, STATE FROM INFORMATION_SCHEMA.INNODB_TABLESPACES
+ WHERE NAME LIKE 'tablespace_name';
+
+
+SELECT TABLESPACE_NAME, FILE_NAME FROM INFORMATION_SCHEMA.FILES WHERE FILE_TYPE LIKE 'UNDO LOG';
+
+SHOW STATUS LIKE 'Innodb_undo_tablespaces%';
+
undo 表空间文件的初始大小取决于innodb_page_size
值。对于默认的 16k InnoDB 页面大小,初始 undo 表空间文件大小为 10MiB。对于 4k,8k,32k 和 64k 页面大小,初始 undo 表空间文件大小分别为 7MiB,8MiB,20MiB 和 40MiB。
在设计数据库时,我们假设数据库可能在任何时刻,由于如硬件故障,软件 Bug,运维操作等原因突然崩溃。这个时候尚未完成提交的事务可能已经有部分数据写入了磁盘,如果不加处理,会违反数据库对 Atomic 的保证,也就是任何事务的修改要么全部提交,要么全部取消。针对这个问题,直观的想法是等到事务真正提交时,才能允许这个事务的任何修改落盘,也就是 No-Steal 策略。显而易见,这种做法一方面造成很大的内存空间压力,另一方面提交时的大量随机 IO 会极大的影响性能。因此,数据库实现中通常会在正常事务进行中,就不断的连续写入 Undo Log,来记录本次修改之前的历史值。当 Crash 真正发生时,可以在 Recovery 过程中通过回放 Undo Log 将未提交事务的修改抹掉。InnoDB 采用的就是这种方式。
-- 如果业务上有跑批量或者大表的DML操作时,引起大事物,或针对多张大表关联更新时间较长,可能短时间内会将undo"撑大",oracle 我们可以通过创建一个新的 undo,通过在线的替换的方式,将膨胀的 undo 使用 drop 删除以释放空间。
+-- MySQL8.0同样可以使用这种方式来处理,因大事物或长事物引起的undo过大占用空间较多的情况。
+
+-- 添加新的undo文件undo003。mysql8.0中默认innodb_undo_tablespace为2个,不足2个时,不允许设置为inactive,且默认创建的undo受保护,不允许删除。
+-- 将膨胀的 undo 临时设置为inactive,以及 innodb_undo_log_truncate=on,自动 truncate 释放膨胀的undo空间。
+-- 重新将释放空间之后的undo设置为active,可重新上线使用。
+
+-- 在undo遇到大事务并持续增长的情况下,通过新增临时undo,手动释放系统默认的2个undo表空间大小。当然,截断 UNDO 表空间文件对数据库性能有一定影响,尽量在相对空闲时间进行。
+-- 当UNDO表空间被截断时,UNDO表空间中的回滚段将被停用。其他UNDO表空间中的活动回滚段负责整个系统负载,这可能会导致性能略有下降。性能受影响的程度取决于诸多因素
+
+-- 因此,避免潜在性能影响的最简单方法:
+-- 1、就是通过create undo tablespace undo_XXX add datafile ‘/path/undo_xxx.ibu’;多添加几个UNDO表空间;
+-- 2、如果条件允许,磁盘上采用高性能的SSD来存储数据,存储REDO、UNDO等。
+-- 引起UNDO过度膨胀的原因大多是因为基础数据量大,业务并发高,表关联操作较频繁,出现大且长的事物操作,导致UNDO一直处于active状态,不能及时释放回滚段等。
+-- 大事务引起的问题由来已久,即使我们能规避99%的大事物,但实际业务遇到那1%的大事物刚性需求发过来时,还要MySQL各种场景、各种架构和业务层好好磨合。
+
undo 两大作用:
实现事务回滚,保障事务的原子性。事务处理过程中,如果出现了错误或者用户执 行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。
实现 MVCC(多版本并发控制)关键因素之一。MVCC 是通过 ReadView + undo log 实现的。undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录。
操作系统使用 缓存 来填补内存和磁盘访问的差距,对磁盘文件的写入会先写入到页面缓存中。
在一些 GNU/Linux 和 UNIX 中,使用 **Unix fsync() 系统调用**来把数据刷到磁盘(InnoDB 默认使用 *fsyn*c 这个)
数据库在事务提交过程中调用 fsync 将数据持久化到磁盘,才满足**ACID**中的**D(持久化)**
fsync 是昂贵的操作,对于普通磁盘,每秒能完成几百次 fsync
MySQL 中使用了两阶段提交协议,为了满足 D(持久化) ,一次事务提交最多会导致 3 次 fsync
提交的事务在存储引擎内部(redo log)中准备好,一次 fsync;(写 redo-log 到磁盘:/datadit/ib_logfile0)
事务写入到 binlog 中并刷盘持久化,一次 fsync;(写 binlog 到磁盘)
事务在存储引擎内部提交,一次 fsync(写数据文件到磁盘,可以省略,存储引擎准备好的事务可以通过 binlog 来恢复)
虽然上面说的这些 redo log 的刷盘可以通过 innodb_flush_log_at_trx_commit ,binlog 刷盘通过 sync_binlog 参数来控制。
但是 binlog 和 redo log 的刷盘还是会成为最大的开销。通过组提交,将多个事务的 binlog,最大化每次刷盘的收益,弱化磁盘瓶颈,提高性能。
组提交(group commit):
如果多个事务,能在同一时间内并发提交成功,那么就说明这几个事务是不冲突的,逻辑上可以认为是一组事务,在从库上可以并发 replay。
基于 Commit_Order 的并行复制是在主数据库实例事务提交时,写入一些额外信息,从而在从机回放时,可以根据这些信息判断是否可以进行并行的回放。
同一组提交的事务之间是不冲突的,因此可以并行回放。
组提交,就好像我们平时渡船时,一般要等到人坐满后,一次性开船。
组提交将事务分为三个阶段(Flush 阶段、Sync 阶段、Commit 阶段)
每个阶段都会维护一个队列。
Flush 阶段:
将 binlog 数据写入文件,当然此时只是写入文件系统的内存缓冲,并不能保证数据库崩溃时 binlog 不丢失。
Flush 阶段队列的作用是提供了 Redo log 的组提交。
如果在这一步完成后数据库崩溃,由于协调者 binlog 中不保证有该组事务的记录,所以 MySQL 可能会在重启后回滚该组事务
参考
创建 InnoDB 表,使用如下语句:
CREATE TABLE t1 (a INT, b CHAR (20), PRIMARY KEY (a)) ENGINE=InnoDB;
+
InnoDB 一般都是默认存储引擎,也可以不用指定 ENGINE。使用如下语句查询 InnoDB 是否默认存储引擎。
SELECT @@default_storage_engine;
+
一个 InnoDB 表和索引,可以在 system tablespace, file-per-table tablespace, ogeneral tablespace 中创建。
默认地,InnoDB 的表都是**独立表空间**。
当 innodb_file_per_table 是 enabled 状态,它是默认的,一个 InnoDB 表被显式创建在单独的表空间中。
如果是 disabled 状态,会创建在系统表空间,如果要用通用表空间,那么使用 CREATE TABLE ... TABLESPACE 语法来创建表。
强烈建议为每个 innodb 表设立主键。
有时候,可能需要创建外部表(即在 datadir 外部创建表),可能是由于空间管理,IO 优化等原因。
InnoDB 支持外部表的语法:
-- 第一种情况,使用 DATA DIRECTORY 子句
+CREATE TABLE t1 (c1 INT PRIMARY KEY) DATA DIRECTORY = '/external/directory';
+
表空间传输特性
前提:
innodb_file_per_table
变量必须开启,默认就是开启的。DISCARD TABLESPACE
语句之前必须先关掉https://blog.k4nz.com/7bbf69045e0da119a1a892e054c6d145/
在 MySQL 中,InnoDB 的异步 IO 主要是用来处理预读以及对数据文件的写请求的。而对于正常的页面数据读取则是通过同步 IO 进行的。
这个是由 innodb_use_native_aio
参数控制的。它适用于 Linux 操作系统,并且默认启用。操作系统需要 libaio
库。
InnoDB 在 I/O 的优化上有个比较重要的特性为预读,预读请求是一个 i/o 请求,它会异步地将多个页面预读入缓冲池,预计很快就会需要这些页面,这些请求在一个范围内引入所有页面。
程序是有空间局部性的,靠近当前被访问数据的数据,在未来很大概率会被访问到。
所以,MySQL 在加载数据页时,会提前把它相邻的数据页一并加载进来,目的是为了减少磁盘 IO。
但是可能这些被提前加载进来的数据页,并没有被访问,相当于这个预读是白做了,这个就是预读失效。
如果使用简单的 LRU 算法,就会把预读页放到 LRU 链表头部,而当 Buffer Pool 空间不够的时候,还需要把末尾的页淘汰掉。
如果这些预读页如果一直不会被访问到,就会出现一个很奇怪的问题,不会被访问的预读页却占用了 LRU 链表前排的位置,而末尾淘汰的页,可能是频繁访问的页,这样就大大降低了缓存命中率。
数据库请求数据的时候,会将读请求交给文件系统,放入请求队列中;相关进程从请求队列中将读请求取出,根据需求到相关数据区(内存、磁盘)读取数据;取出的数据,放入响应队列中,最后数据库就会从响应队列中将数据取走,完成一次数据读操作过程。
接着进程继续处理请求队列,(如果数据库是全表扫描的话,数据读请求将会占满请求队列),判断后面几个数据读请求的数据是否相邻,再根据自身系统 IO 带宽处理量,进行预读,进行读请求的合并处理,一次性读取多块数据放入响应队列中,再被数据库取走。(如此,一次物理读操作,实现多页数据读取,rrqm>0(# iostat -x),假设是 4 个读请求合并,则 rrqm 参数显示的就是 4)
InnoDB 使用两种预读算法来提高 I/O 性能:线性预读(linear read-ahead)和随机预读(randomread-ahead)
为了区分这两种预读的方式,我们可以把线性预读放到以 extent 为单位,而随机预读放到以 extent 中的 page 为单位。线性预读着眼于将下一个 extent 提前读取到 buffer pool 中,而随机预读着眼于将当前 extent 中的剩余的 page 提前读取到 buffer pool 中。
线性预读(linear read-ahead):它可以根据顺序访问缓冲池中的页面,预测哪些页面可能需要很快。通过使用配置参数 innodb_read_ahead_threshold,通过调整触发异步读取请求所需的顺序页访问数,可以控制 Innodb 执行提前读操作的时间。在添加此参数之前,InnoDB 只会计算当在当前范围的最后一页中读取整个下一个区段时是否发出异步预取请求。
线性预读方式有一个很重要的变量控制是否将下一个 extent 预读到 buffer pool 中,通过使用配置参数 innodb_read_ahead_threshold,可以控制 Innodb 执行预读操作的时间。如果一个 extent 中的被顺序读取的 page 超过或者等于该参数变量时,Innodb 将会异步的将下一个 extent 读取到 buffer pool 中,innodb_read_ahead_threshold 可以设置为 0-64 的任何值,默认值为 56,值越高,访问模式检查越严格。
InnoDB 表空间(Tablespace)可以看做一个逻辑概念,InnoDB 把数据保存在表空间,本质上是一个或多个磁盘文件组成的虚拟文件系统。
系统表空间是 change buffer 的存放区域。如果没有启用独立表空间,也会在其中存放业务表数据和索引数据。
在老版本 MySQL,InnoDB 数据字典也存放在系统表空间中。doublewrite buufer 也存放在系统表空间中,在 8.0.20 之后,doublewrite buufer 也存在单独的文件中了。
系统表空间可以由多个文件组成,默认是一个,名为 ibdata1,默认放在 datadir 目录下面。
由如下参数控制
# 文件路径,默认是 datadir 下,也可以自定义路径
+innodb_data_home_dir=/myibdata/
+
+# 参数语法:文件名:文件初始大小(初始大小不低于12M):自增长属性:最大属性
+innodb_data_file_path=file_name:file_size[:autoextend[:max:max_file_size]]
+
+# 每次自动扩展的增量大小,由innodb_autoextend_increment控制,单位为M,默认是64M
+innodb_autoextend_increment=64
+
+# 默认值如下:ibdata1:12M:自增长
+innodb_data_file_path=ibdata1:12M:autoextend
+
+# 也可以一次定义两个系统表空间文件
+innodb_data_file_path=ibdata1:50M;ibdata2:50M:autoextend
+
也可以手动调整增长 ibdata 文件
# 先关停数据库
+
+# 如果ibdata设置了自增长属性,删掉它
+
MySQL 5.6.6 之前的版本,InnoDB 默认会将所有的数据库 InnoDB 引擎的表数据存储在一个共享空间中:ibdata1,这样就会让管理感觉很难受,增删数据库的时候,ibdata1 文件不会自动收缩。
单个数据库的备份也将成为问题。通常只能将数据使用 mysqldump 导出,然后再导入解决这个问题。
在之后的版本,为了优化上述问题,独立表空间 innodb_file_per_table 参数默认开启
mysql> show variables like 'innodb_file_per_table';
++-----------------------+-------+
+| Variable_name | Value |
++-----------------------+-------+
+| innodb_file_per_table | ON |
++-----------------------+-------+
+1 row in set, 1 warning (0.05 sec)
+
独立表空间就是每个表单独创建一个 .ibd 文件,该文件存储着该表的索引和数据。由 innodb_file_per_table 变量控制。禁用 innodb_file_per_table 会导致 InnoDB 在系统表空间中创建表。
InnoDB 表空间文件 .ibd 初始大小为 96K,而 InnoDB 默认页大小为 16K,页大小也可以通过 innodb_page_size 配置。
在 ibd 文件中,0-16KB 偏移量即为 0 号数据页,16KB-32KB 的为 1 号数据页,以此类推。
页的头尾除了一些元信息外,还有 Checksum 校验值,这些校验值在写入磁盘前计算得到,当从磁盘中读取时,重新计算校验值并与数据页中存储的对比,如果发现不同,则会导致 MySQL 崩溃。
InnoDB 是基于**磁盘**存储的,并将其中的数据按**页的方式**进行管理。因此 InnoDB 可视为基于磁盘的数据库系统。由于 CPU 的速度和磁盘 IO 速度的巨大鸿沟,需要缓冲池来提高数据库的整体性能
缓冲池是主内存中的一个区域,在InnoDB
访问表和索引数据时会在其中进行 高速缓存。缓冲池允许直接从内存中处理经常使用的数据,从而加快了处理速度。在专用服务器上,通常将多达 80%的物理内存分配给缓冲池。
为了提高大容量读取操作的效率,缓冲池被分为多个页面,这些页面可能包含多个行。为了提高缓存管理的效率,缓冲池被实现为页面的链接列表。使用LRU算法的变体将很少使用的数据从缓存中老化掉 。
知道如何利用缓冲池将经常访问的数据保留在内存中是 MySQL 优化的重要方面。
简要架构图
它是 MySQL 抽象出来的数据单位,磁盘文件中就是存放了很多数据页,每个数据页里存放了很多行数据。
默认情况下,数据页的大小是 16kb。大概结果如下图所示
所以对应的,在 Buffer Pool
中,也是以数据页为数据单位,存放着很多数据。但是我们通常叫做缓存页,因为 Buffer Pool
毕竟是一个缓冲池,并且里面的数据都是从磁盘文件中缓存到内存中。
所以,默认情况下缓存页的大小也是 16kb,因为它和磁盘文件中数据页是一一对应的。
所以,缓冲池和磁盘之间的数据交换的单位是数据页,包括从磁盘中读取数据到缓冲池和缓冲池中数据刷回磁盘中,如图所示:
缓冲池是 InnoDB 存储引擎中最重要的组件。因为为了提高 MySQL 的并发性能,使用到的数据都会缓存在缓冲池中,然后所有的增删改查操作都将在缓冲池中执行。
通过这种方式,保证每个更新请求,尽量就是**只更新内存,然后往磁盘顺序写日志文件**。
更新内存的性能是极高的,然后顺序写磁盘上的日志文件的性能也是比较高的,因为顺序写磁盘文件,他的性能要远高于随机读写磁盘文件。
mysql> show variables like 'innodb_buffer_pool_size';
++-------------------------+---------+
+| Variable_name | Value |
++-------------------------+---------+
+| innodb_buffer_pool_size | 8388608 |
++-------------------------+---------+
+1 row in set (0.02 sec)
+
到此,我们都知道 Buffer Pool
中是用缓存页来缓存数据的,但是我们怎么知道缓存页对应着哪个表,对应着哪个数据页呢?
所以每个缓存页都会对应着一个描述数据块,里面包含数据页所属的表空间、数据页的编号,缓存页在 Buffer Pool
中的地址等等。
描述数据块本身也是一块数据,它的大小大概是缓存页大小的 5%左右,大概 800 个字节左右的大小。
描述如图所示:
即缓冲池的个数。每页根据哈希值分配到不同缓冲池实例
减少资源竞争、支持更大的并发处理,加快查询速度
查看缓冲池实例个数(默认为 1)
mysql> show variables like 'innodb_buffer_pool_instances';
++------------------------------+-------+
+| Variable_name | Value |
++------------------------------+-------+
+| innodb_buffer_pool_instances | 1 |
++------------------------------+-------+
+1 row in set (0.03 sec)
+
可通过配置文件修改实例个数
直到现在,估计大家都以为缓冲池只是一个大的内存区域,在 InnoDB 存储引擎中只有一个,这是对的吗?
我们可以想象,如果 InnoDB 存储引擎只有一个 Buffer Pool
,当高并发时,多个请求进来,那么为了保证数据的一致性(缓存页、free 链表、flush 链表、lru 链表等多种操作),必须得给缓冲池加锁了,每一时刻只能有一个请求获得锁去操作 Buffer Pool
,其他请求只能排队等待锁释放。那么此时 MySQL 的性能是多么的低!
既然一个 Buffer Pool
不够用,那么整多几个呗。
在生产环境中,其实我们是可以给 MySQL 设置多个 Buffer Pool
来提升 MySQL 的并发能力的~
如何设置?
我们先看看 MySQL 的默认规则:如果你给 Buffer Pool
分配的内存小于 1GB,那么最多就只会给你一个 Buffer Pool
。
但是呢,如果你给 MySQL 设置的内存很大,此时你可以利用下面两个参数来设置 Buffer Pool
的总大小和总实例数,这样,MySQL 就能有多个 Buffer Pool
来支撑高并发了。
[server]
+innodb_buffer_pool_size = 8589934592
+innodb_buffer_pool_instances = 4
+
解释一下:上面利用参数 innodb_buffer_pool_size
来设置 Buffer Pool
的总大小为 8G,利用参数 innodb_buffer_pool_instances
来设置一共有 4 个 Buffer Pool
实例。那么就是说,MySQL 一共有 4 个 Buffer Pool
,每个的大小为 2G。
当然了,每个 Buffer Pool
负责管理着自己的描述数据块和缓存页,有自己独立一套 free 链表、flush 链表和 lru 链表。
到这,我们就晓得,只要你能分配足够大的内存给 Buffer Pool
,你就能创建尽量多的 Buffer Pool
来应对高并发场景~
正所谓,并发性能不高,机器配置来凑,这还是有道理的。
但是正经点来说,最基本最主要的还是咱们写的 SQL。当然了,能写出一手好 SQL,前提我们还是得理解 MySQL 各个组件的原理,熟悉索引的原理、事务原理和锁原理等。当然了,之后我也会分别对这些做出一个学习总结分享出来。
相信基本每个公司,项目上线后,用户和流量会不断地增长,这对于 MySQL 来说,会有什么变化?
首先,访问增多,不断地从磁盘文件中的数据页读取数据到 Buffer Pool
,也不断地将 Buffer Pool
的脏缓存页刷回磁盘文件中。很明显的,Buffer Pool
越小,这两个操作就会越频繁,但是磁盘 IO 操作又是比较耗时的,本来 SQL 执行只要 20 ms,如果碰巧碰到遇到缓存页用完,就要经历一系列的操作,SQL 最后执行完可能就要 200 ms,甚至更多了。
所以我们此时需要及时调整 Buffer Pool
的大小。
但是生产环境,肯定不能让我们直接修改 MySQL 配置然后再重启吧,这估计要骂死。
在 MySQL 5.7 后,MySQL 允许我们动态调整参数 innodb_buffer_pool_size
的值来调整 Buffer Pool
的大小了。
假如就这样直接调大会存在啥问题?
假设调整前的配置:Buffer Pool 的总大小为 8G,一共 4 个 Buffer Pool,每个大小为 2G。
[server]
+innodb_buffer_pool_size = 8589934592
+innodb_buffer_pool_instances = 4
+
假设给 Buffer Pool
调整到 16 G,就是说参数 innodb_buffer_pool_size
改为 17179869184。
此时,MySQL 会为 Buffer Pool
申请一块大小为 16G 的连续内存,然后分成 4 块,接着将每一个 Buffer Pool
的数据都复制到对应的内存块里,最后再清空之前的内存区域。
我们可以发现,全部数据要从一块地方复制到另外一块地方,那这是相当耗费时间的操作,整整 8 个 G 的数据要进行复制粘贴呢!而且,如果本来 Buffer Pool
是更大的话,那就更恐怖了。
为了解决上面的问题,Buffer Pool
引入一个机制:chunk 机制。
Buffer Pool
其实是由多个 chunk 组成的。每个 chunk 的大小由参数 innodb_buffer_pool_chunk_size
控制,默认值是 128M。Buffer Pool
里的所有 chunk 共享一套 free、flush、lru 链表。得益于 chunk 机制,就能避免了上面说到的问题。当扩大 Buffer Pool
内存时,不再需要全部数据进行复制和粘贴,而是在原本的基础上进行增减内存。
下面继续用上面的例子,介绍一下 chunk 机制下,Buffer Pool
是如何动态调整大小的。
调整前 Buffer Pool
的总大小为 8G,调整后的 Buffer Pool
大小为 16 G。
由于 Buffer Pool
的实例数是不可以变的,所以是每个 Buffer Pool
增加 2G 的大小,此时只要给每个 Buffer Pool
申请 (2000M/128M)个 chunk 就行了,但是要注意的是,新增的每个 chunk 都是连续的 128M 内存。
我们都知道,给 Buffer Pool 分配越大的内存,MySQL 的并发性能就越好。那是不是都应该将百分之九十九的机器的内存都分配给 Buffe Pool 呢?
那当然不是了!
先不说操作系统内核也需要几个 G 内存,MySQL 除了 Buffer Pool 还有很多别的内存数据结构呢,这些都是需要内存的,所以说,上面的想法是绝对不行的!
比较合理的比例,应该是 Buffer Pool 的内存大小占机器总内存的 50% ~ 60%,例如机器的总内存有 32G,那么你给 Buffer Pool 分配个 20G 左右就挺合理的了。
Buffer Pool 的总大小搞定了,那应该设置多少个实例数呢?
这里有一个公式:Buffer Pool 总大小 = (chunk 大小 * Buffer Pool 数量)* n 倍。
上个例子解释一下。
假设此时 Buffer Pool 的总大小为 8G,即 8192M,那么 Buffer Pool 的数量应该是多少个呢?
8192 = ( 128 _ Buffer Pool 数量)_ n
64 个:也是可以的,但是每个 Buffer Pool 就只要一个 chunk。
16 个:也是可以的,每个 Buffer Pool 拥有四个 chunk。
8 个:也是可以的,每个 Buffer Pool 拥有八个 chunk。
所以说,只要你的 Buffer Pool 数量符合上面的公式,其实都是可以的,看你们根据业务后怎么选择了。
到此,我们都知道了,Buffer Pool
是缓存数据的数据单位为缓存页,利用描述数据块来标识缓存页。
那么,MySQL 启动时,是如何初始化 Buffer Pool
的呢?
MySQL 启动时,会根据参数 innodb_buffer_pool_size
的值来为 Buffer Pool
分配内存区域。
然后会按照缓存页的默认大小 16k 以及对应的描述数据块的 800 个字节 左右大小,在 Buffer Pool
中划分中一个个的缓存页和一个个的描述数据库块。
注意,此时的缓存页和描述数据块都是空的,毕竟才刚启动 MySQL 呢。
介绍 double write 之前我们有必要了解partial page write
(部分页失效)问题。
InnoDB 的 Page Size
一般是 16KB,其数据校验也是针对这 16KB 来计算的。将数据写入到磁盘是以 Page 为单位进行操作的。我们知道,由于文件系统对一次大数据页(例如 InnoDB 的 16KB)大多数情况下不是原子操作,这意味着如果服务器宕机了,可能只做了部分写入。假如 16K 的数据,写入 4K 时,发生了系统断电/os crash ,只有一部分写是成功的,这种情况下就是 partial page write 问题。
有经验的 DBA 可能会想到,如果发生写失效,MySQL 可以根据 redo log
进行恢复。这是一个办法,但是必须清楚地认识到,redo log
中记录的是对页的物理修改(redo 的最小原子单位是数据库中的页这个概念)。如偏移量 800,写"aaaa"记录。如果这个页本身已经发生了损坏,再对其进行重做是没有意义的。MySQL 在恢复的过程中检查 page 的 checksum,checksum 就是检查 page 的最后事务号,发生 partial page write
问题时,page 已经损坏,找不到该 page 中的事务号。在 InnoDB 看来,这样的数据页是无法通过 checksum 验证的,就无法恢复。即时我们强制让其通过验证,也无法从崩溃中恢复,因为当前 InnoDB 存在的一些日志类型,有些是逻辑操作,并不能做到幂等。
为了解决这个问题,InnoDB 实现了 double write buffer
,简单来说,就是在写数据页之前,先把这个数据页写到一块独立的物理文件位置(ibdata 文件,在 8.0.20 之后,doublewrite buufer 也存在单独的文件中了)。然后再写到数据页。这样在宕机重启时,如果出现数据页损坏,那么在应用 redo log
之前,需要通过该页的副本来还原该页,然后再进行 redo log
重做,这就是 double write。double write 技术带给 innodb 存储引擎的是数据页的可靠性。
按照英文的直接原义就是 Least Recently Used,最近最久未使用。
它是按照一个非常著名的计算机操作系统基础理论得来的:最近使用的页面数据会在未来一段时期内仍然被使用,已经很久没有使用的页面很有可能在未来较长的一段时间内仍然不会被使用。基于这个思想,会存在一种缓存淘汰机制,每次从内存中找到**最久未使用的数据然后置换出来**,从而存入新的数据!
它的主要衡量指标是**使用的时间**,
(附加指标是**使用的次数**)
在计算机中大量使用了这个机制,它的合理性在于**优先筛选热点数据**,所谓热点数据,就是**最近最多使用的数据**!
缓冲池中,页的大小也是 16KB
新增了 midPoint 位置。新读取到的页并没有直接放在 LRU 列的首部,而是放在距离尾部 37%的位置。这个算法称之为**midpoint insertion stategy**。
即 midPoint 在整体列表的 ⅝ 处
midpoint 之前的是 new 区域(热数据)
midpoint 之后的数据是不活跃数据,old 区域。
mysql> show variables like 'innodb_old_blocks_pct';
++-----------------------+-------+
+| Variable_name | Value |
++-----------------------+-------+
+| innodb_old_blocks_pct | 37 |
++-----------------------+-------+
+1 row in set (0.04 sec)
+
例如**索引扫描**或**数据扫描** / 全表扫描,会使大量的缓冲池中大量的页被刷新出去。然而被扫描到的数据页只是本次操作所需要的,并非热点数据。而真正的热点数据还是从磁盘读取,影响了缓冲池效率。
MySQL 为了提高性能,提供了一个机制:预读机制。
当你从磁盘上加载一个数据页的时候,他可能会连带着把这个数据页相邻的其他数据页,也加载到缓存里去。这个机制会带来这么一个问题:连带的数据页可能在后面的查询或者修改中,并不会用到,但是它们却在 lru 链表的头部。
若还未理解,可参阅官方文档:防止缓冲池扫描
理想情况下,您可以将缓冲池的大小设置为尽可能大的值,从而为服务器上的其他进程留出足够的内存来运行,而不需要过多的分页。缓冲池越大,InnoDB 就越像内存中的数据库,从磁盘读取数据一次,然后在后续读取期间访问内存中的数据。但并不是越大越好
在具有足够内存的 64 位系统上,您可以将缓冲池拆分为多个部分,以最大限度地减少并发操作之间对内存结构的争用。即增加缓冲池实例
您可以将频繁访问的数据保留在内存中,而不考虑会将大量不经常访问的数据引入缓冲池的操作的突然活动高峰。可以调整 midpoint 的位置。
您可以控制如何以及何时执行预读请求,以便在预期很快需要页面的情况下将页面异步预取到缓冲池中。
您可以控制何时发生后台刷新,以及是否根据工作负载动态调整刷新速率
您可以配置 InnoDB 如何保留当前的缓冲池状态,以避免服务器重启后过长的预热期
控制最新数据页放到热点区域的时间,达到这个时间后,就可以把数据放到热点区域。
mysql> show variables like 'innodb_old_blocks_time';
++------------------------+-------+
+| Variable_name | Value |
++------------------------+-------+
+| innodb_old_blocks_time | 1000 |
++------------------------+-------+
+1 row in set (0.04 sec)
+
InnoDB
可以使用访问的标准监视器输出, SHOW ENGINE INNODB STATUS
提供有关缓冲池操作的度量。缓冲池度量标准位于BUFFER POOL AND MEMORY
“ InnoDB
标准监视器”输出中的部分,其输出类似于以下内容:
----------------------
+BUFFER POOL AND MEMORY
+----------------------
+Total large memory allocated 2198863872
+Dictionary memory allocated 776332
+Buffer pool size 131072
+Free buffers 124908
+Database pages 5720
+Old database pages 2071
+Modified db pages 910
+Pending reads 0
+Pending writes: LRU 0, flush list 0, single page 0
+Pages made young 4, not young 0
+0.10 youngs/s, 0.00 non-youngs/s
+Pages read 197, created 5523, written 5060
+0.00 reads/s, 190.89 creates/s, 244.94 writes/s
+Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not
+0 / 1000
+Pages read ahead 0.00/s, evicted without access 0.00/s, Random read
+ahead 0.00/s
+LRU len: 5720, unzip_LRU len: 0
+I/O sum[0]:cur[0], unzip sum[0]:cur[0]
+
下表描述了InnoDB
标准监视器报告的缓冲池度量 标准
名称 | 描述 |
---|---|
Total memory allocated | 为缓冲池分配的总内存(以字节为单位) |
Dictionary memory allocated | 为InnoDB 数据字典分配的总内存,以字节为单位 |
Buffer pool size | 分配给缓冲池的页面总大小 |
Free buffers | 缓冲池空闲列表的页面总大小 |
Database pages | 缓冲池 LRU 列表的页面总大小 |
Old database pages | 缓冲池旧 LRU 子列表的页面总大小 |
Modified db pages | 缓冲池中当前修改的页面数 |
Pending reads | 等待读入缓冲池的缓冲池页面数 |
Pending writes LRU | 从 LRU 列表的底部开始写入的缓冲池中的旧脏页数 |
Pending writes flush list | 检查点期间要刷新的缓冲池页面数 |
Pending writes single page | 缓冲池中暂挂的独立页面写入数 |
Pages made young | 缓冲池 LRU 列表中变年轻的页面总数(已移至“ new ”页面的子列表的开头) |
Pages made not young | 缓冲池 LRU 列表中没有年轻的页面总数(保留在“ old ”子列表中但没有年轻的页面) |
youngs/s | 每秒平均访问缓冲池 LRU 列表中的旧页面所导致的页面年轻 |
non-youngs/s | 每秒平均访问缓冲池 LRU 列表中的旧页面导致的页面不年轻 |
Pages read | 从缓冲池读取的页面总数 |
Pages created | 在缓冲池中创建的页面总数 |
Pages written | 从缓冲池写入的页面总数 |
reads/s | 每秒平均每秒读取的缓冲池页面数 |
creates/s | 每秒平均创建的缓冲池页面的每秒数量 |
writes/s | 每秒平均缓冲池页面写入数 |
Buffer pool hit rate | 从缓冲池内存与磁盘存储读取的页面的缓冲池页面命中率 |
young-making rate | 页面访问的平均命中率使页面更年轻 |
not (young-making rate) | 页面访问未使页面变年轻的平均命中率 |
Pages read ahead | 预读操作的每秒平均数 |
Pages evicted without access | 每秒从缓冲池访问而未访问的页面的平均值 |
Random read ahead | 随机预读操作的每秒平均数 |
LRU len | 缓冲池 LRU 列表的页面总大小 |
unzip_LRU len | 缓冲池 unzip_LRU 列表的页面总大小 |
I/O sum | 最近 50 秒内访问的缓冲池 LRU 列表页面的总数 |
I/O cur | 已访问的缓冲池 LRU 列表页面的总数 |
I/O unzip sum | 已访问的缓冲池 unzip_LRU 列表页面的总数 |
I/O unzip cur | 已访问的缓冲池 unzip_LRU 列表页面的总数 |
缓冲池服务器状态变量和
INNODB_BUFFER_POOL_STATS
表提供了许多与InnoDB
标准监视器输出中相同的缓冲池度量 标准
InnoDB 从 1.0.X 开始支持页压缩技术。原本 16k 的页,可以压缩到 2k、4k、8k。因此需要 unzip_LRU 列来管理,但是注意:LRU list 中包含了 unzip_LRU
如何给 unzip_LRU 分配内存:(假设需要从缓冲池中申请 4KB 大小的内存)
它是一个双向链表,链表的每个节点就是一个个空闲的缓存页对应的描述数据块。
他本身其实就是由 Buffer Pool
里的描述数据块组成的,你可以认为是每个描述数据块里都有两个指针,一个是 free_pre 指针,一个是 free_next 指针,分别指向自己的上一个 free 链表的节点,以及下一个 free 链表的节点。
通过 Buffer Pool
中的描述数据块的 free_pre 和 free_next 两个指针,就可以把所有的描述数据块串成一个 free 链表。
下面我们可以用伪代码来描述一下 free 链表中描述数据块节点的数据结构:
DescriptionDataBlock{
+ block_id = block1;
+ free_pre = null;
+ free_next = block2;
+}
+
free 链表有一个基础节点,他会引用链表的头节点和尾节点,里面还存储了链表中有多少个描述数据块的节点,也就是有多少个空闲的缓存页。
下面我们也用伪代码来描述一下基础节点的数据结构:
FreeListBaseNode{
+ start = block01;
+ end = block03;
+ count = 2;
+}
+
到此,free 链表就介绍完了。上面我们也介绍了 MySQL 启动时 Buffer Pool
的初始流程,接下来,我会将结合刚介绍完的 free 链表,讲解一下 SQL 进来时,磁盘数据页读取到 Buffer Pool
的缓存页的过程。但是,我们先要了解一下一个新概念:数据页缓存哈希表,它的 key 是表空间+数据页号,而 value 是对应缓存页的地址。
描述如图所示:
Buffer Pool
的缓存页的过程¶首先,SQL 进来时,判断数据对应的数据页能否在 数据页缓存哈希表里 找到对应的缓存页。
如果找到,将直接在 Buffer Pool
中进行增删改查。
如果找不到,则从 free 链表中找到一个空闲的缓存页,然后从磁盘文件中读取对应的数据页的数据到缓存页中,并且将数据页的信息和缓存页的地址写入到对应的描述数据块中,然后修改相关的描述数据块的 free_pre 指针和 free_next 指针,将使用了的描述数据块从 free 链表中移除。记得,还要在数据页缓存哈希表中写入对应的 key-value 对。最后也是在 Buffer Pool
中进行增删改查。
数据库刚启动时,LRU list 是空的。Free list 是最大的。当需要从缓冲池中分页时,看 Free list 有空闲页:
有则删除 Free list 的页,加入到 LRU list 中。维持一个数量平衡
否则,根据 LRU 算法,淘汰 LRU 末尾的页,省出内存来,分配给新的页
lru 链表尾部的缓存页何时刷入磁盘?
当 free list 为空了,此时需要将数据页加载到缓冲池里,就会 lru list 的 old 数据区域尾部的缓存页刷入磁盘,然后清空,再加载数据页的数据。
一条后台线程,运行一个定时任务,定时将 lru list 的 old 数据区域的尾部的一些缓存页刷入磁盘,然后清空,最后把他们对应的描述数据块加入到 free 链表中去。
当然了,除了 lru list 尾部的缓存页会被刷入磁盘,还有的就是 flush list 的缓存页。
后台线程同时也会在 MySQL 不繁忙的时候,将 flush 链表中的缓存页刷入磁盘中,这些缓存页的描述数据块会从 lru 链表和 flush 链表中移除,并加入到 free 链表中。
我们都知道 SQL 的增删改查都在 Buffer Pool
中执行,慢慢地,Buffer Pool
中的缓存页因为不断被修改而导致和磁盘文件中的数据不一致了,也就是 Buffer Pool
中会有很多个脏页,脏页里面很多脏数据。
所以,MySQL 会有一条后台线程,定时地将 Buffer Pool
中的脏页刷回到磁盘文件中。
但是,后台线程怎么知道哪些缓存页是脏页呢,不可能将全部的缓存页都往磁盘中刷吧,这会导致 MySQL 暂停一段时间。
MySQL 通过 checkPoint 技术将脏页刷新到磁盘
Flush list 也是通过缓存页的描述数据块中的两个指针,让修改过的缓存页的描述数据块能串成一个双向链表,这两指针大家可以认为是 flush_pre 指针和 flush_next 指针。
下面我用伪代码来描述一下:
DescriptionDataBlock{
+ block_id = block1;
+ // free 链表的
+ free_pre = null;
+ free_next = null;
+
+ // flush 链表的
+ flush_pre = null;
+ flush_next = block2;
+}
+
flush 链表也有对应的基础节点,也是包含链表的头节点和尾节点,还有就是修改过的缓存页的数量。
FlushListBaseNode{
+ start = block1;
+ end = block2;
+ count = 2;
+}
+
到这里,我们都知道,SQL 的增删改都会使得缓存页变为脏页,此时会修改脏页对应的描述数据块的 flush_pre 指针和 flush_next 指针,使得描述数据块加入到 flush 链表中,之后 MySQL 的后台线程就可以将这个脏页刷回到磁盘中。
描述如图所示:
数据库中的表是由一行行记录(rows)所组成,每行记录被存储在一个页中,在 MySQL 中,一个页的大小默认为 16K,一个个页又组成了每张表的表空间。
通常我们认为, 如果一个页中存放的记录数越多,数据库的性能越高 。这是因为数据库表空间中的页是存放在磁盘上,MySQL 数据库先要将磁盘中的页读取到内存缓冲池,然后以页为单位来读取和管理记录。
一个页中存放的记录越多,内存中能存放的记录数也就越多,那么存取效率也就越高。若想将一个页中存放的记录数变多,可以启用压缩功能。
此外,启用压缩后,存储空间占用也变小了,同样单位的存储能存放的数据也变多了。
表压缩可以在创建表时开启,压缩表能够使表中的数据以压缩格式存储,压缩能够显著提高原生性能和可伸缩性。压缩意味着在硬盘和内存之间传输的数据更小且占用相对少的内存及硬盘,对于辅助索引,这种压缩带来更加明显的好处,因为索引数据也被压缩了。压缩对于硬盘是 SSD 的存储设备尤为重要,因为它们相对普通的 HDD 硬盘比较贵且容量有限。
我们都知道,CPU 和内存的速度远远大于磁盘,因为对于数据库服务器,磁盘 IO 可能会成为紧要资源或者瓶颈。数据压缩能够让数据库变得更小,从而减少磁盘的 I/O,还能提高系统吞吐量,以很小的成本(耗费较多的 CPU 资源)。对于读比重比较多的应用,压缩是特别有用。压缩能够让系统拥有足够的内存来存储热数据。
在创建InnoDB
表时带上ROW_FORMAT=COMPRESSED
参数能够使用比默认的 16K 更小的页。这样在读写时需要更少的 I/O,对于 SSD 磁盘更有价值。
COMPRESS 页压缩是MySQL5.7
版本之前提供的页压缩功能。只要在创建表时指定ROW_FORMAT=COMPRESS
,并设置通过选项KEY_BLOCK_SIZE
设置压缩的比例。
CREATE TABLE `v_test2` (
+ `a` varchar(4) COLLATE utf8mb4_general_ci DEFAULT NULL,
+ `b` char(4) COLLATE utf8mb4_general_ci DEFAULT NULL,
+ `c` char(10) COLLATE utf8mb4_general_ci DEFAULT NULL,
+ `f` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8 ;
+
InnoDB 未压缩的数据页是 16KB,根据选项组合值,MySQL 为每个表的.ibd 文件使用 1KB,2KB,4KB,8kb,16KB 页大小。
实际的压缩算法并不会受 KEY_BLOCK_SIZE 值影响,这个值只是决定每个压缩块有多大,从而影响多少行被压缩到每个页。
设置 KEY_BLOCK_SIZE 值等于 16k 并不能有效的进行压缩,因为默认的 innodb 页就是 16k,但是对于拥有很多 BLOB,TEXT,VARCHAR 类型字段的表可能会有效果的。
到此,我已经将缓冲池 Buffer Pool
介绍完毕了。
下面简单总结一下 Buffer Pool
从初始化到使用的整个流程。
1、MySQL 启动时会根据分配指定大小内存给 Buffer Pool
,并且会创建一个个描述数据块和缓存页。
2、SQL 进来时,首先会根据数据的表空间和数据页编号查询 数据页缓存哈希表 中是否有对应的缓存页。
3、如果有对应的缓存页,则直接在 Buffer Pool
中执行。
4、如果没有,则检查 free 链表看看有没有空闲的缓存页。
5、如果有空闲的缓存页,则从磁盘中加载对应的数据页,然后将描述数据块从 free 链表中移除,并且加入到 lru 链表的 old 数据区域的链表头部。后面如果被修改了,还需要加入到 flush 链表中。
6、如果没有空闲的缓存页,则将 lru 链表的 old 数据区域的链表尾部的缓存页刷回磁盘,然后清空,接着将数据页的数据加载到缓存页中,并且描述数据块会加入到 lru 链表的 old 数据区域的链表头部。后面如果被修改了,还需要加入到 flush 链表中。
7、5 或者 6 后,就接着在 Buffer Pool
中执行增删改查。
注意:5 和 6 中,缓存页加入到 old 数据区域的链表头部后,如果在 1s 后被访问,则将入到 new 数据区域的链表头部。
8、最后,就是描述数据块随着 SQL 语句的执行不断地在 free 链表、flush 链表和 lru 链表中移动了。
在如今开源数据库的时代,越来越多的人开始研究数据库的源码,并给社区贡献代码,MySQL 官方每次发布新版本都要感谢一些在社区上贡献代码的程序员。 现在新的数据库时代也给 DBA 提出了更高的要求,学会调试源码,通过源码定位问题,这是 DBA 进阶的方向。MySQL 的源码有几百上千万行,想全部搞懂几乎是不可能的,研究源码一般推荐从某个功能点入手。 而学会调试源码,不管对研究源码或通过源码定位问题,都是必备的技能。
一般在实际应用中,MySQL
都是运行在 Linux
平台下,在 Linux
平台下一般是通过 GDB(GNU symbolic debugger)
工具进行调试,C/C++
项目的开发和调试包括故障排查都是利用 GDB
完成的。
此外, VSCODE
这种 IDE
工具可以在本地的 Windows
操作系统下,通过 ssh
远程调试 Linux
平台下的 MySQL
。如果要在 Windows 上调试 Windows Vscode 插件
装完后,左侧会显示:分上下两栏。上栏是你本地 Windows
上装的 VSCode 插件;下栏是你远端 Linux
上装的 VSCode
插件。
源代码版本选择
首先需要从官网上下载源码,操作系统选择为 source code
,操作系统版本选择为 ALL OPERATING SYSTEM
,下载带 boost
头文件的源码包。如果对 MySQL
的版本没有特别要求的话,一般推荐下载最新版本的。 因为老版本中存在 bug
的概率较大,编译过程需要解决这些 bug
,比如在 8.0.23
版本中编译过程中报了这个错:buf0buf.cc:1227:44: error: ‘SYS_gettid’ was not declared in this scope
。 参考 MySQL
官方论坛:https://forums.mysql.com/read.php?117,674410,676378#msg-676378,在storage/innobase/buf/buf0flu.cc
文件代码中加上声明#include <sys/syscall.h>
,解决了这个报错。
环境配置
# 准备环境
+apt install -y cmake make gcc g++ libncurses5-dev bison openssl libssl-dev git autoconf automake libtool unzip build-essential perl pkg-config
+
+# 创建目录
+mkdir -p /data/{mysql_source_code,mysql_install_dir,mysql_data} && cd /data/mysql_source_code
+
+# 直接去 https://dev.mysql.com/downloads/mysql/ 直接下载带 Boost 第三方库依赖的源码。
+# Boost 是一个功能强大、构造精巧、跨平台、开源并且完全免费的 C++ 程序库,可以认为是半个C++标准库。
+# MySQL 的代码依赖 Boost库,所以直接下载一个携带Boost库的源码比较省心,不需要再去下载对应的Boost库。
+wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-boost-8.0.39.tar.gz -P /data/mysql_source_code
+
+# wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.39.tar.gz
+
+# 解压
+cd /data/mysql_source/ && tar -zxvf mysql-boost-8.0.39.tar.gz
+
+# 创建build目录并进入
+mkdir -p /data/mysql_source_code/mysql-8.0.39/build/ && cd /data/mysql_source_code/mysql-8.0.39/build/
+
+# Configure , 负责将源代码与当前系统进行配置和适配。
+
+cmake .. -DWITH_BOOST=/data/mysql_source_code/mysql-8.0.39/boost \
+-DWITH_DEBUG=1 \
+-DCMAKE_BUILD_TYPE=1\
+-DWITH_INNOBASE_STORAGE_ENGINE=1\
+-DWITH_ARCHIVE_STORAGE_ENGINE=1\
+-DWITH_BLACKHOLE_STORAGE_ENGINE=1\
+-DWITH_FEDERATED_STORAGE_ENGINE=1\
+-DWITH_PARTITION_STORAGE_ENGINE=1\
+-DMYSQL_TCP_PORT=3306\
+-DENABLED_LOCAL_INFILE=1\
+-DEXTRA_CHARSETS=all\
+-DEFAULT_CHARSET=utf8\
+-DDEFAULT_COLLATION=utf8_general_ci\
+-DMYSQL_USER=mysql\
+-DWITH_BINLOG_PREALLOC=ON\
+-DCMAKE_INSTALL_PREFIX=/data/mysql_install_dir
+
+# 参数含义
+# DWITH_DEBUG=1 这个是最关键的配置,是为了开启debug调试模式;
+# DCMAKE_INSTALL_PREFIX= 表示编译状态的路径,选择源码文件夹之外的一个自建的build文件夹;
+# DWITH_BOOST= 指定 boost 路径,可以直接指向源码文件夹下的boost文件夹;
+# DCMAKE_BUILD_TYPE=1 表示开启debug,方便后续代码调试;
+# DWITH_BLACKHOLE_STORAGE_ENGINE=1 表示开启BLACKHOLE存储引擎
+# DWITH_PARTITION_STORAGE_ENGINE=1 表示开启PARTITION存储引擎
+# DWITH_FEDERATED_STORAGE_ENGINE=1 表示开启FEDERATED存储引擎
+# CMAKE_INSTALL_PREFIX= 这个表示BASEDIR路径,默认是/usr/local/mysql,是各种配置的路径前缀PREFIX
+# DMYSQL_DATADIR: 这个表示表示MySQL默认的数据目录,选择build文件夹下的data文件
+# 其他详细参数参考官网 https://dev.mysql.com/doc/refman/8.4/en/source-configuration-options.html
+# Cmake构建参数,主要分为几类:
+# 1. 通用参数:
+# 2. 安装布局参数:
+# 3. 存储参数:
+# 4. 特性参数:
+
+# 根据 Makefile 中的规则进行实际的编译过程,生成可执行文件或库。
+make -j4
+
+# 负责将最终编译好的文件复制到指定的安装目录中,以供系统中的其他程序使用。
+make install
+
+# 也可以使用make package来生成安装包(就像二进制包一样)
+
+
+# 接着make install成功后,配置一个简单的常规配置文件/etc/my.cnf,就可以初始化数据库并启动数据库了。
+
+
+
+/usr/local/mysql/bin/mysqld --initialize-insecure
+
+mysqld_safe --user=mysql &
+
+
+# 启动完数据库后,登录数据库可以发现现在已经是debug模式了。
+/usr/local/mysql/bin/mysql -u root -p
+mysql> select version();
++--------------+
+| version() |
++--------------+
+| 8.0.39-debug |
++--------------+
+1 row in set (0.00 sec)
+
+mysql>
+
/include/mysql_version.h
这是一个C语言的头文件,是在编译的过程中生成的,通过cmake和make之后就会生成。源代码目录中实际并不存在这个文件。
源代码实际上只有/include/mysql_version.h.in
,这种.h.in是一个模板文件,它是在cmake或者automake的过程中产生的一个用于输入设置信息等功能的中间文件。它会在你调用confing、automake等.sh文件之后,自动生成一个相应的.h文件,然后就可以在源码中调用。
# Bug #31466846 RENAME THE VERSION FILE TO MYSQL_VERSION
+
+version 是 C++11 的一个头文件,但 MySQL 以往都是在源代码中用 VERSION 这个文件来表示版本号,在引入文件时又因 macOS 不区分文件大小写,产生了冲突,导致编译时报错中断。所以后面改成了MYSQL_VERSION
+
https://dev.mysql.com/doc/dev/mysql-server/8.0.40/mysql__version_8h_source.html
https://cloud.tencent.com/developer/article/2223495
#define PROTOCOL_VERSION 10
+#define MYSQL_SERVER_VERSION "8.0.39"
+#define MYSQL_BASE_VERSION "mysqld-8.0"
+#define MYSQL_SERVER_SUFFIX_DEF ""
+#define MYSQL_VERSION_ID 80039
+#define MYSQL_VERSION_STABILITY "LTS"
+#define MYSQL_PORT 3306
+#define MYSQL_ADMIN_PORT 33062
+#define MYSQL_PORT_DEFAULT 0
+#define MYSQL_UNIX_ADDR "/tmp/mysql.sock"
+#define MYSQL_CONFIG_NAME "my"
+#define MYSQL_PERSIST_CONFIG_NAME "mysqld-auto"
+#define MYSQL_COMPILATION_COMMENT "Source distribution"
+#define MYSQL_COMPILATION_COMMENT_SERVER "Source distribution"
+#define LIBMYSQL_VERSION "8.0.39"
+#define LIBMYSQL_VERSION_ID 80039
+
从官方介绍文档中的这张原理图我们可以看到,用户本地的VS Code
是通过SSH
通道连接到远程主机的。用户的开发代码、运行环境、调试环境都是在远程主机上。
远程连接是基于Visual Studio Code Remote - SSH
这个扩展来实现的。
当用户进行远程连接时,VS Code
会在远程主机上安装一个 server 包,这个安装过程是在首次连接时自动完成的。由于 Server 包是安装和运行在远端服务器上,而本地的 VS Code 只是编辑和展示的窗口,两者之间通过SSH Tunnel
通信,因此实际的工作环境完全是在远端,如果需要使用第三方的扩展,也可以直接安装在远端服务器环境。
1、具体的连接过程如下:
首次连接时,下载VS Code Server
,VS Code Server
包的版本取决于你本地使用的VS Code
的版本,下载地址为:
x86: https://update.code.visualstudio.com/commit:${commit_id}/server-linux-x64/stable
+arm: https://update.code.visualstudio.com/commit:${commit_id}/server-linux-arm64/stable
+
其中commit_id
是变化的,每个不同版本的VS Code
的commit_id
都不同,可以在VS Code
的 Help -> About 中查看
[client]
+port = 3306
+socket = /usr/local/mysql/mysql.sock
+
+[mysql]
+prompt="\u@\h \R:\m:\s [\d]> "
+no-auto-rehash
+
+[mysqld]
+user = root
+port = 3306
+admin_address = 127.0.0.1
+basedir = /usr/local/mysql
+datadir = /data/mysql_data/
+socket = /usr/local/mysql/mysql.sock
+pid-file = /usr/local/mysql/mysqld.pid
+character-set-server = utf8mb4
+
{"use strict";/*!
+ * escape-html
+ * Copyright(c) 2012-2013 TJ Holowaychuk
+ * Copyright(c) 2015 Andreas Lubbe
+ * Copyright(c) 2015 Tiancheng "Timothy" Gu
+ * MIT Licensed
+ */var Va=/["'&<>]/;qn.exports=za;function za(e){var t=""+e,r=Va.exec(t);if(!r)return t;var o,n="",i=0,a=0;for(i=r.index;i