InnoDB体系架构

InnoDB体系架构

 

前言:

    首先我们要知道MySQL的架构,了解InnoDB存储引擎在MySQL中所处的位置,才好对InnoDB做分析,结构图如下:

    从图中可知,MySQL由以下几部分组成:

  1. 连接池组件
  2. 管理服务和工具组件
  3. SQL接口组件
  4. 查询分析器组件
  5. 优化器组件
  6. 缓冲(Cache)组件
  7. 插件式存储引擎
  8. 物理文件

MySQL区别于其他数据库的最重要一个特点是:插件式的存储引擎

存储引擎是基于表的,而不是数据库。

 

InnoDB存储引擎

  InnoDB的设计目标主要是面向在线事务处理(OLTP)的应用。其特点是行锁设计、支持外键,并支持类似于Oracle的非锁定读,即默认读取操作不会产生锁。

每个表会将数据存放到一个独立的 .idb文件中。

  InnoDB通过使用多版本并发控制(MVCC)来获得高并发性,并实现了SQL标准的4中隔离级别,默认为REPEATABLE级别。通过使用一种被称为next-key-locking的策略来避免幻读(phantom)现象的产生。出此之外,InnoDB存储引擎还提供了插入缓冲(insert buffer)、二次写(double write)、自适应哈希索引(adaptive hash index)、预读(read ahead)等高性能和高可用的功能。

  对于表中的数据存储,InnoDB引擎采用了聚集(clustered)的方式,因此每张表的存储都是按主键的顺序进行存放。如果没有显示的在表定义时指定主键,InnoDB存储引擎会为每一行生成一个6字节的ROWID, 并以此作为主键和聚集索引。除了ROWID外,其实每行还有一个6字节的事务ID (DB_TRX_ID) 和一个7字节的回滚指针 (DB_ROLL_PTR) ,事务ID为当前事务的ID号,用于实现MVCC,回滚指针指向一个(包含了恢复到之前状态所需的所有信息)地址。

InnoDB 体系架构如下所示:

    InnoDB有很多个内存块,可以认为这些内存块组成了一个大的内存池,负责如下工作:

  1. 维护所有进程/线程需要访问的多个内部数据结构。
  2. 缓存磁盘上的数据,方便快速读取,同时对磁盘文件的数据修改之前在这里缓存。
  3. 重做日志(redo log)缓冲。

后台线程的主要作用是负责刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据。此外将已修改的数据文件刷新到磁盘文件,同时保证在数据库发生异常的情况下InnoDB能恢复到正常运行的状态。

 

后台线程

    InnoDB是单进程多线程的,因此后台有多个不同的后台线程,负责处理不同的任务。

  1、Master Thread

    Master是一个非常核心的后台线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的舒心、合并插入缓冲(INSERT BUFFER)、UNDO也的回收等。 

  2、IO Thread

    InnoDB中大量使用了AIO(Async IO)来处理写IO请求,这样可以大大提高数据库性能。而IO Thread 的主要工作时负责这些IO请求的回调(call back)处理。常用的有:

      I/O thread 0 state: wait Windows aio (insert buffer thread)

      I/O thread 1 state: wait Windows aio (log thread)

      I/O thread 2 state: wait Windows aio (read thread)

      I/O thread 3 state: wait Windows aio (read thread)

      I/O thread 4 state: wait Windows aio (read thread)

      I/O thread 5 state: wait Windows aio (read thread)

      I/O thread 6 state: wait Windows aio (write thread)

      I/O thread 7 state: wait Windows aio (write thread)

      I/O thread 8 state: wait Windows aio (write thread)

      I/O thread 9 state: wait Windows aio (write thread)

    通过SHOW ENGINE INNODB STATUS;命令可以查看数据库的相关信息。

  3、Purge Thread

    事务被提交后,其所使用的undolog可能不再需要,因此需要PurgeThread来回收已经使用并分配的undo页。可以开启独立的PurgeThread来减轻Master Thread的工作:

      [mysqld]

      Innodb_purge_threads=4

    可以通过SHOW VARIABLES LIKE 'innodb_purge_threads'; 来查看 purge_threads 数量

  4、Page Cleaner Thread

    将脏页的刷新操作都放入到单独的线程中来完成,同样是为了减轻Master Thread的工作压力以及对于用户查询线程的阻塞,进一步提高InnoDB的性能。

 

