learn.lianglianglee.com/文章/MySQL · 引擎特性 · InnoDB Buffer Pool.md.html
2022-05-11 19:04:14 +08:00

1071 lines
68 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>MySQL · 引擎特性 · InnoDB Buffer Pool.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/文章/AQS 万字图文全面解析.md.html">AQS 万字图文全面解析.md.html</a>
</li>
<li>
<a href="/文章/Docker 镜像构建原理及源码分析.md.html">Docker 镜像构建原理及源码分析.md.html</a>
</li>
<li>
<a href="/文章/ElasticSearch 小白从入门到精通.md.html">ElasticSearch 小白从入门到精通.md.html</a>
</li>
<li>
<a href="/文章/JVM CPU Profiler技术原理及源码深度解析.md.html">JVM CPU Profiler技术原理及源码深度解析.md.html</a>
</li>
<li>
<a href="/文章/JVM 垃圾收集器.md.html">JVM 垃圾收集器.md.html</a>
</li>
<li>
<a href="/文章/JVM 面试的 30 个知识点.md.html">JVM 面试的 30 个知识点.md.html</a>
</li>
<li>
<a href="/文章/Java IO 体系、线程模型大总结.md.html">Java IO 体系、线程模型大总结.md.html</a>
</li>
<li>
<a href="/文章/Java NIO浅析.md.html">Java NIO浅析.md.html</a>
</li>
<li>
<a href="/文章/Java 面试题集锦(网络篇).md.html">Java 面试题集锦(网络篇).md.html</a>
</li>
<li>
<a href="/文章/Java-直接内存 DirectMemory 详解.md.html">Java-直接内存 DirectMemory 详解.md.html</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决.md.html">Java中9种常见的CMS GC问题分析与解决.md.html</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决.md.html">Java中9种常见的CMS GC问题分析与解决.md.html</a>
</li>
<li>
<a href="/文章/Java中的SPI.md.html">Java中的SPI.md.html</a>
</li>
<li>
<a href="/文章/Java中的ThreadLocal.md.html">Java中的ThreadLocal.md.html</a>
</li>
<li>
<a href="/文章/Java线程池实现原理及其在美团业务中的实践.md.html">Java线程池实现原理及其在美团业务中的实践.md.html</a>
</li>
<li>
<a href="/文章/Java魔法类Unsafe应用解析.md.html">Java魔法类Unsafe应用解析.md.html</a>
</li>
<li>
<a href="/文章/Kafka 源码阅读笔记.md.html">Kafka 源码阅读笔记.md.html</a>
</li>
<li>
<a href="/文章/Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html">Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html</a>
</li>
<li>
<a class="current-tab" href="/文章/MySQL · 引擎特性 · InnoDB Buffer Pool.md.html">MySQL · 引擎特性 · InnoDB Buffer Pool.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB IO子系统.md.html">MySQL · 引擎特性 · InnoDB IO子系统.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 事务系统.md.html">MySQL · 引擎特性 · InnoDB 事务系统.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 同步机制.md.html">MySQL · 引擎特性 · InnoDB 同步机制.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 数据页解析.md.html">MySQL · 引擎特性 · InnoDB 数据页解析.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB崩溃恢复.md.html">MySQL · 引擎特性 · InnoDB崩溃恢复.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · 临时表那些事儿.md.html">MySQL · 引擎特性 · 临时表那些事儿.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 半同步复制.md.html">MySQL 主从复制 半同步复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 基于GTID复制.md.html">MySQL 主从复制 基于GTID复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制.md.html">MySQL 主从复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 事务日志(redo log和undo log).md.html">MySQL 事务日志(redo log和undo log).md.html</a>
</li>
<li>
<a href="/文章/MySQL 亿级别数据迁移实战代码分享.md.html">MySQL 亿级别数据迁移实战代码分享.md.html</a>
</li>
<li>
<a href="/文章/MySQL 从一条数据说起-InnoDB行存储数据结构.md.html">MySQL 从一条数据说起-InnoDB行存储数据结构.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:事务和锁的面纱.md.html">MySQL 地基基础:事务和锁的面纱.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据字典.md.html">MySQL 地基基础:数据字典.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据库字符集.md.html">MySQL 地基基础:数据库字符集.md.html</a>
</li>
<li>
<a href="/文章/MySQL 性能优化:碎片整理.md.html">MySQL 性能优化:碎片整理.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html">MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:如何在日志中轻松定位大事务.md.html">MySQL 故障诊断:如何在日志中轻松定位大事务.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:教你快速定位加锁的 SQL.md.html">MySQL 故障诊断:教你快速定位加锁的 SQL.md.html</a>
</li>
<li>
<a href="/文章/MySQL 日志详解.md.html">MySQL 日志详解.md.html</a>
</li>
<li>
<a href="/文章/MySQL 的半同步是什么?.md.html">MySQL 的半同步是什么?.md.html</a>
</li>
<li>
<a href="/文章/MySQL中的事务和MVCC.md.html">MySQL中的事务和MVCC.md.html</a>
</li>
<li>
<a href="/文章/MySQL事务_事务隔离级别详解.md.html">MySQL事务_事务隔离级别详解.md.html</a>
</li>
<li>
<a href="/文章/MySQL优化优化 select count().md.html">MySQL优化优化 select count().md.html</a>
</li>
<li>
<a href="/文章/MySQL共享锁、排他锁、悲观锁、乐观锁.md.html">MySQL共享锁、排他锁、悲观锁、乐观锁.md.html</a>
</li>
<li>
<a href="/文章/MySQL的MVCC多版本并发控制.md.html">MySQL的MVCC多版本并发控制.md.html</a>
</li>
<li>
<a href="/文章/QingStor 对象存储架构设计及最佳实践.md.html">QingStor 对象存储架构设计及最佳实践.md.html</a>
</li>
<li>
<a href="/文章/RocketMQ 面试题集锦.md.html">RocketMQ 面试题集锦.md.html</a>
</li>
<li>
<a href="/文章/SnowFlake 雪花算法生成分布式 ID.md.html">SnowFlake 雪花算法生成分布式 ID.md.html</a>
</li>
<li>
<a href="/文章/Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html">Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html</a>
</li>
<li>
<a href="/文章/Spring Boot 教程:如何开发一个 starter.md.html">Spring Boot 教程:如何开发一个 starter.md.html</a>
</li>
<li>
<a href="/文章/Spring MVC 原理.md.html">Spring MVC 原理.md.html</a>
</li>
<li>
<a href="/文章/Spring MyBatis和Spring整合的奥秘.md.html">Spring MyBatis和Spring整合的奥秘.md.html</a>
</li>
<li>
<a href="/文章/Spring 帮助你更好的理解Spring循环依赖.md.html">Spring 帮助你更好的理解Spring循环依赖.md.html</a>
</li>
<li>
<a href="/文章/Spring 循环依赖及解决方式.md.html">Spring 循环依赖及解决方式.md.html</a>
</li>
<li>
<a href="/文章/Spring中眼花缭乱的BeanDefinition.md.html">Spring中眼花缭乱的BeanDefinition.md.html</a>
</li>
<li>
<a href="/文章/Vert.x 基础入门.md.html">Vert.x 基础入门.md.html</a>
</li>
<li>
<a href="/文章/eBay 的 Elasticsearch 性能调优实践.md.html">eBay 的 Elasticsearch 性能调优实践.md.html</a>
</li>
<li>
<a href="/文章/不可不说的Java“锁”事.md.html">不可不说的Java“锁”事.md.html</a>
</li>
<li>
<a href="/文章/互联网并发限流实战.md.html">互联网并发限流实战.md.html</a>
</li>
<li>
<a href="/文章/从ReentrantLock的实现看AQS的原理及应用.md.html">从ReentrantLock的实现看AQS的原理及应用.md.html</a>
</li>
<li>
<a href="/文章/从SpringCloud开始聊微服务架构.md.html">从SpringCloud开始聊微服务架构.md.html</a>
</li>
<li>
<a href="/文章/全面了解 JDK 线程池实现原理.md.html">全面了解 JDK 线程池实现原理.md.html</a>
</li>
<li>
<a href="/文章/分布式一致性理论与算法.md.html">分布式一致性理论与算法.md.html</a>
</li>
<li>
<a href="/文章/分布式一致性算法 Raft.md.html">分布式一致性算法 Raft.md.html</a>
</li>
<li>
<a href="/文章/分布式唯一 ID 解析.md.html">分布式唯一 ID 解析.md.html</a>
</li>
<li>
<a href="/文章/分布式链路追踪:集群管理设计.md.html">分布式链路追踪:集群管理设计.md.html</a>
</li>
<li>
<a href="/文章/动态代理种类及原理,你知道多少?.md.html">动态代理种类及原理,你知道多少?.md.html</a>
</li>
<li>
<a href="/文章/响应式架构与 RxJava 在有赞零售的实践.md.html">响应式架构与 RxJava 在有赞零售的实践.md.html</a>
</li>
<li>
<a href="/文章/大数据算法——布隆过滤器.md.html">大数据算法——布隆过滤器.md.html</a>
</li>
<li>
<a href="/文章/如何优雅地记录操作日志?.md.html">如何优雅地记录操作日志?.md.html</a>
</li>
<li>
<a href="/文章/如何设计一个亿级消息量的 IM 系统.md.html">如何设计一个亿级消息量的 IM 系统.md.html</a>
</li>
<li>
<a href="/文章/异步网络模型.md.html">异步网络模型.md.html</a>
</li>
<li>
<a href="/文章/当我们在讨论CQRS时我们在讨论些神马.md.html">当我们在讨论CQRS时我们在讨论些神马.md.html</a>
</li>
<li>
<a href="/文章/彻底理解 MySQL 的索引机制.md.html">彻底理解 MySQL 的索引机制.md.html</a>
</li>
<li>
<a href="/文章/最全的 116 道 Redis 面试题解答.md.html">最全的 116 道 Redis 面试题解答.md.html</a>
</li>
<li>
<a href="/文章/有赞权限系统(SAM).md.html">有赞权限系统(SAM).md.html</a>
</li>
<li>
<a href="/文章/有赞零售中台建设方法的探索与实践.md.html">有赞零售中台建设方法的探索与实践.md.html</a>
</li>
<li>
<a href="/文章/服务注册与发现原理剖析Eureka、Zookeeper、Nacos.md.html">服务注册与发现原理剖析Eureka、Zookeeper、Nacos.md.html</a>
</li>
<li>
<a href="/文章/深入浅出Cache.md.html">深入浅出Cache.md.html</a>
</li>
<li>
<a href="/文章/深入理解 MySQL 底层实现.md.html">深入理解 MySQL 底层实现.md.html</a>
</li>
<li>
<a href="/文章/漫画讲解 git rebase VS git merge.md.html">漫画讲解 git rebase VS git merge.md.html</a>
</li>
<li>
<a href="/文章/生成浏览器唯一稳定 ID 的探索.md.html">生成浏览器唯一稳定 ID 的探索.md.html</a>
</li>
<li>
<a href="/文章/缓存 如何保证缓存与数据库的双写一致性?.md.html">缓存 如何保证缓存与数据库的双写一致性?.md.html</a>
</li>
<li>
<a href="/文章/网易严选怎么做全链路监控的?.md.html">网易严选怎么做全链路监控的?.md.html</a>
</li>
<li>
<a href="/文章/美团万亿级 KV 存储架构与实践.md.html">美团万亿级 KV 存储架构与实践.md.html</a>
</li>
<li>
<a href="/文章/美团点评Kubernetes集群管理实践.md.html">美团点评Kubernetes集群管理实践.md.html</a>
</li>
<li>
<a href="/文章/美团百亿规模API网关服务Shepherd的设计与实现.md.html">美团百亿规模API网关服务Shepherd的设计与实现.md.html</a>
</li>
<li>
<a href="/文章/解读《阿里巴巴 Java 开发手册》背后的思考.md.html">解读《阿里巴巴 Java 开发手册》背后的思考.md.html</a>
</li>
<li>
<a href="/文章/认识 MySQL 和 Redis 的数据一致性问题.md.html">认识 MySQL 和 Redis 的数据一致性问题.md.html</a>
</li>
<li>
<a href="/文章/进阶Dockerfile 高阶使用指南及镜像优化.md.html">进阶Dockerfile 高阶使用指南及镜像优化.md.html</a>
</li>
<li>
<a href="/文章/铁总在用的高性能分布式缓存计算框架 Geode.md.html">铁总在用的高性能分布式缓存计算框架 Geode.md.html</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html</a>
</li>
<li>
<a href="/文章/面试最常被问的 Java 后端题.md.html">面试最常被问的 Java 后端题.md.html</a>
</li>
<li>
<a href="/文章/领域驱动设计在互联网业务开发中的实践.md.html">领域驱动设计在互联网业务开发中的实践.md.html</a>
</li>
<li>
<a href="/文章/领域驱动设计的菱形对称架构.md.html">领域驱动设计的菱形对称架构.md.html</a>
</li>
<li>
<a href="/文章/高效构建 Docker 镜像的最佳实践.md.html">高效构建 Docker 镜像的最佳实践.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>MySQL · 引擎特性 · InnoDB Buffer Pool</h1>
<h2>前言</h2>
<p>用户对数据库的最基本要求就是能高效的读取和存储数据但是读写数据都涉及到与低速的设备交互为了弥补两者之间的速度差异所有数据库都有缓存池用来管理相应的数据页提高数据库的效率当然也因为引入了这一中间层数据库对内存的管理变得相对比较复杂。本文主要分析MySQL Buffer Pool的相关技术以及实现原理源码基于阿里云RDS MySQL 5.6分支其中部分特性已经开源到AliSQL。Buffer Pool相关的源代码在buf目录下主要包括LRU ListFlu ListDouble write buffer, 预读预写Buffer Pool预热压缩页内存管理等模块包括头文件和IC文件一共两万行代码。</p>
<h2>基础知识</h2>
<p>**Buffer Pool Instance: ** 大小等于innodb_buffer_pool_size/innodb_buffer_pool_instances每个instance都有自己的锁信号量物理块(Buffer chunks)以及逻辑链表(下面的各种List)即各个instance之间没有竞争关系可以并发读取与写入。所有instance的物理块(Buffer chunks)在数据库启动的时候被分配直到数据库关闭内存才予以释放。当innodb_buffer_pool_size小于1GB时候innodb_buffer_pool_instances被重置为1主要是防止有太多小的instance从而导致性能问题。每个Buffer Pool Instance有一个page hash链表通过它使用space_id和page_no就能快速找到已经被读入内存的数据页而不用线性遍历LRU List去查找。注意这个hash表不是InnoDB的自适应哈希自适应哈希是为了减少Btree的扫描而page hash是为了避免扫描LRU List。
<strong>数据页:</strong> InnoDB中数据管理的最小单位为页默认是16KB页中除了存储用户数据还可以存储控制信息的数据。InnoDB IO子系统的读写最小单位也是页。如果对表进行了压缩则对应的数据页称为压缩页如果需要从压缩页中读取数据则压缩页需要先解压形成解压页解压页为16KB。压缩页的大小是在建表的时候指定目前支持16K8K4K2K1K。即使压缩页大小设为16K在blob/varchar/text的类型中也有一定好处。假设指定的压缩页大小为4K如果有个数据页无法被压缩到4K以下则需要做B-tree分裂操作这是一个比较耗时的操作。正常情况下Buffer Pool中会把压缩和解压页都缓存起来当Free List不够时按照系统当前的实际负载来决定淘汰策略。如果系统瓶颈在IO上则只驱逐解压页压缩页依然在Buffer Pool中否则解压页和压缩页都被驱逐。
*Buffer Chunks: ** 包括两部分数据页和数据页对应的控制体控制体中有指针指向数据页。Buffer Chunks是最低层的物理块在启动阶段从操作系统申请直到数据库关闭才释放。通过遍历chunks可以访问几乎所有的数据页有两种状态的数据页除外没有被解压的压缩页(BUF_BLOCK_ZIP_PAGE)以及被修改过且解压页已经被驱逐的压缩页(BUF_BLOCK_ZIP_DIRTY)。此外数据页里面不一定都存的是用户数据,开始是控制信息,比如行锁,自适应哈希等。
**逻辑链表: ** 链表节点是数据页的控制体(控制体中有指针指向真正的数据页),链表中的所有节点都有同一的属性,引入其的目的是方便管理。下面其中链表都是逻辑链表。
**Free List: ** 其上的节点都是未被使用的节点如果需要从数据库中分配新的数据页直接从上获取即可。InnoDB需要保证Free List有足够的节点提供给用户线程用否则需要从FLU List或者LRU List淘汰一定的节点。InnoDB初始化后Buffer Chunks中的所有数据页都被加入到Free List表示所有节点都可用。
**LRU List: ** 这个是InnoDB中最重要的链表。所有新读取进来的数据页都被放在上面。链表按照最近最少使用算法排序最近最少使用的节点被放在链表末尾如果Free List里面没有节点了就会从中淘汰末尾的节点。LRU List还包含没有被解压的压缩页这些压缩页刚从磁盘读取出来还没来的及被解压。LRU List被分为两部分默认前5/8为young list存储经常被使用的热点page后3/8为old list。新读入的page默认被加在old list头只有满足一定条件后才被移到young list上主要是为了预读的数据页和全表扫描污染buffer pool。
**FLU List: ** 这个链表中的所有节点都是脏页也就是说这些数据页都被修改过但是还没来得及被刷新到磁盘上。在FLU List上的页面一定在LRU List上但是反之则不成立。一个数据页可能会在不同的时刻被修改多次在数据页上记录了最老(也就是第一次)的一次修改的lsn即oldest_modification。不同数据页有不同的oldest_modificationFLU List中的节点按照oldest_modification排序链表尾是最小的也就是最早被修改的数据页当需要从FLU List中淘汰页面时候从链表尾部开始淘汰。加入FLU List需要使用flush_list_mutex保护所以能保证FLU List中节点的顺序。
**Quick List: ** 这个链表是阿里云RDS MySQL 5.6加入的使用带Hint的SQL查询语句可以把所有这个查询的用到的数据页加入到Quick List中一旦这个语句结束就把这个数据页淘汰主要作用是避免LRU List被全表扫描污染。
**Unzip LRU List: ** 这个链表中存储的数据页都是解压页,也就是说,这个数据页是从一个压缩页通过解压而来的。
**Zip Clean List: ** 这个链表只在Debug模式下有主要是存储没有被解压的压缩页。这些压缩页刚刚从磁盘读取出来还没来的及被解压一旦被解压后就从此链表中删除然后加入到Unzip LRU List中。
**Zip Free: ** 压缩页有不同的大小比如8K4KInnoDB使用了类似内存管理的伙伴系统来管理压缩页。Zip Free可以理解为由5个链表构成的一个二维数组每个链表分别存储了对应大小的内存碎片例如8K的链表里存储的都是8K的碎片如果新读入一个8K的页面首先从这个链表中查找如果有则直接返回如果没有则从16K的链表中分裂出两个8K的块一个被使用另外一个放入8K链表中。</p>
<h2>核心数据结构</h2>
<p>InnoDB Buffer Pool有三种核心的数据结构buf_pool_tbuf_block_tbuf_page_t。
**but_pool_t: ** 存储Buffer Pool Instance级别的控制信息例如整个Buffer Pool Instance的mutexinstance_no, page_hashold_list_pointer等。还存储了各种逻辑链表的链表根节点。Zip Free这个二维数组也在其中。
**buf_block_t: ** 这个就是数据页的控制体,用来描述数据页部分的信息(大部分信息在buf_page_t中)。buf_block_t中第一字段就是buf_page_t这个不是随意放的是必须放在第一字段因为只有这样buf_block_t和buf_page_t两种类型的指针可以相互转换。第二个字段是frame字段指向真正存数据的数据页。buf_block_t还存储了Unzip LRU List链表的根节点。另外一个比较重要的字段就是block级别的mutex。
**buf_page_t: ** 这个可以理解为另外一个数据页的控制体大部分的数据页信息存在其中例如space_id, page_no, page state, newest_modificationoldest_modificationaccess_time以及压缩页的所有信息等。压缩页的信息包括压缩页的大小压缩页的数据指针(真正的压缩页数据是存储在由伙伴系统分配的数据页上)。这里需要注意一点如果某个压缩页被解压了解压页的数据指针是存储在buf_block_t的frame字段里。</p>
<p>这里介绍一下buf_page_t中的state字段这个字段主要用来表示当前页的状态。一共有八种状态。这八种状态对初学者可能比较难理解尤其是前三种如果看不懂可以先跳过。
**BUF_BLOCK_POOL_WATCH: ** 这种类型的page是提供给purge线程用的。InnoDB为了实现多版本需要把之前的数据记录在undo log中如果没有读请求再需要它就可以通过purge线程删除。换句话说purge线程需要知道某些数据页是否被读取现在解法就是首先查看page hash看看这个数据页是否已经被读入如果没有读入则获取(启动时候通过malloc分配不在Buffer Chunks中)一个BUF_BLOCK_POOL_WATCH类型的哨兵数据页控制体同时加入page_hash但是没有真正的数据(buf_blokc_t::frame为空)并把其类型置为BUF_BLOCK_ZIP_PAGE(表示已经被使用了其他purge线程就不会用到这个控制体了),相关函数<code>buf_pool_watch_set</code>如果查看page hash后发现有这个数据页只需要判断控制体在内存中的地址是否属于Buffer Chunks即可如果是表示对应数据页已经被其他线程读入了相关函数<code>buf_pool_watch_occurred</code>。另一方面如果用户线程需要这个数据页先查看page hash看看是否是BUF_BLOCK_POOL_WATCH类型的数据页如果是则回收这个BUF_BLOCK_POOL_WATCH类型的数据页从Free List中(即在Buffer Chunks中)分配一个空闲的控制体,填入数据。这里的核心思想就是通过控制体在内存中的地址来确定数据页是否还在被使用。
**BUF_BLOCK_ZIP_PAGE: **当压缩页从磁盘读取出来的时候先通过malloc分配一个临时的buf_page_t然后从伙伴系统中分配出压缩页存储的空间把磁盘中读取的压缩数据存入然后把这个临时的buf_page_t标记为BUF_BLOCK_ZIP_PAGE状态(<code>buf_page_init_for_read</code>)只有当这个压缩页被解压了state字段才会被修改为BUF_BLOCK_FILE_PAGE并加入LRU List和Unzip LRU List(<code>buf_page_get_gen</code>)。如果一个压缩页对应的解压页被驱逐了但是需要保留这个压缩页且压缩页不是脏页则这个压缩页被标记为BUF_BLOCK_ZIP_PAGE(<code>buf_LRU_free_page</code>)。所以正常情况下处于BUF_BLOCK_ZIP_PAGE状态的不会很多。前述两种被标记为BUF_BLOCK_ZIP_PAGE的压缩页都在LRU List中。另外一个用法是从BUF_BLOCK_POOL_WATCH类型节点中如果被某个purge线程使用了也会被标记为BUF_BLOCK_ZIP_PAGE。
**BUF_BLOCK_ZIP_DIRTY: ** 如果一个压缩页对应的解压页被驱逐了但是需要保留这个压缩页且压缩页是脏页则被标记为BUF_BLOCK_ZIP_DIRTY(<code>buf_LRU_free_page</code>)如果该压缩页又被解压了则状态会变为BUF_BLOCK_FILE_PAGE。因此BUF_BLOCK_ZIP_DIRTY也是一个比较短暂的状态。这种类型的数据页都在Flush List中。
**BUF_BLOCK_NOT_USED: ** 当链表处于Free List中状态就为此状态。是一个能长期存在的状态。
**BUF_BLOCK_READY_FOR_USE: ** 当从Free List中获取一个空闲的数据页时状态会从BUF_BLOCK_NOT_USED变为BUF_BLOCK_READY_FOR_USE(<code>buf_LRU_get_free_block</code>),也是一个比较短暂的状态。处于这个状态的数据页不处于任何逻辑链表中。
**BUF_BLOCK_FILE_PAGE: ** 正常被使用的数据页都是这种状态。LRU List中大部分数据页都是这种状态。压缩页被解压后状态也会变成BUF_BLOCK_FILE_PAGE。
**BUF_BLOCK_MEMORY: ** Buffer Pool中的数据页不仅可以存储用户数据也可以存储一些系统信息例如InnoDB行锁自适应哈希索引以及压缩页的数据等这些数据页被标记为BUF_BLOCK_MEMORY。处于这个状态的数据页不处于任何逻辑链表中
**BUF_BLOCK_REMOVE_HASH: ** 当加入Free List之前需要先把page hash移除。因此这种状态就表示此页面page hash已经被移除但是还没被加入到Free List中是一个比较短暂的状态。
总体来说大部分数据页都处于BUF_BLOCK_NOT_USED(全部在Free List中)和BUF_BLOCK_FILE_PAGE(大部分处于LRU List中LRU List中还包含除被purge线程标记的BUF_BLOCK_ZIP_PAGE状态的数据页)状态少部分处于BUF_BLOCK_MEMORY状态极少处于其他状态。前三种状态的数据页都不在Buffer Chunks上对应的控制体都是临时分配的InnoDB把他们列为invalid state(<code>buf_block_state_valid</code>)。
如果理解了这八种状态以及其之间的转换关系那么阅读Buffer pool的代码细节就会更加游刃有余。</p>
<p>接下来简单介绍一下buf_page_t中buf_fix_count和io_fix两个变量这两个变量主要用来做并发控制减少mutex加锁的范围。当从buffer pool读取一个数据页时候会其加读锁然后递增buf_page_t::buf_fix_count同时设置buf_page_t::io_fix为BUF_IO_READ然后即可以释放读锁。后续如果其他线程在驱逐数据页(或者刷脏)的时候需要先检查一下这两个变量如果buf_page_t::buf_fix_count不为零且buf_page_t::io_fix不为BUF_IO_NONE则不允许驱逐(<code>buf_page_can_relocate</code>)。这里的技巧主要是为了减少数据页控制体上mutex的争抢而对数据页的内容读取的时候依然要加读锁修改时加写锁。</p>
<h2>Buffer Pool内存初始化</h2>
<p>Buffer Pool的内存初始化主要是Buffer Chunks的内存初始化buffer pool instance一个一个轮流初始化。核心函数为<code>buf_chunk_init</code><code>os_mem_alloc_large</code>
。阅读代码可以发现目前从操作系统分配内存有两种方式一种是通过HugeTLB的方式来分配另外一种使用传统的mmap来分配。
**HugeTLB: ** 这是一种大内存块的分配管理技术。类似数据库对数据的管理内存也按照页来管理默认的页大小为4KBHugeTLB就是把页大小提高到2M或者更加多。程序传送给cpu都是虚拟内存地址cpu必须通过快表来映射到真正的物理内存地址。快表的全集放在内存中部分热点内存页可以放在cpu cache中从而提高内存访问效率。假设cpu cache为100KB每条快表占用1KB页大小为4KB则热点内存页为100KB/1KB=100条覆盖1004KB=400KB的内存数据但是如果也默认页大小为2M则同样大小的cpu cache可以覆盖100*2M=200MB的内存数据也就是说访问200MB的数据只需要一次读取内存即可(如果映射关系没有在cache中找到则需要先把映射关系从内存中读到cache然后查找最后再去读内存中需要的数据会造成两次访问物理内存)。也就是说使用HugeTLB这种大内存技术可以提高快表的命中率从而提高访问内存的性能。当然这个技术也不是银弹内存页变大了也必定会导致更多的页内的碎片。如果需要从swap分区中加载虚拟内存也会变慢。当然最终要的理由是4KB大小的内存页已经被业界稳定使用很多年了如果没有特殊的需求不需要冒这个风险。在InnoDB中如果需要用到这项技术可以使用super-large-pages参数启动MySQL。
<strong>mmap分配</strong> 在Linux下多个进程需要共享一片内存可以使用mmap来分配和绑定所以只提供给一个MySQL进程使用也是可以的。用mmap分配的内存都是虚存在top命令中占用VIRT这一列而不是RES这一列只有相应的内存被真正使用到了才会被统计到RES中提高内存使用率。这样是为什么常常看到MySQL一启动就被分配了很多的VIRT而RES却是慢慢涨上来的原因。这里大家可能有个疑问为啥不用malloc。其实查阅malloc文档可以发现当请求的内存数量大于MMAP_THRESHOLD(默认为128KB)时候malloc底层就是调用了mmap。在InnoDB中默认使用mmap来分配。
分配完了内存,<code>buf_chunk_init</code>函数中,把这片内存划分为两个部分,前一部分是数据页控制体(buf_block_t)在阿里云RDS MySQL 5.6 release版本中每个buf_block_t是424字节一共有innodb_buffer_pool_size/UNIV_PAGE_SIZE个。后一部分是真正的数据页按照UNIV_PAGE_SIZE分隔。假设page大小为16KB则数据页控制体占的内存:数据页约等于1:38.6也就是说如果innodb_buffer_pool_size被配置为40G则需要额外的1G多空间来存数据页的控制体。
划分完空间后遍历数据页控制体设置buf_block_t::frame指针指向真正的数据页然后把这些数据页加入到Free List中即可。初始化完Buffer Chunks的内存还需要初始化BUF_BLOCK_POOL_WATCH类型的数据页控制块page hash的结构体zip hash的结构体(所有被压缩页的伙伴系统分配走的数据页面会加入到这个哈希表中)。注意这些内存是额外分配的不包含在Buffer Chunks中。
除了<code>buf_pool_init</code>外,建议读者参考一下<code>but_pool_free</code>这个内存释放函数加深对Buffer Pool相关内存的理解。</p>
<h2>Buf_page_get函数解析</h2>
<p>这个函数极其重要是其他模块获取数据页的外部接口函数。如果请求的数据页已经在Buffer Pool中了修改相应信息后就直接返回对应数据页指针如果Buffer Pool中没有相关数据页则从磁盘中读取。<code>Buf_page_get</code>是一个宏定义,真正的函数为<code>buf_page_get_gen</code>参数主要为space_id, page_no, lock_type, mode以及mtr。这里主要介绍一个mode这个参数其表示读取的方式目前支持六种前三种用的比较多。
**BUF_GET: ** 默认获取数据页的方式如果数据页不在Buffer Pool中则从磁盘读取如果已经在Buffer Pool中需要判断是否要把他加入到young list中以及判断是否需要进行线性预读。如果是读取则加读锁修改则加写锁。
**BUF_GET_IF_IN_POOL: ** 只在Buffer Pool中查找这个数据页如果在则判断是否要把它加入到young list中以及判断是否需要进行线性预读。如果不在则直接返回空。加锁方式与BUF_GET类似。
**BUF_PEEK_IF_IN_POOL: ** 与BUF_GET_IF_IN_POOL类似只是即使条件满足也不把它加入到young list中也不进行线性预读。加锁方式与BUF_GET类似。
**BUF_GET_NO_LATCH: ** 不管对数据页是读取还是修改都不加锁。其他方面与BUF_GET类似。
**BUF_GET_IF_IN_POOL_OR_WATCH: ** 只在Buffer Pool中查找这个数据页如果在则判断是否要把它加入到young list中以及判断是否需要进行线性预读。如果不在则设置watch。加锁方式与BUF_GET类似。这个是要是给purge线程用。
**BUF_GET_POSSIBLY_FREED: ** 这个mode与BUF_GET类似只是允许相应的数据页在函数执行过程中被释放主要用在估算Btree两个slot之前的数据行数。
接下来,我们简要分析一下这个函数的主要逻辑。</p>
<ul>
<li>首先通过<code>buf_pool_get</code>函数依据space_id和page_no查找指定的数据页在那个Buffer Pool Instance里面。算法很简单<code>instance_no = (space_id &lt;&lt; 20 + space_id + page_no &gt;&gt; 6) % instance_num</code>也就是说先通过space_id和page_no算出一个fold value然后按照instance的个数取余数即可。这里有个小细节page_no的第六位被砍掉这是为了保证一个extent的数据能被缓存到同一个Buffer Pool Instance中便于后面的预读操作。</li>
<li>接着,调用<code>buf_page_hash_get_low</code>函数在page hash中查找这个数据页是否已经被加载到对应的Buffer Pool Instance中如果没有找到这个数据页且mode为BUF_GET_IF_IN_POOL_OR_WATCH则设置watch数据页(<code>buf_pool_watch_set</code>)接下来如果没有找到数据页且mode为BUF_GET_IF_IN_POOL、BUF_PEEK_IF_IN_POOL或者BUF_GET_IF_IN_POOL_OR_WATCH函数直接返回空表示没有找到数据页。如果没有找到数据但是mode为其他就从磁盘中同步读取(<code>buf_read_page</code>)。在读取磁盘数据之前我们如果发现需要读取的是非压缩页则先从Free List中获取空闲的数据页如果Free List中已经没有了则需要通过刷脏来释放数据页这里的一些细节我们后续在LRU模块再分析获取到空闲的数据页后加入到LRU List中(<code>buf_page_init_for_read</code>)。在读取磁盘数据之前我们如果发现需要读取的是压缩页则临时分配一个buf_page_t用来做控制体通过伙伴系统分配到压缩页存数据的空间最后同样加入到LRU List中(<code>buf_page_init_for_read</code>)。做完这些后我们就调用IO子系统的接口同步读取页面数据如果读取数据失败我们重试100次(<code>BUF_PAGE_READ_MAX_RETRIES</code>)然后触发断言,如果成功则判断是否要进行随机预读(随机预读相关的细节我们也在预读预写模块分析)。</li>
<li>接着读取数据成功后我们需要判断读取的数据页是不是压缩页如果是的话因为从磁盘中读取的压缩页的控制体是临时分配的所以需要重新分配block(<code>buf_LRU_get_free_block</code>)把临时分配的buf_page_t给释放掉<code>buf_relocate</code>函数替换掉接着进行解压解压成功后设置state为BUF_BLOCK_FILE_PAGE最后加入Unzip LRU List中。</li>
<li>接着我们判断这个页是否是第一次访问如果是则设置buf_page_t::access_time如果不是我们则判断其是不是在Quick List中如果在Quick List中且当前事务不是加过Hint语句的事务则需要把这个数据页从Quick List删除因为这个页面被其他的语句访问到了不应该在Quick List中了。</li>
<li>接着如果mode不为BUF_PEEK_IF_IN_POOL我们需要判断是否把这个数据页移到young list中具体细节在后面LRU模块中分析。</li>
<li>接着如果mode不为BUF_GET_NO_LATCH我们给数据页加上读写锁。</li>
<li>最后如果mode不为BUF_PEEK_IF_IN_POOL且这个数据页是第一次访问则判断是否需要进行线性预读(线性预读相关的细节我们也在预读预写模块分析)。</li>
</ul>
<h2>LRU List中young list和old list的维护</h2>
<p>当LRU List链表大于512(<code>BUF_LRU_OLD_MIN_LEN</code>)时在逻辑上被分为两部分前面部分存储最热的数据页这部分链表称作young list后面部分则存储冷数据页这部分称作old list一旦Free List中没有页面了就会从冷页面中驱逐。两部分的长度由参数innodb_old_blocks_pct控制。每次加入或者驱逐一个数据页后都要调整young list和old list的长度(<code>buf_LRU_old_adjust_len</code>),同时引入<code>BUF_LRU_OLD_TOLERANCE</code>来防止链表调整过频繁。当LRU List链表小于512则只有old list。
新读取进来的页面默认被放在old list头在经过innodb_old_blocks_time后如果再次被访问了就挪到young list头上。一个数据页被读入Buffer Pool后在小于innodb_old_blocks_time的时间内被访问了很多次之后就不再被访问了这样的数据页也很快被驱逐。这个设计认为这种数据页是不健康的应该被驱逐。
此外如果一个数据页已经处于young list当它再次被访问的时候不会无条件的移动到young list头上只有当其处于young list长度的1/4(大约值)之后才会被移动到young list头部这样做的目的是减少对LRU List的修改否则每访问一个数据页就要修改链表一次效率会很低因为LRU List的根本目的是保证经常被访问的数据页不会被驱逐出去因此只需要保证这些热点数据页在头部一个可控的范围内即可。相关逻辑可以参考函数<code>buf_page_peek_if_too_old</code></p>
<h2>buf_LRU_get_free_block函数解析</h2>
<p>这个函数以及其调用的函数可以说是整个LRU模块最重要的函数在整个Buffer Pool模块中也有举足轻重的作用。如果能把这几个函数吃透相信其他函数很容易就能读懂。</p>
<ul>
<li>首先如果是使用ENGINE_NO_CACHE发送过来的SQL需要读取数据则优先从Quick List中获取(<code>buf_quick_lru_get_free</code>)。</li>
<li>接着统计Free List和LRU List的长度如果发现他们再Buffer Chunks占用太少的空间则表示太多的空间被行锁自使用哈希等内部结构给占用了一般这些都是大事务导致的。这时候会给出报警。</li>
<li>接着查看Free List中是否还有空闲的数据页(<code>buf_LRU_get_free_only</code>),如果有则直接返回,否则进入下一步。大多数情况下,这一步都能找到空闲的数据页。</li>
<li>如果Free List中已经没有空闲的数据页了则会尝试驱逐LRU List末尾的数据页。如果系统有压缩页情况就有点复杂InnoDB会调用<code>buf_LRU_evict_from_unzip_LRU</code>来决定是否驱逐压缩页如果Unzip LRU List大于LRU List的十分之一或者当前InnoDB IO压力比较大则会优先从Unzip LRU List中把解压页给驱逐否则会从LRU List中把解压页和压缩页同时驱逐。不管走哪条路径最后都调用了函数<code>buf_LRU_free_page</code>来执行驱逐操作这个函数由于要处理压缩页解压页各种情况极其复杂。大致的流程首先判断是否是脏页如果是则不驱逐否则从LRU List中把链表删除必要的话还从Unzip LRU List移走这个数据页(<code>buf_LRU_block_remove_hashed</code>)接着如果我们选择保留压缩页则需要重新创建一个压缩页控制体插入LRU List中如果是脏的压缩页还要插入到Flush List中最后才把删除的数据页插入到Free List中(<code>buf_LRU_block_free_hashed_page</code>)。</li>
<li>如果在上一步中没有找到空闲的数据页,则需要刷脏了(<code>buf_flush_single_page_from_LRU</code>)由于buf_LRU_get_free_block这个函数是在用户线程中调用的所以即使要刷脏这里也是刷一个脏页防止刷过多的脏页阻塞用户线程。</li>
<li>如果上一步的刷脏因为数据页被其他线程读取而不能刷脏则重新跳转到上述第二步。进行第二轮迭代与第一轮迭代的区别是第一轮迭代在扫描LRU List时最多只扫描innodb_lru_scan_depth个而在第二轮迭代开始扫描整个LRU List。如果很不幸这一轮还是没有找到空闲的数据页从三轮迭代开始在刷脏前等待10ms。</li>
<li>最终找到一个空闲页后page的state为BUF_BLOCK_READY_FOR_USE。</li>
</ul>
<h2>控制全表扫描不增加cache数据到Buffer Pool</h2>
<p>全表扫描对Buffer Pool的影响比较大即使有old list作用但是old list默认也占Buffer Pool的3/8。因此阿里云RDS引入新的语法ENGINE_NO_CACHE(例如SELECT ENGINE_NO_CACHE count(*) FROM t1)。如果一个SQL语句中带了ENGINE_NO_CACHE这个关键字则由它读入内存的数取据页都放入Quick List中当这个语句结束时会删除它独占的数据页。同时引入两个参数。innodb_rds_trx_own_block_max这个参数控制使用Hint的每个事物最多能拥有多少个数据页如果超过这个数据就开始驱逐自己已有的数据页防止大事务占用过多的数据页。innodb_rds_quick_lru_limit_per_instance这个参数控制每个Buffer Pool Instance中Quick List的长度如果超过这个长度后续的请求都从Quick List中驱逐数据页进而获取空闲数据页。</p>
<h2>删除指定表空间所有的数据页</h2>
<p>函数(<code>buf_LRU_remove_pages</code>)提供了三种模式,第一种(<code>BUF_REMOVE_ALL_NO_WRITE</code>)删除Buffer Pool中所有这个类型的数据页(LRU List和Flush List)同时Flush List中的数据页也不写回数据文件这种适合rename table和5.6表空间传输新特性因为space_id可能会被复用所以需要清除内存中的一切防止后续读取到错误的数据。第二种(<code>BUF_REMOVE_FLUSH_NO_WRITE</code>)仅仅删除Flush List中的数据页同时Flush List中的数据页也不写回数据文件这种适合drop table即使LRU List中还有数据页但由于不会被访问到所以会随着时间的推移而被驱逐出去。第三种(<code>BUF_REMOVE_FLUSH_WRITE</code>)不删除任何链表中的数据仅仅把Flush List中的脏页都刷回磁盘这种适合表空间关闭例如数据库正常关闭的时候调用。这里还有一点值得一提的是由于对逻辑链表的变动需要加锁且删除指定表空间数据页这个操作是一个大操作容易造成其他请求被饿死所以InnoDB做了一个小小的优化每删除BUF_LRU_DROP_SEARCH_SIZE个数据页(默认为1024)就会释放一下Buffer Pool Instance的mutex便于其他线程执行。</p>
<h2>LRU_Manager_Thread</h2>
<p>这是一个系统线程随着InnoDB启动而启动作用是定期清理出空闲的数据页(数量为innodb_LRU_scan_depth)并加入到Free List中防止用户线程去做同步刷脏影响效率。线程每隔一定时间去做BUF_FLUSH_LRU即首先尝试从LRU中驱逐部分数据页如果不够则进行刷脏从Flush List中驱逐(<code>buf_flush_LRU_tail</code>)。线程执行的频率通过以下策略计算:我们设定<code>max_free_len = innodb_LRU_scan_depth * innodb_buf_pool_instances</code>如果Free List中的数量小于max_free_len的1%则sleep time为零表示这个时候空闲页太少了需要一直执行buf_flush_LRU_tail从而腾出空闲的数据页。如果Free List中的数量介于max_free_len的1%-5%则sleep time减少50ms(默认为1000ms)如果Free List中的数量介于max_free_len的5%-20%则sleep time不变如果Free List中的数量大于max_free_len的20%则sleep time增加50ms但是最大值不超过<code>rds_cleaner_max_lru_time</code>。这是一个自适应的算法,保证在大压力下有足够用的空闲数据页(<code>lru_manager_adapt_sleep_time</code>)。</p>
<h2>Hazard Pointer</h2>
<p>在学术上Hazard Pointer是一个指针如果这个指针被一个线程所占有在它释放之前其他线程不能对他进行修改但是在InnoDB里面概念刚好相反一个线程可以随时访问Hazard Pointer但是在访问后他需要调整指针到一个有效的值便于其他线程使用。我们用Hazard Pointer来加速逆向的逻辑链表遍历。
先来说一下这个问题的背景我们知道InnoDB中可能有多个线程同时作用在Flush List上进行刷脏例如LRU_Manager_Thread和Page_Cleaner_Thread。同时为了减少锁占用的时间InnoDB在进行写盘的时候都会把之前占用的锁给释放掉。这两个因素叠加在一起导致同一个刷脏线程刷完一个数据页A就需要回到Flush List末尾(因为A之前的脏页可能被其他线程给刷走了之前的脏页可能已经不在Flush list中了)重新扫描新的可刷盘的脏页。另一方面数据页刷盘是异步操作在刷盘的过程中我们会把对应的数据页IO_FIX住防止其他线程对这个数据页进行操作。我们假设某台机器使用了非常缓慢的机械硬盘当前Flush List中所有页面都可以被刷盘(<code>buf_flush_ready_for_replace</code>返回true)。我们的某一个刷脏线程拿到队尾最后一个数据页IO fixed发送给IO线程最后再从队尾扫描寻找可刷盘的脏页。在这次扫描中它发现最后一个数据页(也就是刚刚发送到IO线程中的数据页)状态为IO fixed(磁盘很慢,还没处理完)所以不能刷跳过开始刷倒数第二个数据页同样IO fixed发送给IO线程然后再次重新扫描Flush List。它又发现尾部的两个数据页都不能刷新(因为磁盘很慢,可能还没刷完)直到扫描到倒数第三个数据页。所以存在一种极端的情况如果磁盘比较缓慢刷脏算法性能会从O(N)退化成O(N*N)。
要解决这个问题最本质的方法就是当刷完一个脏页的时候不要每次都从队尾重新扫描。我们可以使用Hazard Pointer来解决方法如下遍历找到一个可刷盘的数据页在锁释放之前调整Hazard Pointer使之指向Flush List中下一个节点注意一定要在持有锁的情况下修改。然后释放锁进行刷盘刷完盘后重新获取锁读取Hazard Pointer并设置下一个节点然后释放锁进行刷盘如此重复。当这个线程在刷盘的时候另外一个线程需要刷盘也是通过Hazard Pointer来获取可靠的节点并重置下一个有效的节点。通过这种机制保证每次读到的Hazard Pointer是一个有效的Flush List节点即使磁盘再慢刷脏算法效率依然是O(N)。
这个解法同样可以用到LRU List驱逐算法上提高驱逐的效率。相应的Patch是在MySQL 5.7上首次提出的阿里云RDS把其Port到了我们5.6的版本上,保证在大并发情况下刷脏算法的效率。</p>
<h2>Page_Cleaner_Thread</h2>
<p>这也是一个InnoDB的后台线程主要负责Flush List的刷脏避免用户线程同步刷脏页。与LRU_Manager_Thread线程相似其也是每隔一定时间去刷一次脏页。其sleep time也是自适应的(<code>page_cleaner_adapt_sleep_time</code>)主要由三个因素影响当前的lsnFlush list中的oldest_modification以及当前的同步刷脏点(<code>log_sys-&gt;max_modified_age_sync</code>有redo log的大小和数量决定)。简单的来说lsn - oldest_modification的差值与同步刷脏点差距越大sleep time就越长反之sleep time越短。此外可以通过<code>rds_page_cleaner_adaptive_sleep</code>变量关闭自适应sleep time这是sleep time固定为1秒。
与LRU_Manager_Thread每次固定执行清理innodb_LRU_scan_depth个数据页不同Page_Cleaner_Thread每次执行刷的脏页数量也是自适应的计算过程有点复杂(<code>page_cleaner_flush_pages_if_needed</code>)。其依赖当前系统中脏页的比率日志产生的速度以及几个参数。innodb_io_capacity和innodb_max_io_capacity控制每秒刷脏页的数量前者可以理解为一个soft limit后者则为hard limit。innodb_max_dirty_pages_pct_lwm和innodb_max_dirty_pages_pct_lwm控制脏页比率即InnoDB什么脏页到达多少才算多了需要加快刷脏频率了。innodb_adaptive_flushing_lwm控制需要刷新到哪个lsn。innodb_flushing_avg_loops控制系统的反应效率如果这个变量配置的比较大则系统刷脏速度反应比较迟钝表现为系统中来了很多脏页但是刷脏依然很慢如果这个变量配置很小当系统中来了很多脏页后刷脏速度在很短的时间内就可以提升上去。这个变量是为了让系统运行更加平稳起到削峰填谷的作用。相关函数<code>af_get_pct_for_dirty</code><code>af_get_pct_for_lsn</code></p>
<h2>预读和预写</h2>
<p>如果一个数据页被读入Buffer Pool其周围的数据页也有很大的概率被读入内存与其分开多次读取还不如一次都读入内存从而减少磁盘寻道时间。在官方的InnoDB中预读分两种随机预读和线性预读。
**随机预读: ** 这种预读发生在一个数据页成功读入Buffer Pool的时候(<code>buf_read_ahead_random</code>)。在一个Extent范围(1M如果数据页大小为16KB则为连续的64个数据页)内如果热点数据页大于一定数量就把整个Extend的其他所有数据页(依据page_no从低到高遍历读入)读入Buffer Pool。这里有两个问题首先数量是多少默认情况下是13个数据页。接着怎么样的页面算是热点数据页阅读代码发现只有在young list前1/4的数据页才算是热点数据页。读取数据时候使用了异步IO结合使用<code>OS_AIO_SIMULATED_WAKE_LATER</code><code>os_aio_simulated_wake_handler_threads</code>便于IO合并。随机预读可以通过参数innodb_random_read_ahead来控制开关。此外<code>buf_page_get_gen</code>函数的mode参数不影响随机预读。
**线性预读: ** 这中预读只发生在一个边界的数据页(Extend中第一个数据页或者最后一个数据页)上(<code>buf_read_ahead_linear</code>)。在一个Extend范围内如果大于一定数量(通过参数innodb_read_ahead_threshold控制默认为56)的数据页是被顺序访问(通过判断数据页access time是否为升序或者逆序来确定)的则把下一个Extend的所有数据页都读入Buffer Pool。读取的时候依然采用异步IO和IO合并策略。线性预读触发的条件比较苛刻触发操作的是边界数据页同时要求其他数据页严格按照顺序访问主要是为了解决全表扫描时的性能问题。线性预读可以通过参数<code>innodb_read_ahead_threshold</code>来控制开关。此外,当<code>buf_page_get_gen</code>函数的mode为BUF_PEEK_IF_IN_POOL时不触发线性预读。
InnoDB中除了有预读功能在刷脏页的时候也能进行预写(<code>buf_flush_try_neighbors</code>)。当一个数据页需要被写入磁盘的时候,查找其前面或者后面邻居数据页是否也是脏页且可以被刷盘(没有被IOFix且在old list中),如果可以的话,一起刷入磁盘,减少磁盘寻道时间。预写功能可以通过<code>innodb_flush_neighbors</code>参数来控制。不过在现在的SSD磁盘下这个功能可以关闭。</p>
<h2>Double Write Buffer(dblwr)</h2>
<p>服务器突然断电,这个时候如果数据页被写坏了(例如数据页中的目录信息被损坏)由于InnoDB的redolog日志不是完全的物理日志有部分是逻辑日志因此即使奔溃恢复也无法恢复到一致的状态只能依靠Double Write Buffer先恢复完整的数据页。Double Write Buffer主要是解决数据页半写的问题如果文件系统能保证写数据页是一个原子操作那么可以把这个功能关闭这个时候每个写请求直接写到对应的表空间中。
Double Write Buffer大小默认为2M即128个数据页。其中分为两部分一部分留给batch write另一部分是single page write。前者主要提供给批量刷脏的操作后者留给用户线程发起的单页刷脏操作。batch write的大小可以由参数<code>innodb_doublewrite_batch_size</code>控制例如假设innodb_doublewrite_batch_size配置为120则剩下8个数据页留给single page write。
假设我们要进行批量刷脏操作我们会首先写到内存中的Double Write Buffer(也是2M在系统初始化中分配不使用Buffer Chunks空间)如果dblwr写满了一次将其中的数据刷盘到系统表空间指定位置注意这里是同步IO操作在确保写入成功后然后使用异步IO把各个数据页写回自己的表空间由于是异步操作所有请求下发后函数就返回表示写成功了(<code>buf_dblwr_add_to_batch</code>)。不过这个时候后续的写请求依然会阻塞,知道这些异步操作都成功,才清空系统表空间上的内容,后续请求才能被继续执行。这样做的目的就是,如果在异步写回数据页的时候,系统断电,发生了数据页半写,这个时候由于系统表空间中的数据页是完整的,只要从中拷贝过来就行(<code>buf_dblwr_init_or_load_pages</code>)。
异步IO请求完成后会检查数据页的完整性以及完成change buffer相关操作接着IO helper线程会调用<code>buf_flush_write_complete</code>函数把数据页从Flush List删除如果发现batch write中所有的数据页都写成了则释放dblwr的空间。</p>
<h2>Buddy伙伴系统</h2>
<p>与内存分配管理算法类似InnoDB中的伙伴系统也是用来管理不规则大小内存分配的主要用在压缩页的数据上。前文提到过InnoDB中的压缩页可以有16K8K4K2K1K这五种大小压缩页大小的单位是表也就是说系统中可能存在很多压缩页大小不同的表。使用伙伴体统来分配和回收能提高系统的效率。
申请空间的函数是<code>buf_buddy_alloc</code>其首先在zip free链表中查看指定大小的块是否还存在如果不存在则从更大的链表中分配这回导致一些列的分裂操作。例如需要一块4K大小的内存则先从4K链表中查找如果有则直接返回没有则从8K链表中查找如果8K中还有空闲的则把8K分成两部分低地址的4K提供给用户高地址的4K插入到4K的链表中便与后续使用。如果8K中也没有空闲的了就从16K中分配16K首先分裂成2个8K高地址的插入到8K链表中低地址的8K继续分裂成2个4K低地址的4K返回给用户高地址的4K插入到4K的链表中。假设16K的链表中也没有空闲的了则调用<code>buf_LRU_get_free_block</code>获取新的数据页然后把这个数据页加入到zip hash中同时设置state状态为BUF_BLOCK_MEMORY表示这个数据页存储了压缩页的数据。
释放空间的函数是<code>buf_buddy_free</code>相比于分配空间的函数有点复杂。假设释放一个4K大小的数据块其先把4K放回4K对应的链表接着会查看其伙伴(释放块是低地址,则伙伴是高地址,释放块是高地址,则伙伴是低地址)是否也被释放了如果也被释放了则合并成8K的数据块然后继续寻找这个8K数据块的伙伴试图合并成16K的数据块。如果发现伙伴没有被释放函数并不会直接退出而是把这个伙伴给挪走(<code>buf_buddy_relocate</code>)例如8K数据块的伙伴没有被释放系统会查看8K的链表如果有空闲的8K块则把这个伙伴挪到这个空闲的8K上这样就能合并成16K的数据块了如果没有函数才放弃合并并返回。通过这种relocate操作内存碎片会比较少但是涉及到内存拷贝效率会比较低。</p>
<h2>Buffer Pool预热</h2>
<p>这个也是官方5.6提供的新功能可以把当前Buffer Pool中的数据页按照space_id和page_no dump到外部文件当数据库重启的时候Buffer Pool就可以直接恢复到关闭前的状态。
**Buffer Pool Dump: ** 遍历所有Buffer Pool Instance的LRU List对于其中的每个数据页按照space_id和page_no组成一个64位的数字写到外部文件中即可(<code>buf_dump</code>)。
**Buffer Pool Load: ** 读取指定的外部文件把所有的数据读入内存后使用归并排序对数据排序以64个数据页为单位进行IO合并然后发起一次真正的读取操作。排序的作用就是便于IO合并(<code>buf_load</code>)。</p>
<h1>总结</h1>
<p>InnoDB的Buffer Pool可以认为很简单就是LRU List和Flush List但是InnoDB对其做了很多性能上的优化例如减少加锁范围page hash加速查找等导致具体的实现细节相对比较复杂尤其是引入压缩页这个特性后有些核心代码变得晦涩难懂需要读者细细琢磨。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/文章/Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/文章/MySQL · 引擎特性 · InnoDB IO子系统.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997fd12b458b66","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>