共计 4503 个字符,预计需要花费 12 分钟才能阅读完成。
这篇文章主要讲解了“MySQL 性能优化 InnoDB buffer pool flush 分析”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着丸趣 TV 小编的思路慢慢深入,一起来研究和学习“MySQL 性能优化 InnoDB buffer pool flush 分析”吧!
背景
我们知道 InnoDB 使用 buffer pool 来缓存从磁盘读取到内存的数据页。buffer pool 通常由数个内存块加上一组控制结构体对象组成。内存块的个数取决于 buffer pool instance 的个数,不过在 5.7 版本中开始默认以 128M(可配置)的 chunk 单位分配内存块,这样做的目的是为了支持 buffer pool 的在线动态调整大小。
Buffer pool 的每个内存块通过 mmap 的方式分配内存,因此你会发现,在实例启动时虚存很高,而物理内存很低。这些大片的内存块又按照 16KB 划分为多个 frame,用于存储数据页。
虽然大多数情况下 buffer pool 是以 16KB 来存储数据页,但有一种例外:使用压缩表时,需要在内存中同时存储压缩页和解压页,对于压缩页,使用 Binary buddy allocator 算法来分配内存空间。例如我们读入一个 8KB 的压缩页,就从 buffer pool 中取一个 16KB 的 block,取其中 8KB,剩下的 8KB 放到空闲链表上;如果紧跟着另外一个 4KB 的压缩页读入内存,就可以从这 8KB 中分裂 4KB,同时将剩下的 4KB 放到空闲链表上。
为了管理 buffer pool,每个 buffer pool instance 使用如下几个链表来管理:
LRU 链表包含所有读入内存的数据页;
Flush_list 包含被修改过的脏页;
unzip_LRU 包含所有解压页;
Free list 上存放当前空闲的 block。
另外为了避免查询数据页时扫描 LRU,还为每个 buffer pool instance 维护了一个 page hash,通过 space id 和 page no 可以直接找到对应的 page。
一般情况下,当我们需要读入一个 Page 时,首先根据 space id 和 page no 找到对应的 buffer pool instance。然后查询 page hash,如果 page hash 中没有,则表示需要从磁盘读取。在读盘前首先我们需要为即将读入内存的数据页分配一个空闲的 block。当 free list 上存在空闲的 block 时,可以直接从 free list 上摘取;如果没有,就需要从 unzip_lru 或者 lru 上驱逐 page。
这里需要遵循一定的原则(参考函数 buf_LRU_scan_and_free_block , 5.7.5):
首先尝试从 unzip_lru 上驱逐解压页;
如果没有,再尝试从 Lru 链表上驱逐 Page;
如果还是无法从 Lru 上获取到空闲 block,用户线程就会参与刷脏,尝试做一次 SINGLE PAGE FLUSH,单独从 Lru 上刷掉一个脏页,然后再重试。
Buffer pool 中的 page 被修改后,不是立刻写入磁盘,而是由后台线程定时写入,和大多数数据库系统一样,脏页的写盘遵循日志先行 WAL 原则,因此在每个 block 上都记录了一个最近被修改时的 Lsn,写数据页时需要确保当前写入日志文件的 redo 不低于这个 Lsn。
然而基于 WAL 原则的刷脏策略可能带来一个问题:当数据库的写入负载过高时,产生 redo log 的速度极快,redo log 可能很快到达同步 checkpoint 点。这时候需要进行刷脏来推进 Lsn。由于这种行为是由用户线程在检查到 redo log 空间不够时触发,大量用户线程将可能陷入到这段低效的逻辑中,产生一个明显的性能拐点。
Page Cleaner 线程
在 MySQL5.6 中,开启了一个独立的 page cleaner 线程来进行刷 lru list 和 flush list。默认每隔一秒运行一次,5.6 版本里提供了一大堆的参数来控制 page cleaner 的 flush 行为,包括:
innodb_adaptive_flushing_lwm,
innodb_max_dirty_pages_pct_lwm
innodb_flushing_avg_loops
innodb_io_capacity_max
innodb_lru_scan_depth
这里我们不一一介绍,总的来说,如果你发现 redo log 推进的非常快,为了避免用户线程陷入刷脏,可以通过调大 innodb_io_capacity_max 来解决,该参数限制了每秒刷新的脏页上限,调大该值可以增加 Page cleaner 线程每秒的工作量。如果你发现你的系统中 free list 不足,总是需要驱逐脏页来获取空闲的 block 时,可以适当调大 innodb_lru_scan_depth。该参数表示从每个 buffer pool instance 的 lru 上扫描的深度,调大该值有助于多释放些空闲页,避免用户线程去做 single page flush。
为了提升扩展性和刷脏效率,在 5.7.4 版本里引入了多个 page cleaner 线程,从而达到并行刷脏的效果。目前 Page cleaner 并未和 buffer pool 绑定,其模型为一个协调线程 + 多个工作线程,协调线程本身也是工作线程。因此如果 innodb_page_cleaners 设置为 4,那么就是一个协调线程,加 3 个工作线程,工作方式为生产者 - 消费者。工作队列长度为 buffer pool instance 的个数,使用一个全局 slot 数组表示。
协调线程在决定了需要 flush 的 page 数和 lsn_limit 后,会设置 slot 数组,将其中每个 slot 的状态设置为 PAGE_CLEANER_STATE_REQUESTED, 并设置目标 page 数及 lsn_limit,然后唤醒工作线程 (pc_request)
工作线程被唤醒后,从 slot 数组中取一个未被占用的 slot,修改其状态,表示已被调度,然后对该 slot 所对应的 buffer pool instance 进行操作。直到所有的 slot 都被消费完后,才进入下一轮。通过这种方式,多个 page cleaner 线程实现了并发 flush buffer pool,从而提升 flush dirty page/lru 的效率。
MySQL5.7 的 InnoDB flush 策略优化
在之前版本中,因为可能同时有多个线程操作 buffer pool 刷 page(在刷脏时会释放 buffer pool mutex),每次刷完一个 page 后需要回溯到链表尾部,使得扫描 bp 链表的时间复杂度最差为 O(N*N)。
在 5.6 版本中针对 Flush list 的扫描做了一定的修复,使用一个指针来记录当前正在 flush 的 page,待 flush 操作完成后,再看一下这个指针有没有被别的线程修改掉,如果被修改了,就回溯到链表尾部,否则无需回溯。但这个修复并不完整,在最差的情况下,时间复杂度依旧不理想。
因此在 5.7 版本中对这个问题进行了彻底的修复,使用多个名为 hazard pointer 的指针,在需要扫描 LIST 时,存储下一个即将扫描的目标 page,根据不同的目的分为几类:
flush_hp: 用作批量刷 FLUSH LIST
lru_hp: 用作批量刷 LRU LIST
lru_scan_itr: 用于从 LRU 链表上驱逐一个可替换的 page,总是从上一次扫描结束的位置开始,而不是 LRU 尾部
single_scan_itr: 当 buffer pool 中没有空闲 block 时,用户线程会从 FLUSH LIST 上单独驱逐一个可替换的 page 或者 flush 一个脏页,总是从上一次扫描结束的位置开始,而不是 LRU 尾部。
后两类的 hp 都是由用户线程在尝试获取空闲 block 时调用,只有在推进到某个 buf_page_t::old 被设置成 true 的 page (大约从 Lru 链表尾部起至总长度的八分之三位置的 page) 时,再将指针重置到 Lru 尾部。
这些指针在初始化 buffer pool 时分配,每个 buffer pool instance 都拥有自己的 hp 指针。当某个线程对 buffer pool 中的 page 进行操作时,例如需要从 LRU 中移除 Page 时,如果当前的 page 被设置为 hp,就要将 hp 更新为当前 Page 的前一个 page。当完成当前 page 的 flush 操作后,直接使用 hp 中存储的 page 指针进行下一轮 flush。
社区优化
一如既往的,Percona Server 在 5.6 版本中针对 buffer pool flush 做了不少的优化,主要的修改包括如下几点:
优化刷 LRU 流程 buf_flush_LRU_tail
该函数由 page cleaner 线程调用。
原生的逻辑:依次 flush 每个 buffer pool instance,每次扫描的深度通过参数 innodb_lru_scan_depth 来配置。而在每个 instance 内,又分成多个 chunk 来调用;
修改后的逻辑为:每次 flush 一个 buffer pool 的 LRU 时,只刷一个 chunk,然后再下一个 instance,刷完所有 instnace 后,再回到前面再刷一个 chunk。简而言之,把集中的 flush 操作进行了分散,其目的是分散压力,避免对某个 instance 的集中操作,给予其他线程更多访问 buffer pool 的机会。
允许设定刷 LRU/FLUSH LIST 的超时时间,防止 flush 操作时间过长导致别的线程(例如尝试做 single page flush 的用户线程)stall 住;当到达超时时间时,page cleaner 线程退出 flush。
避免用户线程参与刷 buffer pool
当用户线程参与刷 buffer pool 时,由于线程数的不可控,将产生严重的竞争开销,例如 free list 不足时做 single page flush,以及在 redo 空间不足时,做 dirty page flush,都会严重影响性能。Percona Server 允许选择让 page cleaner 线程来做这些工作,用户线程只需要等待即可。出于效率考虑,用户还可以设置 page cleaner 线程的 cpu 调度优先级。
另外在 Page cleaner 线程经过优化后,可以知道系统当前处于同步刷新状态,可以去做更激烈的刷脏 (furious flush),用户线程参与到其中,可能只会起到反作用。
允许设置 page cleaner 线程,purge 线程,io 线程,master 线程的 CPU 调度优先级,并优先获得 InnoDB 的 mutex。
使用新的独立后台线程来刷 buffer pool 的 LRU 链表,将这部分工作负担从 page cleaner 线程剥离。
实际上就是直接转移刷 LRU 的代码到独立线程了。从之前 Percona 的版本来看,都是在不断的强化后台线程,让用户线程少参与到刷脏 /checkpoint 这类耗时操作中。
感谢各位的阅读,以上就是“MySQL 性能优化 InnoDB buffer pool flush 分析”的内容了,经过本文的学习后,相信大家对 MySQL 性能优化 InnoDB buffer pool flush 分析这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是丸趣 TV,丸趣 TV 小编将为大家推送更多相关知识点的文章,欢迎关注!