内存

  1、缓冲池

  InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。因此可以将其视为基于磁盘的数据库系统。

  由于CPU速度与磁盘速度之间的鸿沟,基于磁盘的数据库系统通常使用缓冲池技术来提高数据库的整体性能。 一般都是使用一块内存区域来当做缓冲池,通过内存的速度来弥补磁盘速度较慢对数据库性能的影响。

  在数据库中进行读取页的操作,首先将从磁盘读到的也存放在缓冲池中,这个过程称为将页 "FIX" 在缓冲池中。下一次再读相同的页时,首先判断该页是否在缓冲池中。若在缓冲池中,称该页在缓冲池中被命中,直接读取该页。否则,读取磁盘上的页。

  对于数据库中页的修改,首先修改缓冲池中的页,然后再以一定的频率 ( 通过Checkpoint的机制 ) 刷新到磁盘上。 因此缓冲池的大小直接影响着数据库的整体性能。

  SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; 查看缓冲池大小, Windows上默认大小为:134217728 等于128 MB

  缓冲池中缓存的数据页类型有:索引页、数据页、undo页、插入缓冲(insert buffer)、自适应哈希索引(adaptive hash index)、InnoDB存储的锁信息(lock info)、数据字典信息(data dictionary)等。

  2、LRU List、Free List 和 Flush List

  在前面我们知道了缓冲池是一个很大的内存区域,其中存放了各种类型的页。那么内存总有用尽的时候,它们是如何进行管理的呢?

  通常来说,数据库总的缓冲池是通过LRU(Latest Recent Used,最近最少使用)算法来进行管理的。即最频繁使用的页在LRU列表的前端,而最少使用的页在LRU列表的尾端。当缓冲池放不下新读取到的页时,将首先释放LRU列表中尾端的页。

  InnoDB中,缓冲池中的页大小默认为16KB,InnoDB同样使用LRU算法对缓冲池进行管理,只不过对LRU算法进行了一些优化。

  比如在LRU列表中加入了midpoint位置。新读取到的页,虽然是最新访问的页,但并不是直接放入到LRU列表的首部,而是放入到LRU列表的midpoint位置。这个算法在Innodb中被称为midpoint insertion strategy。默认配置下,该位置在LRU列表长度的5/8处。Midpoint位置可由参数innodb_old_blocks_pct控制,

  SHOW VARIABLES LIKE 'innodb_old_blocks_pct'; 查看midpoint,默认为37,表示新读取到页插入到LRU列表尾端的37%的位置(差不多3/8的位置),midpoint之后的列表称为old列表,之前的列表称为new列表,可以理解为new列表中的页都是最为活跃的热点数据。

  为什么不采用朴素的LRU算法,直接将读取到的页放入到LRU列表的首部呢?因为当进行表扫描等操作时,会产生大量的页,直接放到LRU首部会将热点数据(甚至全部)挤出去,而留下来的扫描产生的页并不是我们需要的热点数据,这样会非常影响缓冲池的效率。

  LRU解决这个问题的一部分,同时通过innodb_old_blocks_time来进一步管理LRU列表,用于表示页读取到mid位置后需要等待多久才会加入到LRU列表的热端。

    SET GLOBAL innodb_old_blocks_time=0; -- 设置mid位置的页多久可以加入到LRU,默认1000。

    SET GLOBAL innodb_old_blocks_pct=20; -- 将LRU的热端设置为80%,该项默认值为37。

  LRU用来管理已经读取到的页,但数据库刚启动时,LRU列表时空的,即没有任何页。这时页都放在Free列表中,当需要从缓冲池中分页时,首先从Free List中查找是否有可用的空闲页,若有则将该页从Free列表中删除, 放入到LRU列表中,如果Free List被用光,在根据LRU算法,淘汰LRU列表末尾的页,将内存空间分配给新的页。当页从LRU列表的old部分加入new部分时,称为page made young,而因innodb_old_blocks_time参数导致页没有从old部分移动到new部分的操作则称之为page not make young。可以通过SHOW ENGINE INNODB STATUS来观察LRU以及Free 列表的使用情况。

    BUFFER POOL AND MEMORY

    ----------------------

    Total memory allocated 549453824; in additional pool allocated 0

    Dictionary memory allocated 2976932

    Buffer pool size 32767 // 缓冲池大小

    Free buffers 4205 // 可用页的数量

    Database pages 28430 // LRU列表中页的总数量

    Old database pages 10474 // LRU中非热点数据页的数量

    Modified db pages 0 // 脏页的数量

    Pending reads 0

    Pending writes: LRU 0, flush list 0, single page 0

    Pages made young 26, not young 0

    0.00 youngs/s, 0.00 non-youngs/s

    Pages read 3675, created 24778, written 5900584

    0.00 reads/s, 0.00 creates/s, 0.00 writes/s

    Buffer pool hit rate 1000 / 1000 (缓存命中率为100%,一般不小于95%即可), 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: 28430, unzip_LRU len: 0

    I/O sum[0]:cur[0], unzip sum[0]:cur[0]

  在LRU列表中的页被修改后,该页就变成了脏页(dirty page),即缓冲池中的页和磁盘上的页的数据产生了不一致。这时数据库会通过Checkpoint机制将脏页刷新会磁盘,而Flush list中的页即为脏页列表。需要注意的是,脏页既存在于LRU列表中,也存在于Flush list列表中。LRU列表用来管理缓冲池中页的可用性,Flush列表用来管理将页刷新回磁盘,二者互不影响。

  上面的SHOW ENGINE INNODB STATUS 展示的Modified db pages 即为脏页的数量。

  3、重做日志缓冲

  从InnoDB内存数据对象的图中,可以看到InnoDB的内存区除了有缓冲池外,还有重做日志缓冲(redo log buffer),InnoDB首先将重做日志信息先放到这个缓冲区,然后按一定频率将其刷洗到重做日志文件。重做日志缓冲一般不需要

