1、架构分层
总体上,我们可以把MySQL分成三层,跟客户端对接的连接层,真正执行操作的服务层,和跟硬件打交道的存储引擎层。
1.1 连接层
我们的客户端要连接到MySQL服务器3306端口,必须要跟服务端建立连接,那么管理所有的连接,验证客户端的身份和权限,这些功能就在连接层完成。
1.2 服务层
连接层会把SQL语句交给服务层,这里面又包含一系列的流程:
比如查询缓存的判断、根据SQL调用相应的接口,对我们的SQL语句进行词法和语 法的解析(比如关键字怎么识别,别名怎么识别,语法有没有错误等等)。
然后就是优化器,MySQL底层会根据一定的规则对我们的SQL语句进行优化,最 后再交给执行器去执行
1.3 存储引擎层
存储引擎就是我们的数据真正存放的地方,在MySQL里面支持不同的存储引擎,再往下就是内存或者磁盘。
2 总体结构
官网地址: https://dev.mysql.com/doc/refman/5.7/en/innodb-architecture.html
2.1 内存结构
Buffer Pool主要分为3个部分:Buffer Pool、Change Buffer、Adaptive Hash Index,另外还有一个(redo)log buffer.
2.1.1 Buffer Pool
Buffer Pool缓存的是页面信息,包括数据页、索引页。
Buffer Pool默认大小是128M (134217728字节),可以调整。
査看系统变量:
SHOW VARIABLES like '%innodb_buffer_pool%';
查看服务器状态,里面有很多跟Buffer Pool相关的信息:
SHOW STATUS LIKE '%innodb_buffer_pool%'
这些参数都可以在官网查到详细的含义,用搜索功能。
server-system-variables
内存的缓冲池满了怎么办?(Redis设置的内存满了怎么办?)Innodb用LRU算法来管理缓冲池(链表实现,不是传统的LRU,分成young和old),经过淘汰的数据就是热点数据。
2.1.2 LRU
传统LRU,可以用Map+链表实现。value存的是在链表中的地址。
首先,InnoDB 中确实使用了一个双向链表,LRU list。但是这个 LRU list 放的不是data page,而是指向缓存页的指针
如果写 buffer pool的时候发现没有空闲页了,就要从 buffer pool 中淘汰数据页了,它要根据 LRU 链表的数据来操作。
首先,InnoDB 的数据页并不是都是在访问的时候才缓存到buffer pool 的。InnoDB 有一个预读机制(read ahead)。也就是说,设计者认为访问某个 page的数据的时候,相邻的一些 page 可能会很快被访问到,所以先把这些 page 放到 bufferpool 中缓存起来。
https://dev.mysql.com/doc/refman/5.7/en/innodb-performance-read_ahead.html
这种预读的机制又分为两种类型,一种叫线性预读(异步的)(Linear read-ahead)为了便于管理,InnoDB 中把 64个相邻的 page 叫做一个extent(区)。如果顺序地访问了一个 extent的56 个 page,这个时候InnoDB 就会把下一个 extent(区)缓存到buffer pool中。
顺序访问多少个page才缓存下一个extent,由一个参数控制
show variables like 'innodb_read_ahead_threshold';
第二种叫做随机预读(Random read-ahead),如果 buffer pool 已经缓存了同一个 extent(区)的数据页的个数超过 13 时,就会把这个 extent 剩余的所有 page 全部缓存到 buffer pool.
但是随机预读的功能默认是不启用的,由一个参数控制:
show variables like 'innodb_random_read_ahead';
很明显,线性预读或者异步预读,能够把可能即将用到的数据提前加载到 buffepool,肯定能提升 I/0 的性能,所以是一种非常有用的机制。但是预读肯定也会带来一些副作用,就是导致占用的内存空间更多,剩余的空闲页更少。如果说 buffer pool size 不是很大,而预读的数据很多,很有可能那些真正的需要被缓存的热点数据被预读的数据挤出 buffer pool,淘汰掉了。下次访问的时候又要先去磁盘。
所以问题就来了,怎么让这些真正的热点数据不受到预读的数据的影响呢?
我想了一个办法,干脆把 LRU list 分成两部分,靠近 head 的叫做 new sublist用来放热数据(我们把它叫做热区)。靠近 tail 的叫做 old sublist,用来放冷数据(我们把它叫做冷区)。中间的分割线叫做 midpoint。也就是对 buffer pool 做一个冷热分离。
https://dev.mysql.com/doc/refman/5.7/en/innodb-buffer-pool.html
所有新数据加入到 buffer pool的时候,一律先放到冷数据区的 head,不管是预读的,还是普通的读操作。所以如果有一些预读的数据没有被用到,会在old
sublist(冷区)直接被淘汰。放到 LRU List 以后,如果再次被访问,都把它移动到热区的 head.如果热区的数据长时间没有被访问,会被先移动到冷区的 head 部,最后慢慢在 tail 被淘汰。
在默认情况下,热区占了5/8的大小,冷区占了3/8,这个值由innodb old blocks pct 控制,它代表的是 old 区的大小,默认是 37%也就是 3/8。innodb old blocks pct 的值可以调整,在 5%到 95%之间,这个值越大,new区越小,这个 LRU 算法就接近传统 LRU。如果这个值太小,old 区没有被访问的速度淘汰会更快。
OK,预读的问题,通过冷热分离解决了,还有没有其他的问题呢?我们先把数据放到冷区,用来避免占用热数据的存储空间。但是如果刚加载到冷区的数据立即被访问了一次,按照原来的逻辑,这个时候我们会马上把它移动到热区。
假设这一次加载然后被立即访问的冷区数据量非常大,比如我们查询了一张几千万数据的大表,没有使用索引,做了一个全表扫描。或者,dump 全表备份数据,这种查询属于短时间内访问,后面再也不会用到了,
如果短时间之内被访问了一次,导致它们全部被移动到热区的 head,它会导致很多热点数据被移动到冷区甚至被淘汰,造成了缓冲池的污染。
这个问题我们又怎么解决呢?那我们得想一个办法,对于加载到冷区然后被访问的数据,设置一个时间窗口只有超过这个时间之后被访问,我们才认为它是有效的访问。InnoDB 里面通过innodb old blocks time 这个参数来控制,默认是 1秒钟,也就是说 1秒钟之内被访问的,不算数,待在冷区不动。只有1秒钟以后被访问的才从冷区移动到热区的 head。
这样就可以从很大程度上避免全表扫描或者预读的数据污染真正的热数据
似乎比较完美了。
这样的算法,还有没有可以优化的空间呢?为了避免并发的问题,对于 LRU 链表的操作是要加锁的。也就是说每一次链表的移动,都会带来资源的竞争和等待。从这个角度来说,如果要进一步提升 InnoDBLRU 的效率,就要尽量地减少 LRU 链表的移动。
比如,把热区一个非常靠近 head 的 page 移动到 head,有没有这个必要呢?所以InnoDB 对于 new 区还有一个特殊的优化:
如果一个缓存页处于热数据区域,且在热数据区域的前1/4区域(注意是热数据区域的 1/4,不是整个链表的 1/4),那么当访问这个缓存页的时候,就不用把它移动到热数据区域的头部:如果缓存页处于热区的后 3/4 区域,那么当访问这个缓存页的时候,会把它移动到热区的头部。当需要更新一个数据页时,如果数据页在 Buffer Pool中存在,那么就直接更新好了否则的话就需要从磁盘加载到内存,再对内存的数据页进行操作。也就是说,如果没有命中缓冲池,至少要产生一次磁盘10,有没有优化的方式呢?
2.1.3 Change Buffer写缓冲
change buffer是buffer pool的一部分
如果这个数据页不是唯一索引,不存在数据重复的情况,也就不需要从磁盘加载索引页数据判断数据是不是重复(唯一性检查)。这种情况下可以先修改记录在内存的缓冲池中,从而提升更新(insert update delete)语句的执行速度。
这一块区域就是change buffer,5.5之前也叫Insert Buffer插入缓冲,现在也能支持delete和update。
最后把change buffer记录到数据页的操作叫做merge。当在访问这个数据页的时候、通过后台线程、数据库shutdown、redo log写满的情况下会触发merge操作,真实将数据数据存储到磁盘中。
当业务是写多读少,不会在写数据后立刻读取,并且数据库大部分索引都是非唯一索引时,就可以使用Change Buffer(写缓冲)。
可以修改change的大小,来支持写多读少的业务场景。参数表示changebuffer占bufferpool的比例默认25%
SHOW VARIABLES LIKE 'innodb_change_buffer_max_size';
2.1.4 Adaptive Hash Index
后续补充
2.1.5 Redo Log Buffer
官网说明:https://dev.mysql.com/doc/refman/5.7/en/innodb-redo-log.html
Redolog 也不是每一次都直接写入磁盘,在 Buffer Pool里面有一块内存区域(LogBuffer)专门用来保存即将要写入日志文件的数据,默认 16M,它一样可以节省磁盘 IO。
SHOW VARIABLES LIKE 'innodb_log_buffer_size';
需要注意:redo log 的内容主要是用于崩溃恢复。磁盘的数据文件,数据来自 bufferpool。redo log写入磁盘,不是写入数据文件。
那么,Log Buffer 什么时候写入 log file?
在我们写入数据到磁盘的时候,操作系统本身是有缓存的。fush 就是把操作系统缓冲区写入到磁盘。
log buffer 写入磁盘的时机,由一个参数控制,默认是 1。
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
0 (延退写) | log buffer将每秒一次地写入log file中,并且log file的flush操作同时进行。 该模式下,在事务提交的时候,不会主动触发写入磁盘的操作。 |
1 (默认,实时 写,实时刷) | 每次事务提交时MySQL都会把log buffer的数据写入log file,并且刷到磁盘 中去 |
2 (实时写,延 退刷) | 每次事务提交时MySQL都会把log buffer的数据写入log file。但是flush操 作并不会同时进行。该模式下,MySQL会每秒执行一次也sh操作。 |
刷盘越快,越安全,但是也会越消耗性能。
2.2 磁盘结构
2.2.1 系统表空间
在默认情况下 InnoDB 存储引擎有一个共享表空间(对应文件/var/lib/mysqlibdata1),也叫系统表空间。
InnoDB 系统表空间包含InnoDB 数据字典和双写缓冲区,Change Buffer和 UndoLogs),如果没有指定 file-per-table,也包含用户创建的表和索引数据。
1、undo 在后面介绍,因为也可以设置独立的表空间
2、数据字典:由内部系统表组成,存储表和索引的元数据(定义信息)。
3、双写缓冲(InnoDB 的一大特性)
InnoDB 的页和操作系统的页大小不一致,InnoDB 页大小一般为16K,操作系统页大小为 4K,InnoDB 的页写入到磁盘时,一个页需要分4次写:
如果存储引擎正在写入页的数据到磁盘时发生了宕机,可能出现页只写了一部分的情况,比如只写了4k,就宕机了,这种情况叫做部分写失效(partial page write),可能会导致数据丢失。
show variables like 'innodb_doublewrite'
我们不是有 redo log 吗?但是有个问题,如果这个页本身已经损坏了,用它来做崩溃恢复是没有意义的。所以在对于应用redolog 之前,需要一个页的副本。如果出现了写入失效,就用页的副本来还原这个页,然后再应用 redo log。这个页的副本就是 doublewrite,InnoDB 的双写技术。通过它实现了数据页的可靠性跟 redo log 一样,double write 由两部分组成,一部分是内存的 double write-个部分是磁盘上的 double write。因为 double write 是顺序写入的,不会带来很大的开销。
在默认情况下,所有的表共享一个系统表空间,这个文件会越来越大,而且它的空间不会收缩。
2.2.2 独占表空间(file-per-table tablespaces)
我们可以让每张表独占一个表空间。这个开关通过innodb file per table 设置,默认开启。
SHOW VARIABLES LIKE 'innodb_file_per_table';
开启后,则每张表会开辟一个表空间,这个文件就是数据目录下的ibd 文件(例如/var/lib/mysql/gupao/user innodb.ibd),存放表的索引和数据。
但是其他类的数据,如回滚(undo)信息,插入缓冲索引页、系统事务信息,二次写缓冲(Double write buffer)等还是存放在原来的共享表空间内。
2.2.3 通用表空间
通用表空间也是一种共享的表空间,跟ibdata1类似。
可以创建一个通用的表空间,用来存储不同数据库的表,数据路径和文件可以自定义
2.2.4 临时表空间
存储临时表的数据,包括用户创建的临时表,和磁盘的内部临时表。对应数据目录下的ibtmp1文件,当数据服务器正常关闭时,该表空间被删除,下次重新产生。
2.2.5 redo log
磁盘结构里面的redo log
2.2.5 undo表空间
undo log的数据默认在系统表空间ibdata1文件中,因为共享表空间不会自动收缩,也可以单独创建一个undo空间
2.3 后台线程
后台线程的主要作用是负责刷新内存池中的数据和把修改的数据页刷新到磁盘。启台线程分为:master thread,l0 thread,purge thread, page cleaner thread。后台线程的主要作用是负责刷新内存池中的数据和把修改的数据页刷新到磁盘。后台线程分为:master thread,l0 thread,purge thread, page cleaner thread
master thread 负责刷新缓存数据到磁盘并协调调度其它后台进程,
lO thread 分为 insert buffer、log、read、write 进程。分别用来处理 insert buffer.重做日志、读写请求的 IO 回调。
purge thread 用来回收 undo 页。
page cleaner thread 用来刷新脏页.