MySQL基本上可以分为Server层和引擎层。Server层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖了MySQL的大部分核心服务以及所有的内置函数;存储引擎层负责数据的存储和提取,其架构为插件形式,支持Innodb、MyISAM、Memory等多种存储引擎,目前OLTP系统大多数采用InnoDB,从MySQL5.5开始便作为默认存储引擎存在。
MySQL Server
连接器
连接器主要负责与客户端建立通信连接,获取权限,维持和管理连接。建立连接在完成TCP握手后会首先验证账户名密码,如果错误则会抛出Access denind for user的错误。如果用户名密码正确会验证连接用户的权限,一个用户成功建立连接后,即使更改了权限也不会影响当前会话,新建立的连接才会获取新的权限。
连接会话在没有操作的情况下会处于sleep空闲状态,当长时间处于空闲状态(默认8小时),连接器会关闭该会话,时间由参数wait_timeout控制
Q:为什么修改了wait_timeout超时时间没有发生变化? A:修改wait_timeout的同时需要修改interactive_timeout参数,这一点是根据客户端类型来决定调用哪个参数。在通过set global的方式在线修改参数后需要同步写入my.cnf,否则重启将失效。
Q:使用长连接后,MySQL内存增长太快? A:MySQL在执行过程中临时使用的内存是管理在连接对象中的,在会话断开后才会释放,大量长连接累积容易导致内存增长太快,甚至发生OOM。因此可以考虑如下两种方案:定时断开长连接;如果MySQL版本为5.7或更高,可在大事务操作后执行mysql_reset_connection来重新初始化连接资源,相当于恢复到刚连接成功时的状态。
查询缓存
通过连接器连接上数据库之后,执行一条select查询,执行逻辑会先判断查询缓存中是否存在,如果之前执行过相同SQL,会以key-value的形式放在缓存中,后续查询就能直接从查询缓存中通过key查询对应的value直接返回客户端;如果不存在则会往后执行,在执行完成后再存到缓存中。
但查询缓存失效非常频繁,只要对表进行更新,这个表上所有的查询缓存都会被清空,查询缓存命中率太低。因此,建议在生产环境关闭查询缓存或者将参数query_cache_type设置为DEMAND让SQL需要显示指定使用查询缓存,如下面这个SQL。这一功能已在8.0中删除。
|
|
分析器
在绕过查询缓存后,MySQL需要对SQL语句进行解析。首先会进行词法分析,对SQL语句各个字符串进行分析,操作类型,表,字段等等;然后再进行语法分析,判断SQL是否满足MySQL的语法要求
优化器
在解析完SQL之后,在正式执行之前还需要经过优化器的处理;比如索引的选择,表连接的顺序,生成相应的执行计划。优化器的逻辑就是寻找一个最优的执行计划,以最小的代价去执行语句。
执行器
在优化器生成执行计划之后,就正式开始执行SQL了,执行之前会先判断用户的权限,紧接着打开表,根据表的定义调用引擎提供的接口去操作数据。
INNODB存储引擎
MySQL存储引擎以插件形式提供,存储引擎是基于表的,针对不同的表可以使用不同的存储引擎。INNODB目前是MySQL默认的存储引擎,使用最为广泛。
- INNODB存储引擎支持事务,主要面向OLTP的应用,支持行锁,外键等特性。另外还有一些其它的存储引擎:
- MyISAM:MyISAM存储引擎不支持事务,采用表锁设计,并发性较差,主要面向OLAP的应用。
- NDB:NDB存储引擎是一个集群存储引擎,NDB将内存全部放在内存中,并且支持横向扩展存储节点来提供数据库性能以及高可用性。
- Federated:Federated存储引擎表并不存放数据,仅通过连接远程MySQL获取表数据,类似Oracle中的透明网关。
|
|
后台线程
- Master Thread:主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性。其任务包含脏页刷新,合并插入缓冲,undo页回收等
- IO Thread:INNODB中大量使用Async IO来处理写IO请求,提高数据库性能,IO Thread主要负责IO请求的回调处理,其中包含read和write,由参数innodb_read_io_threads和innodb_write_io_threads控制数量
- Purge Thread:事务提交之后undo log就没用了,因此需要Purge Thread来回收undo页。可以通过参数innodb_purge_threads增加purge线程数,加快回收速度
- Page Cleaner Thread:为了减轻Master Thread压力,利用Page Cleaner Thread来完成脏页刷新的功能
内存
Buffer Pool
INNODB通过划分一块共享内存区域作为数据缓存池,其中包含changer buffer,lock info、数据字典信息等,其大小由参数innodb_buffer_pool_size控制。缓冲池能够有效减少随机IO请求,提升数据库性能,数据库读取页时会将数据存放到缓冲池,如果下次读取相同的页则直接从内存返回;对数据页进行修改时,也是先修改缓冲池中的页,这就已经完成了修改,数据等后续脏页刷新到磁盘中。
INNODB支持多个缓冲池实例,每个页根据哈希值平均分配到不同缓冲池实例中,减少内部资源争用,增加并发处理能力,可以通过参数innodb_buffer_pool_instances来进行设置,默认为1,我们可以设置为8。通过show engine innodb status能够查看每个实例的情况
|
|
LRU
数据库缓冲池通过LRU(Lastest Recent Used)算法来进行页管理的,也就是最频繁使用的页在列表的前端,最少使用的页在尾端。当缓冲池没有空闲空间时,将释放最尾端的页来保存新的数据。其中新读取的页并不是直接放在列表前端,而是放在LRU中的midpoint位置,midpoint的位置由参数innodb_old_blocks_pct控制,默认37,也就是LRU列表尾端37%的位置,在midpoint之后的为old列表,之前的为new列表。为了进一步加强LRU的管理,引入参数innodb_old_blocks_time参数控制页读取到midpoint后需要多久才能加载到LRU列表前端,避免热点数据页被移除。
数据库刚启动时,LRU列表为空,页都存放在Free列表中,当需要从缓冲池分配页时,先从Free列表查找是否有空闲页,有就将该页从Free列表中删除放入LRU中,否则根据算法淘汰LRU尾端的页,将内存页分配给新的页。当页从LRU列表的old部分加入new部分时的操作为page made young,因为innodb_old_blocks_time的设置没有从old加入new的操作为page not made young。通过show engine innodb status可以观察LRU列表即FREE列表的使用情况和运行状态
|
|
从上述信息可以看出Buffer pool中有982920个页,即9829208*16K;free buffers表示当前Free列表中页的数量;Database pages表示LRU列表中页的数量;Buffer pool hit rate表示缓冲池命中率,这个值应该无限接近100%。
Innodb支持压缩页功能,可以将原本16K的页压缩为4K、8K,而由于页的大小发生了改变,LRU列表也有些改变,对于非16K的页是通过unzip_LRU进行管理的,对于不同的压缩页大小,分别管理并通过伙伴算法进行内存分配,例如要申请4K的页,其过程如下:
- 检查4K的unzip_LRU列表,检查是否有可用页
- 若有,则直接使用
- 没有,检查8K的unzip_LRU列表
- 若能得到空闲页,将页分裂成2个4K页,存放到4K的unzip_LRU列表
- 若不能得到空闲页则向上申请一个16K的页,将页分成一个8K页,2个4K页,分别存放到对应的unzip_LRU列表 我们可以通过information_schema.INNODB_BUFFER_PAGE_LRU来查看unzip_LRU中的页
在LRU中的页被修改后,就变成了脏页。这时内存中的数据和磁盘上的数据发生了不一致的情况,数据库会通过checkpoint机制将脏页刷新回磁盘,而Flush列表的页则为脏页列表。Modified db pages表示脏页数量,我们也可以通过查询information_schema.INNODB_BUFFER_PAGE_LRU中OLDEST_MODIFICATION大于0的页
重做日志缓冲
在一个高并发的OLTP系统中,如果每次DML操作都要从数据文件拉取对应记录修改再回写到数据文件中,这样产生的IO成本和时间成本都太高。因此MySQL采用了WAL(Write-Ahead Logging)机制,即先写日志,再写磁盘。InnoDB将更新记录写到redo log buffer中,然后同步刷新到重做日志文件,这个时候就已经完成了。redo log buffer的大小由参数innodb_log_buffer_size控制,默认为8MB。在下列三种情况下,会将redo log buffer中的内容刷新到redo log磁盘中
- Master Thread每一秒将重做日志缓冲刷新到重做日志文件
- 每个事务提交时会将重做日志缓冲刷新到重做日志文件
- 当重做日志缓冲池空间小于1/2时,重做日志缓冲会刷新到重做日志文件
Redo log能够在实例崩溃或异常重启时,保证已提交的记录不会丢失,这种能力称之为crash-safe。因此建议将参数innodb_flush_log_at_trx_commit设置为1,确保每个事务的redo log持久化到磁盘。
Redo log刷盘的触发条件有四种:
- redo log写满了,这时数据库无法写入,需要等待推进check point
- 内存不足,把数据加载到内存中时内存不足了会把脏页写入磁盘
- 系统空闲时间刷盘,当系统空闲时,数据库会自动进行刷盘
- 数据库正常关闭,会把脏页写入数据文件,启动时能更快
Checkpoint
Checkpoint用于解决下列几个问题:
- 缩短数据库的恢复时间。
- 缓冲池不够用时,将内存中的脏页刷新到磁盘中
- 重做日志不可用时,刷新脏页
缓冲池不够用时,根据LRU算法会溢出最近最少使用的页,若此页为脏页,那么就需要通过checkpoint先将脏页回刷到磁盘中,此时内存页就变成了干净页,可以复写了。
重做日志文件大小是固定的,由innodb_log_file_size控制大小,innodb_log_file_in_group控制数量。日志从头开始写,写满之后又从头开始循环写入。
Write pos是当前记录位置,一边写一边向后移动,写道logfile_3的末尾后就要回到文件0的开头位置;Check point是当前要擦除的位置,也是向后推动的,擦除记录前要把记录写入到数据文件中。Write pos和check point之间表示干净页,可直接写入。如果Write pos追上check point,则表示日志满了,需要先将redo写入数据文件,推进check point。
对于InnoDB而言,其是通过LSN(Log Sequence Number)来标记版本的,LSN由8个字节的数字组成,每个页有LSN,重做日志也有LSN,Checkpoint也有LSN
|
|
在Innodb引擎内部,有两种checkpoint,分别为sharp Checkpoint和Fuzzy Checkpoint。Sharp发生在数据库关闭时将所有脏页都刷新回磁盘,而Fuzzy则为每次都只刷新部分脏页,触发Fuzzy checkpoint有下面几种情况:
- Master Thread以每秒或每10秒的速度从缓冲池的脏页列表中刷新一定比例的页到磁盘中,这个过程是异步进行的。
- Innodb需要保证LRU列表中有一定的空闲页可供使用,如果不满足就需要从LRU尾端释放页,如果是脏页就需要进行checkpoint。检查由Page Cleaner线程进行,用户可以通过参数innodb_lru_scan_depth控制LRU可用页的数量,默认为1024
- 重做日志文件不可用的情况下,需要强制将一些页刷回磁盘,页是从脏页列表中选取。若将已经写入到重做日志的LSN记为redo_lsn,将已刷新回磁盘最新页的LSN记为checkpoint_lsn,则定义 checkpoint_age = redo_lsn - checkpoint_lsn 再定义以下变量: async_water_mark = 75% * total_redo_log_file_size sync_water_mark = 90% * total_redo_log_file_size 若每个重做日志文件大小为1G,有2个重做日志文件,则重做日志大小为2G,那么async_water_mark=1.5G,sync_water_mark=1.8G,则当checkpoint_age<async_water_mark时,不需要刷新任何脏页到磁盘;当async_water_mark<checkpoint_age<sync_water_mark时触发Async Flush,从脏页列表刷新足够的脏页回磁盘,使得满足checkpoint_age<async_water_mark
- 当脏页比例超过参数innodb_max_dirty_pages_pct时,强制推进checkpoint,默认为75%。当前脏页比例通过算法得到一个0-100的值,当前日志序号与checkpoint序号的差值通过算法得到一个0-100的值记,取二者其中的最大值记为R,刷脏页的速度就等于innodb_io_capacity乘以R%。当前脏页比例可通过innodb_buffer_pool_pages_dirty/innodb_buffer_pool_pages_total计算
控制脏页刷盘策略,首先要告诉Innodb磁盘IO的能力,刷脏页的时候能有多快,通过参数innodb_io_capacity来设置,建议设置为磁盘的IOPS。可以通过fio工具来测试磁盘IOPS
|
|
在准备刷一个脏页时,如果数据页的邻居也是脏页,则一同写入磁盘,这个过程会一直延续下去,类似于连坐机制。这种行为是由参数innodb_flush_neighbors控制,为1则开启,为0则禁止,为2则只刷新连续的脏页。在使用SSD或者IOPS比较高的情况下,IOPS已经不存在性能瓶颈情况下,可以设置为0,并且8.0中也将这一参数默认设置为0
double write
doublewrite确保了Innodb存储引擎的数据页的可靠性。当数据库宕机时,可能存在部分写失效问题,也就是一个数据页只写了一部分就发生了宕机,这种情况页已经损坏,是无法利用redo日志进行恢复的。doublewrite的解决这种问题的方法就是在应用重做日志前,对页复制出一个副本,当写入失效发生时,先通过页的副本来还原页,再进行重做。
doublewrite由两部分组成,一部分是在内存中分配的doublewrite buffer,大小为2MB,另一部分就是共享表空间中连续的128个页,即2个区,大小也为2M。在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是通过memcpy函数将脏页先复制到doublewrite buffer,之后通过doublewrite buffer分两次,每次1MB顺序写入共享表空间,然后调用fsync函数同步磁盘。在完成double write页的写入后,再将doublewrite buffer中的页写入各个表空间,此时是随机的。通过下列命令观察doublewrite的情况
|
|
Innodb_dblwr_pages_written:Innodb_dblwr_writes远小于64:1,说明系统写入压力并不是很高
changer buffer
在进行插入操作时,数据页的存放是按照主键进行顺序存放的,对于非主键二级索引叶子节点的插入不再是顺序的,这时就会造成随机IO,导致插入性能降低。为此,InnoDB引入了Insert buffer,对于非聚集索引的插入,不是直接插入索引页中,而是先判断插入的非聚集所以是否在缓冲池,若在的话直接插入;若不在则先放入到Insert buffer中,后续再以一定的频率和情况进行Insert buffer和二级索引叶子节点的merge合并操作,提升了非聚集索引的插入性能。
changer buffer是对insert buffer的升级,在insert的基础上增加了delete、update的支持。在更新数据页时,如果数据页在内存中则直接更新,如果不在内存则在不影响一致性的前提下将操作记录在change buffer中,待下次访问数据页再从磁盘读取到内存中并执行change buffer,这个过程称之为merge,后台线程会定期进行merge,正常关闭数据库也会执行merge。
change buffer是可持久化的,既写入内存,也会写入磁盘。change buffer使用的是buffer pool的内存,由参数innodb_change_buffer_max_size控制,默认为25,最大可以设置为50则表示能占用50%的buffer pool。
|
|
seg size显示了当前Insert buffer的大小为2*16KB,free list len表示空闲列表的长度,size表示已经合并记录页的数量,merges表示合并的次数。insert表示insert buffer,delete mark表示delete buffer,delete表示purge buffer,discarded operations表示merge时表已删除,无需再进行merge了。
唯一索引必须将数据页读到内存中才能做判断,因此唯一索引无法使用change buffer或insert buffer,只能针对普通二级索引。在插入完后立马进行读的场景下,会频繁进行merge,增加change buffer的维护工作,建议通过参数innodb_change_buffering关闭change buffer,其它场景都建议通过普通索引配合change buffer使用,减少磁盘IO。
自适应哈希
hash是一种非常快的查询方法,一般情况下时间复杂度未O(1),而B+树的查找次数取决于B+树的高度。InnoDB会监控表上各索引页的查询,如果观察到建立哈希索引可以带来速度提升,则建立哈希索引,这种机制称之为自适应哈希索引(AHI),AHI是通过缓冲池的B+树页构造而来,因此建立的速度很快,而不需要对整张表结构建立哈希索引。InnoDB会根据访问的频率和模式来自动为某些热点页建立哈希索引,哈希索引仅适用于等值查询。
AHI要求页的连续访问模式是一样的,例如对于复合索引(a,b),其访问模式可能是a单独作为条件或者ab同时作为条件,如果两种模式交叉访问就不会对该页构造AHI。此外AHI还要求以该模式访问100次,页通过该模式访问N次,其中N等于页中记录*1/16
启用AHI后,读取和写入速度都得到了提升,其是数据库自动优化的,无需人工干预。我们可以通过show engine来查看AHI的使用情况
|
|
InnoDB关闭行为
在实例关闭时,参数innodb_fast_shutdown影响着表的存储引擎为InnoDB的行为,参数取值范围为0、1、2,默认值为1。
- 0表示MySQL关闭时,InnoDB需要完成所有的full purge和merge changer buffer,并将所有的脏页刷新回磁盘。在升级时,必须将这个参数调为0,然后再关闭数据库
- 1表示不需要完成上述full purge和merge changer buffer,但还是会刷新部分脏页
- 2表示不完成full purge和merge changer buffer操作,也不将缓冲池脏页刷新,而是将日志全部写入日志文件,在下次启动时会进行recovery操作
参数innodb_force_recovery影响了整个InnoDB恢复的情况,某些情况下用户自己知道如何进行恢复,例如在ALTER TABLE时出现异常,InnoDB会进行回滚操作,对于大表非常耗时,用户可以通过自己操作更快的恢复。参数可选值范围为0-6
- 0为默认值,当需要恢复时,进行所有的恢复操作,如果恢复失败则关闭实例并将错误写入错误日志
- 1表示忽略检查到的corrupt页
- 2表示阻止MT线程的运行,如果MT线程需要执行full purge,则会发生crash
- 3表示不进行事务的回滚操作
- 4表示不进行插入缓冲的合并操作
- 5表示不查看undo log,所有未提交的事务都会视为已提交
- 6表示不进行前滚操作
当innodb_force_recovery大于0时,用户可以对表执行select、create和drop,但不能执行DML操作