MySQL · 源码分析 · InnoDB LRU List刷脏改进之路

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS PostgreSQL,集群系列 2核4GB
简介:

之前的一篇内核月报MySQL · 引擎特性 · InnoDB Buffer Pool 中对InnoDB Buffer pool的整体进行了详细的介绍。文章已经提到了LRU List以及刷脏的工作原理。本篇文章着重从MySQL 5.7源码层面对LRU List刷脏的工作原理,以及Percona针对MySQL LRU Flush的一些性能问题所做的改进,进行一下分析。

在MySQL中,如果当前数据库需要操作的数据集比Buffer pool中的空闲页面大的话,当前Buffer pool中的数据页就必须进行脏页淘汰,以便腾出足够的空闲页面供当前的查询使用。如果数据库负载太高,对于空闲页面的需求超出了page cleaner的淘汰能力,这时候是否能够快速获取空闲页面,会直接影响到数据库的处理能力。我们将从下面三个阶段来看一下MySQL以及Percona对LRU List刷脏的改进过程。

众所周知,MySQL操作任何一个数据页面都需要读到Buffer pool进行才会进行操作。所以任何一个读写请求都需要从Buffer pool来获取所需页面。如果需要的页面已经存在于Buffer pool,那么直接利用当前页面进行操作就行。但是如果所需页面不在Buffer pool,比如UPDATE操作,那么就需要从Buffer pool中新申请空闲页面,将需要读取的数据放到Buffer pool中进行操作。那么官方MySQL 5.7.4之前的版本如何从buffer pool中获取一个页面呢?请看如下代码段:


buf_block_t*
buf_LRU_get_free_block(
/*===================*/
 buf_pool_t* buf_pool) /*!< in/out: buffer pool instance */
{
 buf_block_t* block = NULL;
 bool freed = false;
 ulint n_iterations = 0; 
 ulint flush_failures = 0; 
 bool mon_value_was = false;
 bool started_monitor = false;

 MONITOR_INC(MONITOR_LRU_GET_FREE_SEARCH);
loop:
 buf_pool_mutex_enter(buf_pool); // 这里需要对当前buf_pool使用mutex,存在锁竞争 // 当前函数会检查一些非数据对象,比如AHI, lock 所占用的buf_pool是否太高并发出警告
 buf_LRU_check_size_of_non_data_objects(buf_pool);

 /* If there is a block in the free list, take it */
 block = buf_LRU_get_free_only(buf_pool);

 // 如果获取到了空闲页面,清零之后就直接使用。否则就需要进行LRU页面淘汰 if (block != NULL) {

 buf_pool_mutex_exit(buf_pool);
 ut_ad(buf_pool_from_block(block) == buf_pool);
 memset(&block->page.zip, 0, sizeof block->page.zip);

 if (started_monitor) {
 srv_print_innodb_monitor =
 static_cast<my_bool>(mon_value_was);
 }

 block->skip_flush_check = false;
 block->page.flush_observer = NULL;
 return(block);
 }

 MONITOR_INC( MONITOR_LRU_GET_FREE_LOOPS );

 freed = false;
 /**
 这里会重复进行空闲页扫描,如果没有空闲页面,会根据LRU list对页面进行淘汰。
 这里设置buf_pool->try_LRU_scan是做了一个优化,如果当前用户线程扫描的时候
 发现没有空闲页面,那么其他用户线程就不需要进行同样的扫描。
 */ if (buf_pool->try_LRU_scan || n_iterations > 0) {
 /* If no block was in the free list, search from the
 end of the LRU list and try to free a block there.
 If we are doing for the first time we'll scan only
 tail of the LRU list otherwise we scan the whole LRU
 list. */
 freed = buf_LRU_scan_and_free_block(
 buf_pool, n_iterations > 0);

 if (!freed && n_iterations == 0) {
 /* Tell other threads that there is no point
 in scanning the LRU list. This flag is set to
 TRUE again when we flush a batch from this
 buffer pool. */
 buf_pool->try_LRU_scan = FALSE;
 }
 }

 buf_pool_mutex_exit(buf_pool);

 if (freed) {
 goto loop;
 }

 if (n_iterations > 20
 && srv_buf_pool_old_size == srv_buf_pool_size) {
	// 如果循环获取空闲页的次数大于20次,系统将发出报警信息
 ...
}
 /* If we have scanned the whole LRU and still are unable to
 find a free block then we should sleep here to let the
 page_cleaner do an LRU batch for us. */ if (!srv_read_only_mode) {
 os_event_set(buf_flush_event);
 }

 if (n_iterations > 1) {

 MONITOR_INC( MONITOR_LRU_GET_FREE_WAITS );
	// 这里每次循环释放空闲页面会间隔10ms
 os_thread_sleep(10000);
 }

 /* 如果buffer pool里面没有发现可以直接替换的页面(所谓直接替换的页面,
 是指页面没有被修改, 也没有别的线程进行引用,同时当前页已经被载入buffer pool),
 注意:上面的页面淘汰过程至少会尝试
 innodb_lru_scan_depth个页面。如果上面不存在可以淘汰的页面。那么系统将尝试淘汰一个
 脏页面(可替换页面或者已经被载入buffer pool的脏页面)。
 */ if (!buf_flush_single_page_from_LRU(buf_pool)) {
 MONITOR_INC(MONITOR_LRU_SINGLE_FLUSH_FAILURE_COUNT);
 ++flush_failures;
 }

 srv_stats.buf_pool_wait_free.add(n_iterations, 1);

 n_iterations++;

 goto loop;
}



