今日笔记:“自我完整”的人,不管遇到什么挫折,都能维持一整感觉--我在这儿,我相信自己有能力面对生活的挑战。如果出现挫折,能客观对待,既不归罪别人,也不容易怪罪自己,并且懂的安抚自己的挫败感,同时去寻找资源帮助自己。
相反,“未形成自我或自我破碎”的人,遇到挫折,就会觉得“我”被瓦解了,挫败感非常强烈,需要归罪,甩给其他人。
一、前言背景
二、bufferpool是什么?
2.1 数据在bufferpool 是如何存取的?一行一行数据来加载的吗?
2.2 怎么知道数据页是否在缓存池?
三、bufferpool如何管理这些数据页?
3.1 free链表-可用数据页
3.2 flush链表-脏数据页
3.3 free链表可用数据页不足,如何淘汰缓存页?
3.4 LRU链表-淘汰数据页
3.4.1 InnoDB的预读机制
3.4.2 传统LRU链表
3.4.3 改进型LRU链表-1分为2冷热分离设计
3.5 缓存数据页刷盘机制
一、前言背景
通过前面7篇系列文章,我们已经初步了解MySQL整体架构、三大日志法宝、事务隔离级别、锁、mvcc机制、索引优化等相关领域,然而鲜有人知但非常重要的bufferpool同样值得深入探讨学习。InnoDB高效的读写表现,bufferpool提供内存级别的读写管理能力,功不可没。
本文通过bufferpool的free链表、flush链表、lru链表来详细分析缓存池数据页加载、淘汰、刷盘等多个核心机制,希望对大家理解bufferpool运行原理有些许帮助。
二、bufferpool是什么?
作为MySQL高并发读写核心模块,buffer pool是一块基于内存的数据缓存池,默认大小是128Mb。
show variables like '%buffer_pool%';
如果MySQL服务器内存够大,比如16G、32G,可以考虑适当调整buffer pool大小到2G、4G,充分发挥服务高配置的优势,有效提升MySQL读写性能。
bufferpool的内存区域是一片连续的内存区域,里面就是按数据页来划分管理,可以把它想象为一个个16kb大小的数据块拼装成的128Mb 的缓存池。数据库数据逻辑概念,数据库是库、表、行来划分,习惯性的认为数据读写是一行一行的读取更新。实际上,在MySQL数据存储读写最小单元是【数据页】。
每个数据页大小是16Kb,通过命令查询innodb_page_size参数:show variables like '%page_size%';
这个参数不能在运行时修改,上线运行后不应该再次修改,影响非常大。
此外,MySQL的InnoDB存储引擎,支持bufferpool多实例配置,可以提升并发读写性能。
2.1 数据在bufferpool 是如何存取的?一行一行数据来加载的吗?
数据页在内存划分是紧密相连的,可以有效避免内存碎片。每个数据页里除了存放真实数据外,还有数据页号、所属表空间、在缓存池的地址等描述信息。
此外,在磁盘里也是以一个个数据页为最小单元去存取数据,当数据页被用到的时候,就会被加载到缓存池。
2.2 怎么知道数据页是否在缓存池?
当MySQL需要查询一个数据时,需要先判断该数据对应的数据页是否存在bufferpool。
MySQL有个数据页缓存记录hash表,key=表空间号+数据页号,value是=数据页地址。如果该记录表能找到对应数据页key,说明已缓存到bufferpool。否则就需要从磁盘里加载数据页到bufferpool里。
三、bufferpool如何管理这些数据页?
3.1 free链表-可用数据页
为了记录每个数据页是否空闲,bufferpool设计了一个free链表-双向链表,来记录当前缓存池空闲的数据页。如果某个数据页加载存放了数据,该数据页的描述信息就会从free链表移除。
3.2 flush链表-脏数据页
MySQL给bufferpool缓存池维护一个flush链表,用来记录哪些数据页已经被修改过,需要刷到磁盘的脏数据页。如果某个被数据页被修改过,该数据页的描述信息被加入flush链表的节点。flush链表和free链表几乎是一样的,里面主要存放的也是数据页的描述信息,非常轻巧,没有多少数据冗余。
3.3 free链表可用数据页不足,如何淘汰缓存页?
随着时间推移,bufferpool加载进来的数据页越来越多,最终free链表无可用空闲数据页存放新加载数据。此时需要对缓存里的数据页进行筛选淘汰那些命中率低、缓存价值低的数据页。而缓存命中价值通过另一个链表-LRU链表来记录判断。
3.4 改进型LRU链表-淘汰数据页
LRU,全称是least recently used最近最少使用。LRU链表也是双向链表,和free、flush链表类似,里面也是数据页的描述信息。那InnoDB的改进型LRU链表如何设计,和传统的LRU算法有什么区别?我们先了解read ahead预读机制,和传统LRU链表。
3.4.1 InnoDB的预读机制
InnoDB read ahead预读机制:InnoDB在执行加载某些数据页的时候,认为后期会读到某些数据页,就预先把部分非目标数据页一起加载到bufferpool中。预读机制有2种子类型:线性预读和随机预读。
线性预读:参数innodb_read_ahead_threshold=56,这个参数含义是当顺序加载一个数据区(extent)数据页数量大于56个时,就会把相邻数据区(extent)中的全部数据页预读进来。
随机预读:参数innodb_random_read_ahead是关闭的。如果打开该参数为ON,当在一个数据区随机加载了13个数据页,就直接把该数据区其他数据页全部加载到bufferpool。
综上所述,InnoDB默认情况下,只会触发线性预读。也就是当顺序访问一个区多个数据页,数量大于56,就会把下一个相邻数据区全部数据页加载进bufferpool。这种情况,就让bufferpool里一下子多了很多可能不会被用到的数据页。
比如,某些SQL触发了全表扫描,bufferpool就被迫加载了全表数据到bufferpool,bufferpool可用空闲数据页很快就会被占满,淘汰数据页的考虑就迫在眉睫。
3.4.2 传统LRU链表
如果按普通LRU最近最少使用链表设计,当bufferpool加载一个数据页进来时,LRU链表就会把该数据页描述信息放置到LRU链表头部节点,后续如果查询命中、或者修改了该数据页,该数据页描述信息也会被挪到LRU链表头部。这样LRU链表的尾部节点,一定是最近最少被使用的节点。淘汰数据页就从LRU尾部进行淘汰。
但是刚才InnoDB 预读机制来看,传统简单的LUR链表,之前被频繁访问修改的数据页,很可能被预加载进来的其他没有价值的数据页一下子排挤到LRU链表的尾部。比如数据页C本来是LRU链表头部节点,最近1min被访问了100次,但是由于最近一次全表扫描、或者触发了线性预读,下一个数据区的不需要的数据页B等多个数据页被加载进来,数据页C直接被LRU挪到尾部,面临被淘汰的风险。这个传统简单LRU设计,在预读机制影响下,并不合理公平,需要改进。
3.4.3 改进型LRU链表-1分为2冷热分离设计
bufferpool的LRU链表是经过优化改进的链表,它的设计和优化思路值得参考学习。具体是:LRU链表,一分为二,有热数据区+冷数据区组成链表。
冷热数据占比情况,通过参数innodb_old_blocks_pct配置,默认冷数据占比37%,热数据63%,大概是46开占比。
当数据页首次被加载到bufferpool后,不是直接放到LRU链表头部,而是放在LRU链路冷数据区部分的头部。
此外,数据页描述信息节点进入冷数据区头部后,需要经过innodb_old_blocks_time(默认是1000ms)后,也就是1s后,如果有被再次访问,才能被挪到热数据区的头部,也就是整个LRU链表的头部。
这个LRU链表冷热分离,以及数据页从冷数据到成为真正热数据的规则的设计,可以有效避免预读机制对LRU淘汰公平性破坏。
最后,MySQL在热数据区里也继续做了优化,在LRU热数据区只有后75%的数据页被访问了,才会挪到热数据区头部。前面25%被访问的数据页不会再次修改LRU链表,这样可以有效避免LRU链表因为那些top级火热的数据,频繁访问引起LRU节点变化影响性能。
3.5 缓存数据页刷盘机制
当bufferpool存满后,free链表没有可用空闲数据页,必须要刷盘。当然也不是一定要等到bufferpool没有可用数据页空间才刷盘。MySQL有个定时线程专门负责将LRU链表尾部冷数据以及flush链表脏数据刷盘。该线程会定时把lru链表尾部未被修改的数据直接释放掉,给free链表增加可用数据页。也会从flush链表找那些已经被修改过的脏数据页刷到磁盘,然后增加到free链表。
这里有个参数innodb_lru_scan_depth,用来设置LRU列表中可用页的数量,默认是1024。当lru可用数据页节点数量小于1024时,会发生checkpoint,需要移除LRU列表冷数据区部分数据页。
综上,bufferpool核心运行原理大致如下:当bufferpool加载一个数据页后,free链表移除该数据页描述信息节点,而lru链表会在冷数据区头部新增该数据页描述信息节点。当该数据页被修改后,flush链表会新增该数据页描述信息节点,此外lru链表会把该数据页从冷数据区移动到热数据区,或者在热数据区后75%的位置挪到lru热数据区头部位置。当触发刷盘策略后,lru链表的冷数据或者flush表对应的脏数据页,会被优先刷盘或者从对应链表移除释放,提供新的free可用数据页节点。