设置得很大,因为一般情况下每一秒钟会将重做日志缓冲刷新到日志文件,因此用户只需要保证每秒产生的事务量在这个缓冲大小之内即可。该值由innodb_log_buffer_size控制,默认8388608 等于 8MB。

SHOW VARIABLES LIKE 'innodb_log_buffer_size'; 可以查看该参数

    通常情况下,8MB的重做日志缓冲池足以满足绝大部分的应用,应用重做日志在下列的三种情况下会将日志缓冲中的内容刷新到重做日志文件。

    (1)、Master Thread 每一秒将重做日志缓冲刷新到重做日志文件

    (2)、每个事物提交时会将重做日志缓冲刷新到重做日志文件

    (3)、当重做日志缓冲池剩余空间小于1/2时,将缓冲刷新到重做日志文件

  1. 额外的内存池

在InnoDB中,对内存的管理是用过一种称为内存堆(heap)的方式进行的。在对一些数据结构本身的内存进行分配时,需要从额外的内存池中进行申请,当该区域的内存不够时,会从缓冲池中进行申请。

列如,分配了缓冲池(innodb_buffer_pool),但是每个缓冲池中的帧缓冲(frame buffer)还有对应的缓冲控制对象(buffer control block),这些对象记录了一些诸如LRU、锁、等待等信息,而这个对象的内存需要先从额外内存池中申请,因此在申请了很大

的InnoDB缓冲池时,也应该考虑相应的增加这个值。

 