AI 代码解读

从上面获取一个空闲页的源码逻辑可以看出,buf_LRU_get_free_block会循环尝试去淘汰LRU list上的页面。每次循环都会去访问free list,查看是否有足够的空闲页面。如果没有将继续从LRU list去淘汰。这样的循环在负载比较高的情况下,会加剧对free list以及LRU list的mutex竞争。

MySQL空闲页面的获取依赖于page cleaner的刷新能力,如果page cleaner不能即时的刷新足够的空闲页面,那么系统就会使用上面的逻辑来为用户线程申请空闲页面。但如果让page cleaner加快刷新,又会导致频繁刷新脏数据,引发性能问题。 为了改善系统负载太高的情况下,page cleaner刷脏能力不足,进而用户线程调用LRU刷脏导致锁竞争加剧影响数据库性能,Percona对此进行了改善,引入独立的线程负责LRU list的刷脏。目的是为了让独立线程根据系统负载动态调整LRU的刷脏能力。由于LRU list的刷脏从page cleaner线程中脱离出来,调整LRU list的刷脏能力不再会影响到page cleaner。下面我们看一下相关的源码:

/**
 该函数会根据系统的负载情况,或者是buffer pool的空闲页面的情况来动态调整lru_manager_thread的 刷脏能力。
*/ static void lru_manager_adapt_sleep_time(
/*==============================*/
 ulint* lru_sleep_time) /*!< in/out: desired page cleaner thread sleep
 time for LRU flushes */ {
 /* 实际的空闲页 */
 ulint free_len = buf_get_total_free_list_length();
 /* 期望至少保持的空闲页 */
 ulint max_free_len = srv_LRU_scan_depth * srv_buf_pool_instances;

 /* 下面的逻辑会根据当前的空闲页面与期望的空闲页面之间的比对,
 来调整lru_manager_thread的刷脏频率
 */ if (free_len < max_free_len / 100) {

 /* 实际的空闲页面小于期望的1%,系统会触使lru_manager_thread不断刷脏。*/
 *lru_sleep_time = 0;
 } else if (free_len > max_free_len / 5) {

 /* Free lists filled more than 20%, sleep a bit more */
 *lru_sleep_time += 50;
 if (*lru_sleep_time > srv_cleaner_max_lru_time) {
 *lru_sleep_time = srv_cleaner_max_lru_time;
 }
 } else if (free_len < max_free_len / 20 && *lru_sleep_time >= 50) {

 /* Free lists filled less than 5%, sleep a bit less */
 *lru_sleep_time -= 50;
 } else {

 /* Free lists filled between 5% and 20%, no change */
 }
}

extern "C" UNIV_INTERN
os_thread_ret_t
DECLARE_THREAD(buf_flush_lru_manager_thread)(
/*==========================================*/ void* arg __attribute__((unused)))
 /*!< in: a dummy parameter required by
 os_thread_create */ {
 ulint next_loop_time = ut_time_ms() + 1000;
 ulint lru_sleep_time = srv_cleaner_max_lru_time;

#ifdef UNIV_PFS_THREAD
 pfs_register_thread(buf_lru_manager_thread_key);
#endif /* UNIV_PFS_THREAD */ #ifdef UNIV_DEBUG_THREAD_CREATION fprintf(stderr, "InnoDB: lru_manager thread running, id %lu\n",
 os_thread_pf(os_thread_get_curr_id()));
