最新的MySQL 8.0.20版本添加了一个新的优化,即doublewrite性能优化,一举解决了困扰MySQL多时的写入性能瓶颈。在极端I/O密集的应用场景下,能有超过10倍的写入性能提升。
doublewrite机制是MySQL InnoDB存储引擎所独有的功能,用以解决物理硬件发生宕机时可能产生的partial write问题。简单来说,在MySQL InnoDB存储中,脏页列表的刷新,会先顺序地写入到doublewrite,然后再随机写入到每个页应该所在的问题。更详细的内容可参见《MySQL技术内幕:InnoDB存储引擎》一书。有同学会说,为什么地球最强大的Oracle数据库没有采用类似机制?这样的机制会导致一个脏页写入变成了2次写,即doublewrite。不得不承认,Oracle数据库没有InnoDB存储引擎在数据安全性方面严谨,至少在脏页刷新这块。Oracle若发生partial write的问题,只能通过RMAN恢复物理页。由于脏页需要写2次,开启doublewrite特性会导致性能下降一半么?之前大多数DBA的理解是,由于doublewrite的128个页对象是先顺序写的,因此性能开销较小。一般在5% ~ 25%的性能开销。然而,在极端并发场景下,doublewrite的性能退化会非常厉害,使得MySQL写入性能大幅退化,甚至远超之前5%~25%的认知。在讲MySQL 8.0对于doublewrite的优化前,先看看这10年MySQL InnoDB存储引擎对于脏页刷新的性能优化措施。
在MySQL 5.5版本中,脏页的刷新是在Master线程中定期完成。比如每秒刷多少脏页,每10秒刷新多少脏页,且在代码中硬编码规定每次刷新的脏页数量最多为200个。
此外,除了完成脏页的刷新工作,Master线程还需要负责undo回收,日志写入等工作。在SSD设备出现前,由于IOPS都很小,因此这个工作机制并没有太大的问题。此外,除了脏页列表的刷新,还有一种称为LRU的脏页刷新,这种刷新机制是当LRU可替换页不多的情况下,需要刷新LRU最尾部的脏页。在MySQL 5.5版本中,这个刷新机制可能在用户线程触发,即当一个用户发起普通SELECT操作,都可能触发LRU脏页刷新,从而阻塞用户的查询。Percona 5.5版本对比官方MySQL 5.5版本提升非常多,大多是针对上述的I/O刷新优化策略。可以说,Percona 5.5版本就是当时的网红版本,大多互联网公司的MySQL分支版本也是在这个时期萌芽和发展起来的。
针对5.5版本的刷新性能瓶颈,MySQL 5.6版本做了几个优化工作。首先,引入变量innodb_io_capacity,设置每次能刷新最大的脏页数量。从而提升MySQL在SSD存储设备下的性能。调整变量innodb_log_file_size 4G大小限制,提升MySQL在刷新时的稳定性。
其次,引入Page Cleaner线程,将脏页刷新都交由此线程完成,减轻Master线程的负载。同时,新增自适应刷新多发,通过重做日志增速,对刷新脏页进行“补偿”。使得在I/O密集的场景下,性能更为平滑。最后,将LRU脏页刷新放到后台线程完成,不再阻塞用户线程的查询。到5.6版本时,Percona版本在刷新性能上已无太大优势。正是从这个版本开始,Percona最大的优势仅在移植MariaDB的线程池到MySQL。
5.6版本引入变量innodb_buffer_pool_instances,拆分一个大缓冲池为多个缓冲池,降低缓冲池latch冲突,从而提升性能。但在引入这个机制后,原来的一个脏页列表,变为了多个脏页列表。但Page Cleaner刷新线程仅有一个,因此多个缓冲池机制未能充分发挥写入的并行性能。
在5.7版本中引入变量innodb_page_cleaner,可设置Page Cleaner线程的数量,从而提升脏页刷新的效率。需要特别提及的是,每个Page Cleaner线程并不是和缓冲池一一绑定的。因为这样每个缓冲池依然是单个线程刷新,并不能充分发挥优势。因此Page Cleaner线程是由1个协调线程和多个工作线程组成,协调线程也可以是工作线程,可以负责脏页的刷新工作。上图中可以看到变量innodb_page_cleaners设置为了8,但其实是由1个协调线程和7个刷新的工作线程组成。具体工作机制可见源码中的函数buf_flush_page_coordinator_thread。
到5.7版本为止,对MySQL脏页刷新本身的优化都已完成,需要进入到更为核心的底层优化,即对于latch锁的优化。这部分工作的难度相对前面来说,更大一些。不过,相信对于经验丰富的内核开发人员来说,也只是piece of breeze~~~
MySQL 8.0对InnoDB Redo模块进行全面的设计与重构,解决了之前log mutex这把困扰性能多年的“大锁”。在事务运行过程中,需要将redo重做日志写入到Log Buffer,这时需要log mutex这把锁的保护。很明显,任何用户线程的DML操作都会引发log mutex这把锁的获取和释放,从而导致性能热点与瓶颈。在进行完Redo模块的重构后,MySQL在写入方面能有30%+的性能提升。
在最新的MySQL 8.0.20版本中,终于对doublewrite机制进行了彻底的优化。doublewrite最大的瓶颈在于虽然Page Cleaner线程已经是由多个线程组成并负责脏页的刷新,但最后这些脏页都需要先拷贝到doublewrite内存中,然后再写入到doublewrite物理存储中。而拷贝脏页到doublewrite内存中,需要持有doublewrite的mutex。同样的,这把锁也是竞争的热点,在大并发写入场景下,会导致性能瓶颈。
8.0.20版本对于doublewrite的优化在于将一个doublewrite对象拆分为了多个独立doublewrite文件保存。每个文件中各有1个FLUSH_LIST脏页刷新doubewrite段(segment)和LRU_LIST脏页刷新doublewrite段。新引入的参数innodb_doublewrite_files,默认值为2,表示有2个doublewrite文件组成,每个doublewrite文件又有2个doublewrite对象。即,默认配置下,将原来的doublewrite对象拆分为了4个doublewrite对象,从而提升并发写入的性能。可以看到在上述doublewrite优化措施下,在并发128、256、512、1024线程下,8.0.20版本的性能不会有退化,对比8.0.19版本则性能可有10多倍的显著提升。
从MySQL 5.6版本开始,官方一直在对SSD这类超快存储设备下的写入性能和稳定性进行优化,目前MySQL 8.0.20版本下,InnoDB存储引擎整体的脏页刷新机制如下,多个缓冲池对象,多个脏页刷新线程,多个Doublewrite对象,多个异步I/O回调线程......可以发现优化措施无外乎拆分,线程拆分,锁拆分,从而提升整体性能。
MySQL 8.0.20版本对于doublewrite有了比较彻底的优化,相信未来doublewrite不会再成为拖累性能的元凶。然而,必须牢记的是,所谓的10倍性能提升有且仅发生在大并发写入场景下。最后,姜老师想问,有同学还能想到InnoDB存储引擎性能优化点么?欢迎留下你的宝贵意见。-----------------------
知乎:破产码农
长按下图二维码关注,将感受到一个有趣的灵魂,每篇文章都会有新惊喜。