Checkpoint机制

  前面已经讲了,缓冲池的设计目的是为了协调CPU速度与磁盘速度之间的鸿沟。因此页的操作首先都是在缓冲池中完成的。如果一条DML语句,如Update或Delete改变了页中的记录,那么此时页时脏的,即缓冲池中的页的版本要比磁盘的新。数据库需要将新版本的页从缓冲池刷新到磁盘。

  倘若每次一个页变化,就将新页的版本刷新到磁盘,那么这个开销是非常大的。若热点数据集中在某几个页中,那么数据库的性能将变得非常差。同时,如果在从缓冲池将页的新版本刷新到磁盘时发生了宕机,那么数据就不能恢复了。为了避免发生数据丢失的问题,当前事务数据库系统普遍都是用了Write Ahead Log策略,即当事务提交时,先写重做日志,在修改页。当由于发生宕机而导致数据丢失时,通过重做日志来完成数据的恢复。这也是事务ACID中的D(Durability 持久性)的要求。

  思考下面的场景,如果重做日志可以无限大,同时缓冲池也足够大,能够缓冲所有数据库的数据,那么是不需要将缓冲池中页的新版本刷新回磁盘的。因为发生宕机时,完全可以通过重做日志来恢复整个数据库系统中的数据到宕机之前的时刻。但这是有两个前提条件的:

      (1)、缓冲池可以缓存数据库中所有的数据。

      (2)、重做日志可以无限增大。

  显然这是不可能的,即使这两个条件都满足了,数据恢复时间将也是不可接受的,可能是几天也可能是几年,因为数据太多了。

  因此Checkpoint(检查点)技术的目的就是解决一下几个问题的:

    (1)、缩短数据库的恢复时间

    (2)、缓冲池不够用时,将脏页刷新到磁盘

    (3)、重做日志不可用时,刷新脏页。

  当数据库发生宕机时,数据库不需要重做所有的日志,因为Checkpoint之前的页都已经刷新回磁盘了。故数据库只需要对Checkpoint后的重做日志进行恢复即可,这样大大减少了数据库恢复的时间。

  此外,当缓冲池不够用时,根据LRU算法会溢出最近最少使用的页,若此页为脏页,那么需要强制执行Checkpoint,将脏页也就是页的新版本刷会磁盘。

  重做日志出现不可用的情况是因为当前事务数据库系统对重做日志的设计都是循环使用的,并不是让其无限增大,这从成本及管理上都是比较困难的。重做日志可以被重用的部分是指这些重做日志已经不再需要,即当数据库发生宕机时,数据库恢复时不需要这部分的重做日志,因此这部分就可以被覆盖重用。若此时重做日志还需要使用,那么必须强制产生Checkpoint,将缓冲池中的页至少刷新到当前重做日志的位置。

  对于InnoDB而言,其是通过LSN(Log Sequence Number)来标记版本的。而LSN是8字节的数字,其单位是字节。每个页有LSN,重做日志中也有LSN,Checkpoint也有LSN。可以通过SHOW ENGINE INNODB STATUS; 来查看

  LOG

  ---

  Log sequence number 5394374716

  Log flushed up to 5394374716

  Last checkpoint at 5394374716

  0 pending log writes, 0 pending chkp writes

  8991629 log i/o's done, 0.00 log i/o's/second

 

  在InnoDB中,Checkpoint发生的时间、条件、及脏页的选择等都非常复杂。而Checkpoint所做的事情无非就是将缓冲池中的脏页刷回到磁盘。不同之处在于刷新多少页到磁盘,每次从哪里去脏页,以及什么时间触发Checkpoint,在InnoDB中有两种Checkpoint,分别为:

    (1)、Sharp Checkpoint

    (2)、Fuzzy Checkpoint

    Sharp Checkpoint 发生在数据库关闭时将所有的脏页刷新回磁盘,这是工作方式,即参数 innodb_fast_shutdown=1。

    但是若数据库在运行时也使用Sharp Checkpoint,那么数据库的可用性会受到很大的影响。故在InnoDB内部使用Fuzzy Checkpoint 进行页的刷新,即只刷新一部分脏页,而不是刷新所有的脏页回磁盘。

    在InnoDB中可能发生如下几种情况的Fuzzy Checkpoint:

      (1)、Master Thread Checkpoint

      (2)、FLUSH_LRU_LIST Checkpoint

      (3)、Async/Sync Flush Checkpoint

      (4)、Dirty Page too much Checkpoint

    对于Master Thread Checkpoint,差不多以每秒或每十秒的速度从缓冲池的脏页列表中刷新一定比例的页回磁盘。这个过程是异步的,即此时InnoDB存储引擎可以进行其他的操作,用户查询线程不会阻塞。

    FLUSH_LRU_LIST Checkpoint 是因为InnoDB需要保证LRU列表中需要有差不多100个空闲页可供使用。倘若没有100个可用空闲页,那么InnoDB会将LRU列表尾部的页移除。如果这些页中有脏页,那么需要进行Checkpoint,而这些页是来自LRU列表的,因此称为FLUSH_LRU_LIST Checkpoint。在MySQL5.6之后,这个检查被放在一个单独的Page Cleaner线程中进行,并且用户可以通过参数innodb_lru_scan_depth控制LRU列表中可用页的数量,该值默认是1024。