#endif /* UNIV_DEBUG_THREAD_CREATION */

 buf_lru_manager_is_active = true;
 /* On server shutdown, the LRU manager thread runs through cleanup
 phase to provide free pages for the master and purge threads. */ while (srv_shutdown_state == SRV_SHUTDOWN_NONE
 || srv_shutdown_state == SRV_SHUTDOWN_CLEANUP) {
 /* 根据系统负载情况,动态调整lru_manager_thread的工作频率 */
 lru_manager_sleep_if_needed(next_loop_time);

 lru_manager_adapt_sleep_time(&lru_sleep_time);

 next_loop_time = ut_time_ms() + lru_sleep_time;

 /**
 这里lru_manager_thread轮询每个buffer pool instances,尝试从LRU的尾部开始淘汰 innodb_lru_scan_depth个页面
 */
 buf_flush_LRU_tail();
 }

 buf_lru_manager_is_active = false;

 os_event_free(buf_lru_event);
 /* We count the number of threads in os_thread_exit(). A created
 thread should always use that to exit and not use return() to exit. */
 os_thread_exit(NULL);

 OS_THREAD_DUMMY_RETURN;
}

AI 代码解读

从上面的源码可以看到,LRU list的刷脏依赖于LRU_mangager_thread, 当然正常的page cleaner也会对LRU list进行刷脏。但是整个Buffer pool的所有instances都依赖于一个LRU list刷脏线程,负载比较高的情况下也很有可能成为瓶颈。

官方MySQL 5.7版本为了缓解单个page cleaner线程进行刷脏的压力,在5.7.4中引入了multiple page cleaner threads这个feature,用来增强刷脏速度,但是从下面的测试可以发现,即便是multiple page cleaner threads在高负载的情况下,还是会对系统性能有影响。下面的测试结果也显示了性能方面受到的影响。

5.7-mpc.png

就multiple page cleaner刷脏能力受到限制,主要是因为存在以下问题: 1) LRU List刷脏在先,Flush list的刷脏在后,但是是互斥的。也就是说在进Flush list刷脏的时候,LRU list不能继续去刷脏,必须等到下一个循环周期才能进行。 2) 另外一个问题就是,刷脏的时候,page cleaner coodinator会等待所有的page cleaner线程完成之后才会继续响应刷脏请求。这带来的问题就是如果某个buffer pool instance比较热的话,page cleaner就不能及时进行响应。

针对上面的问题,Percona改进了原来的单线程LRU list刷脏的方式,继续将LRU list独立于page cleaner threads并将LRU list单线程刷脏增加为多线程刷脏。page cleaner只负责flush list的刷脏,lru_manager_thread只负责LRU List刷脏。这样的分离,可以使得LRU list刷脏和Flush List刷脏并行执行。看一下修改之后的测试情况:

pc-mlf.png

下面用Multiple LRU list flush threads的源码patch简单介绍一下Percona所做的更改。

@@ -2922,26 +2876,12 @@ pc_flush_slot(void)
 } 
 
 if (!page_cleaner->is_running) {
- slot->n_flushed_lru = 0;
 slot->n_flushed_list = 0; 
 goto finish_mutex;
 } 
 
 mutex_exit(&page_cleaner->mutex);
 
/* 这里的patch可以看出LRU list的刷脏从page cleaner线程里隔离开来 */
- lru_tm = ut_time_ms();
-
- /* Flush pages from end of LRU if required */
- slot->n_flushed_lru = buf_flush_LRU_list(buf_pool);
-
- lru_tm = ut_time_ms() - lru_tm;
- lru_pass++;
-

@@ -1881,6 +1880,13 @@ innobase_start_or_create_for_mysql(void)
 NULL, NULL);
 }
/* 这里在MySQL启动的时候,会同时启动和Buffer pool instances同样数量的LRU list刷脏线程。 */
+ for (i = 0; i < srv_buf_pool_instances; i++) {
/* 这里每个LRU list线程负责自己对应的Buffer pool instance的LRU list刷脏 */
+ os_thread_create(buf_lru_manager, reinterpret_cast<void *>(i),
+ NULL);
+ }
+
+ buf_lru_manager_is_active = true;
+

AI 代码解读

综上所述,本篇文章主要从源码层面对Percona以及官方对于LRU list刷脏方面所做的改进进行了分析。Percona对于LRU list刷脏问题做了很大的贡献。从测试结果可以看到,如果负载较高,空闲页不足的情况下,Percona的改进起到了明显的作用。

相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
MySQL底层概述—2.InnoDB磁盘结构
InnoDB磁盘结构主要包括表空间(Tablespaces)、数据字典(Data Dictionary)、双写缓冲区(Double Write Buffer)、重做日志(redo log)和撤销日志(undo log)。其中,表空间分为系统、独立、通用、Undo及临时表空间,分别用于存储不同类型的数据。数据字典从MySQL 8.0起不再依赖.frm文件,转而使用InnoDB引擎存储,支持事务原子性DDL操作。
274 100
MySQL底层概述—2.InnoDB磁盘结构
MySQL底层概述—1.InnoDB内存结构
本文介绍了InnoDB引擎的关键组件和机制,包括引擎架构、Buffer Pool、Page管理机制、Change Buffer、Log Buffer及Adaptive Hash Index。
300 97
MySQL底层概述—1.InnoDB内存结构
MySQL底层概述—10.InnoDB锁机制
本文介绍了:锁概述、锁分类、全局锁实战、表级锁(偏读)实战、行级锁升级表级锁实战、间隙锁实战、临键锁实战、幻读演示和解决、行级锁(偏写)优化建议、乐观锁实战、行锁原理分析、死锁与解决方案
162 24
MySQL底层概述—10.InnoDB锁机制
MySQL底层概述—5.InnoDB参数优化
本文介绍了MySQL数据库中与内存、日志和IO线程相关的参数优化,旨在提升数据库性能。主要内容包括: 1. 内存相关参数优化:缓冲池内存大小配置、配置多个Buffer Pool实例、Chunk大小配置、InnoDB缓存性能评估、Page管理相关参数、Change Buffer相关参数优化。 2. 日志相关参数优化:日志缓冲区配置、日志文件参数优化。 3. IO线程相关参数优化: 查询缓存参数、脏页刷盘参数、LRU链表参数、脏页刷盘相关参数。
122 12
MySQL底层概述—5.InnoDB参数优化
MySQL底层概述—4.InnoDB数据文件
本文介绍了InnoDB表空间文件结构及其组成部分,包括表空间、段、区、页和行。表空间是最高逻辑层,包含多个段;段由若干个区组成,每个区包含64个连续的页,页用于存储多条行记录。文章还详细解析了Page结构,分为通用部分(文件头与文件尾)、数据记录部分和页目录部分。此外,文中探讨了行记录格式,包括四种行格式(Redundant、Compact、Dynamic和Compressed),重点介绍了Compact行记录格式及其溢出机制。最后,文章解释了不同行格式的特点及应用场景,帮助理解InnoDB存储引擎的工作原理。
MySQL底层概述—4.InnoDB数据文件
MySQL底层概述—3.InnoDB线程模型
InnoDB存储引擎采用多线程模型,包含多个后台线程以处理不同任务。主要线程包括:IO Thread负责读写数据页和日志;Purge Thread回收已提交事务的undo日志;Page Cleaner Thread刷新脏页并清理redo日志;Master Thread调度其他线程,定时刷新脏页、回收undo日志、写入redo日志和合并写缓冲。各线程协同工作,确保数据一致性和高效性能。
MySQL底层概述—3.InnoDB线程模型
MySQL原理简介—2.InnoDB架构原理和执行流程
本文介绍了MySQL中更新语句的执行流程及其背后的机制,主要包括: 1. **更新语句的执行流程**:从SQL解析到执行器调用InnoDB存储引擎接口。 2. **Buffer Pool缓冲池**:缓存磁盘数据,减少磁盘I/O。 3. **Undo日志**:记录更新前的数据,支持事务回滚。 4. **Redo日志**:确保事务持久性,防止宕机导致的数据丢失。 5. **Binlog日志**:记录逻辑操作,用于数据恢复和主从复制。 6. **事务提交机制**:包括redo日志和binlog日志的刷盘策略,确保数据一致性。 7. **后台IO线程**:将内存中的脏数据异步刷入磁盘。
150 12
MySQL进阶突击系列(08)年少不知BufferPool核心原理 | 大哥送来三条大金链子LRU、Flush、Free
本文深入探讨了MySQL中InnoDB存储引擎的buffer pool机制,包括其内存管理、数据页加载与淘汰策略。Buffer pool作为高并发读写的缓存池,默认大小为128MB,通过free链表、flush链表和LRU链表管理数据页的存取与淘汰。其中,改进型LRU链表采用冷热分离设计,确保预读机制不会影响缓存公平性。文章还介绍了缓存数据页的刷盘机制及参数配置,帮助读者理解buffer pool的运行原理,优化MySQL性能。
【YashanDB知识库】原生mysql驱动配置连接崖山数据库
【YashanDB知识库】原生mysql驱动配置连接崖山数据库
【YashanDB知识库】原生mysql驱动配置连接崖山数据库
docker拉取MySQL后数据库连接失败解决方案
通过以上方法,可以解决Docker中拉取MySQL镜像后数据库连接失败的常见问题。关键步骤包括确保容器正确启动、配置正确的环境变量、合理设置网络和权限,以及检查主机防火墙设置等。通过逐步排查,可以快速定位并解决连接问题,确保MySQL服务的正常使用。
231 82
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等