SHOW VARIABLES LIKE 'innodb_lru_scan_depth'; 查看该值

    Asybc/Sync Flush Checkpoint 指的是重做日志文件不可用的情况,这时需要强制将一些页刷新回磁盘,而此时脏页是从脏页列表中取出来的。若将已经写入到重做日志的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

 若每个重做日志文件的大小为1GB,并且定义了两个重做日志文件,则重做日志文件的总大小为2GB。那么async_water_mark=1.5GB,sync_water_mark=1.8GB。则:

  (1)、当checkpoint_age < async_water_mark时,不需要刷新任何脏页到磁盘

  (2)、当async_water_mark < checkpoint_age < sync_water_mark 时触发Async Flush,从Flush列表中刷新足够的脏页回磁盘,使得刷新后满足checkpoint_age < async_water_mark。

  (3)、checkpoint_age > sync_water_mark 这种情况一般很少发生,除非设置的重做日志文件太小,并且在进行类似LOAD_DATA的BULK INSERT 操作。此时触发Sync Flush 操作,从Flush 列表中刷新足够的脏页回磁盘,使得刷新后满足checkpoint_age < async_water_mark。

  可见,Async/Sync Flush Checkpoint 是为了保证重做日志的循环使用的可用性。在MySQL 5.6之后这部分操作同样放到了单独的Page Cleaner Thread 中,不会阻塞用户的查询线程。

最后一种Checkpoint的情况是Dirty Page too much,即脏页的数量太多,导致InnoDB存储引擎强制进行Checkpoint。其目的总的来说还是为了保证缓冲池中有足够可用的页。其参数可以由innodb_max_dirty_pages_pct控制:

  SHOW VARIABLES LIKE 'innodb_max_dirty_pages_pct'; 可以查看该参数的值。

  innodb_max_dirty_pages_pct 的默认值为75,当缓冲池中脏页的数量占据75%是强制进行checkpoint,刷新一部分的脏页到磁盘。

 

 

以上就是InnoDB的体系架构,以及部分核心的工作原理,另附文中用到的SQL

```

  SHOW ENGINE INNODB STATUS;  -- 查看状态,包括线程等参数
  SELECT version();           -- 查看版本
  SHOW VARIABLES LIKE 'innodb_purge_threads';  -- 查看 purge_threads 数量
  SHOW VARIABLES LIKE 'innodb_buffer_pool_size';  -- 查看缓冲池大小, 134217728 为128 MB
  SHOW VARIABLES LIKE 'innodb_old_blocks_pct';   -- 查看midpoint的值
  SET GLOBAL innodb_old_blocks_time=0;  -- 设置mid位置的页多久可以加入到LRU,默认1000
  SET GLOBAL innodb_old_blocks_pct=37;  -- 将LRU的热端设置为80%
  SELECT POOL_ID, HIT_RATE, PAGES_MADE_YOUNG, PAGES_NOT_MADE_YOUNG FROM information_schema.INNODB_BUFFER_POOL_STATS;  --  直接查询内存命中率和page made young
  SHOW VARIABLES LIKE 'innodb_log_buffer_size';  -- 查看重做日志缓冲的大小
  SHOW VARIABLES LIKE 'innodb_lru_scan_depth';   -- 当LRU的可用页数量低于该值时执行checkpoint,默认1024
  SHOW VARIABLES LIKE 'innodb_max_dirty_pages_pct';  -- 脏页列表使用75%时进行checkpoint,默认为75

```

posted @ 2020-01-07 18:27  _Eternity味道  Views(377)  Comments(0Edit  收藏  举报