大幅提升MySQL中InnoDB的全表扫描速度的方法

 在 InnoDB中更加快速的全表扫描
 一般来讲,大多数应用查询的时候都会用索引,查找很少的几行数据(主键查找或百行内的查询),但有时候我们需要全表查询。典型的全表扫描就是逻辑备份  (mysqldump) 和 online schema changes( 注:在线上对大表 schema 的操作,也是 facebook 的一个开源项目) (SELECT ... INTO OUTFILE).

在 Facebook我们用 mysqldump 来备份数据库. 正如你所知MySql提供两种备份方式,提供了物理备份和逻辑备份的命令和工具. 相对物理备份,逻辑备份有一定的优势,例如:

  • 逻辑备份备份数据要小得多. 3x-10x 尺寸差异并不少见。
  • 更容易解析备份数据库. 在物理备份中,在出现严重问题时候,如校验失败。如果我们不能将数据库恢复 ,想知道InnoDB内部数据结构,或者修复损坏是十分困难的。比起物理备份我们更加相逻辑备份。

逻辑备份的主要缺点是数据库的完全备份和完全还原比物理的备份恢复慢得多。

缓慢的完全逻辑备份往往会导致问题.如果数据库中存在很多大小支离破碎的表,它可能需要很长的时间。在 脸书,我们面临 mysqldump 的性能问题,导致我们不能在合理的时间内对一些(基于HDD和Flashcache的)服务器完成完整逻辑备份。我们知道 InnoDB做全表扫描并不高效,因为 InnoDB 实际上并没有顺序读取,在大多情况下是在随机读取。这是一个已知多年的老问题了。我们的数据库存储容量一直在增长,缓慢的全表扫描问题给我们造成了严重的影响,因此,我们决定加强 InnoDB 做顺序读取的速度。最后我们的数据库攻坚工程师团队在InnoDB 中实现了"Logical Readahead"功能。应用"Logical readahead",在通常生产工作负载下,我们全表扫描速比之从前度提高 9 ~ 10 倍。在超负荷生产中,全表扫描速度达到 15 ~ 20 倍的速度甚至更快。

全表扫描在大的、碎片化数据表上的问题
做全表扫描时,InnoDB 会按主键顺序扫描页面和行。这应用于所有的InnoDB 表,包括碎片化的表。如果主键页表没有碎片(存储主键和行的页表),全表扫描是相当快,因为读取顺序接近物理存储顺序。这是类似于读取文件的操作系统命令(dd/cat/etc) 像下面。

代码如下:

dd if=/data/mysql/dbname/large_table.ibd of=/dev/null bs=16k iflag=direct

你可能会发现即使在商业HDD服务器上,你可以达到高于比100 MB/s 乘以"驱动器数目"的速度。超过1GB/s并不少见。

不幸的是,在许多情况下主要关键页表存在碎片。例如,如果您需要管理 user_id 和 object_id 映射,主键将会是(user_id,object_id)。插入排序与 user_id并不一致,那么新插入/更新往往导致页拆分。新的拆分页将被分配在远离当前页的位置。这意味着页面将会碎片化。

如果主键页是碎片化的,全表扫描将会变得极其缓慢。图1阐释了这个问题。在InnoDB读取叶子页#3之后,它需要读取页#5230,在那之后还要读页#4。页#5230位置离页#3和页#4很远,所以磁盘读操作顺序开始变得几乎是随机的,而不是连续的。大家都知道HDD上的随机读要比连续读慢得多。一个有效的改进随机读性能的办法是使用SSD。不过SSD每个GB的价钱要比HDD昂贵的多,所以使用SSD通常是不可能的。

图 1.全表扫描实际没有连续读

线性预读取真的有意义吗?
InnoDB支持预读取特性,称作“线性预读取”( Linear Read Ahead)。拥有线性预读取,如果N个page可以顺序访问(N可以通过innodb_read_ahead_threshold参数进行配置,默认为56),InnoDB可以一次读取一个extent(64个连续的page,如果不压缩每个page为1MB)。但是,实际来说这么做的意义不大。一个extent(64个page)非常小。对于一个支离破碎的较大的数据库表来说,下一个page不一定在同一个extent当中。上面图1就是一个很好的例子。读取page#3之后,InnoDB需要读取page#5230。page#3和page#5230并不在同一个extent当中,所以线性预读取技术在这里用处不大。这对于大表来说是非常常见的情况,所以这也解释了线性预读取技术为什么不能有效改善全表扫描的性能。
 
物理预读取
正如上面描述的,全表扫描速度较慢的主要原因是InnoDB主要进行随机读取。为了加速全表扫描,需要使InnoDB进行顺序读取。我想到的第一个方法就是创建一个UDF(user defined function)顺序的读取ibd文件(InnoDB的数据文件)。UDF执行完成后,ibd文件的page应当保存在InnoDB的缓存池当中,所以在进行全表扫描时无需再进行随机读取。下面是一个示例用法:

mysql> SELECT buf_warmup ("db1", "large_table"); /* loading into buf pool */
mysql> SELECT * FROM large_application_table; /* in-memory select */

buf_warmup() 是一个用户自定义函数,用来读取数据库“db1"的表”large_table"的整个ibd文件。该函数需要花费时间将ibd文件从硬盘读取,但因为是顺序读取的,所以比随机读取要快的多。在我的测试当中,比普通的线性预读取快差不多5倍左右。

这证明ibd文件的顺序读取能够有效的改善吞吐率,但也存在一些缺点:

  • 如果table的大小超过InnoDB缓存池的大小,这种方法就不能工作
  • 在全表扫描过程中,读取整个的ibd文件就意味着不但需要读取primary key page还需要读取二级索引page以及一些其他不需要的page,并将其保存在缓存池,尽管只有primary key page是实际需要的。如果拥有大量的二级索引,这种方法就不能有效的工作
  • 应用需要做出一定的修改以便调用UDF

这看起来是一个足够好的解决方案,但我们的数据库设计团队想出了一个更好的解决方法叫做“逻辑预读取”(Logical Read Ahead),所以我们并不选择UDF的方法。

逻辑预读取
逻辑预读取(LRA)的工作流程如下:

  • 读取主键的一些分支page
  • 计算叶子page的数量
  • 以page number的顺序(大多数是顺序磁盘读取)依次读取一些(通过配置控制数量的多少)叶子page
  • 以主键的顺序读取行

整个流程如图2所示:

Fig 2: Logical Read Ahead

逻辑预读取解决了物理预读取所存在的问题。LRA使InnoDB仅读取主键page(不需要读取二级索引页面),并且每一次预读取页面的数量是可以控制的。除此之外,LRA对SQL语法不需要做任何修改。

为了使LRA工作,我们需要增加两个session变量。一个是"innodb_lra_size",用来控制预读取叶子页面(page)大小。另外一个是"innodb_lra_sleep",用来控制每一次预读取之间休眠多长时间。我们用512MB~4096MB的大小以及50毫秒的休眠时间来进行测试,到目前为止我们还没有遇到任何严重问题(例如崩溃/阻塞/不一致等)。这些session变量仅在需要进行全表的时候进行设置。在我们的应用中,mysqldump以及其他一些辅助脚本启用了逻辑预读取。

一次提交多个async I/O请求

我们注意到,另外一个导致性能问题的原因是InnoDB 每次i/o仅读取一个页面,即使开启了预读取技术。每次仅读取16KB对于顺序读取来说实在是太小了,效率相比大的读取单元要低很多。

在版本5.6中,InnoDB默认使用Linux本地I/O。如果一次提交多个连续的16KB读请求,Linux在内部会将这些请求合并,读操作能够更有效的执行。不幸的是,InnoDB一次只会提交一个页面的i/o请求。我提交了一个bug report#68659.正如bug report中所写,在一个当代的HDD RAID 1+0环境中,如果我一次性提交64个连续的页面读取请求,我可以获得超过1000MB/s的硬盘读取速度;如果每次只提交一个页面读取请求,我们仅可以获得160MB/s的硬盘读取速度。

为了使LRA在我们的应用环境中更好的工作,我们修正了这个问题。在我们的MySQl中,InnoDB在调用io_submit()之前会提交多个页面i/o请求。

基准测试
在所有的测试中,我们使用的都是生产环境下的数据库表(分页的表)。

1. 纯HDD环境全表扫描 (基础的基准测试, 没有其他的工作负载)

2. Online schema change under heavy workload

* dump time only, not counting data loading time
 源码
 我们做出的所有增强修改都可以在GitHub上获取。

  • - 逻辑预读取实现 : diff
  • - 一次提交多个i/o请求:diff
  • - 在mydqldump中启用逻辑预读取 :diff

结论

对于全表扫描来说InnoDB的工作效率不高,所以我们对它做了一定的修改。我在两方面进行了改进,一是实现了逻辑预读取;一是实现了一次提交多个async read i/o请求。对于我们生产环境中的数据库表来说,我们获得了8-18倍的性能提高,这对于减少备份时间、模式修改时间等来说是非常有用的。我希望这些特性能够在InnoDB中获得Oracle官方支持,至少是主要的MySQL分支。

(0)

相关推荐

  • MySQL Innodb表导致死锁日志情况分析与归纳

    案例描述在定时脚本运行过程中,发现当备份表格的sql语句与删除该表部分数据的sql语句同时运行时,mysql会检测出死锁,并打印出日志.两个sql语句如下:(1)insert into backup_table select * from source_table(2)DELETE FROM source_table WHERE Id>5 AND titleWeight<32768 AND joinTime<'$daysago_1week'teamUser表的表结构如下:PRIMARY

  • 浅谈InnoDB隔离模式的使用对MySQL性能造成的影响

    在这篇文章里我将讨论一个相关的主题 – InnoDB 事务隔离模式,还有它们与MVCC(多版本并发控制)的关系,以及它们是如何影响MySQL性能的. MySQL手册提供了一个关于MySQL支持的事务隔离模式的恰当描述 – 在这里我并不会再重复,而是聚焦到对性能的影响上. SERIALIZABLE – 这是最强的隔离模式,本质上打败了在锁管理(设置锁是很昂贵的)的条件下,多版本控制对所有选择进行锁定造成大量的开销,还有你得到的并发.这个模式仅在MySQL应用中非常特殊的情况下使用. REPEATA

  • 可以改善mysql性能的InnoDB配置参数

    而由于InnoDB是一个健壮的事务型存储引擎,已经有10多年的历史,一些重量级的互联网公司(Yahoo,Google Netease ,Taobao)也经常使用 我的日常工作也经常接触InnoDB,现在就InnoDB一部分可以改善性能的参数列举 1. innodb_additional_mem_pool_size 除了缓存表数据和索引外,可以为操作所需的其他内部项分配缓存来提升InnoDB的性能.这些内存就可以通过此参数来分配.推荐此参数至少设置为2MB,实际上,是需要根据项目的InnoDB表的

  • 大幅提升MySQL中InnoDB的全表扫描速度的方法

     在 InnoDB中更加快速的全表扫描  一般来讲,大多数应用查询的时候都会用索引,查找很少的几行数据(主键查找或百行内的查询),但有时候我们需要全表查询.典型的全表扫描就是逻辑备份  (mysqldump) 和 online schema changes( 注:在线上对大表 schema 的操作,也是 facebook 的一个开源项目) (SELECT ... INTO OUTFILE). 在 Facebook我们用 mysqldump 来备份数据库. 正如你所知MySql提供两种备份方式,提

  • Mysql如何避免全表扫描的方法

    在以下几种条件下,MySQL就会做全表扫描: 1>数据表是在太小了,做一次全表扫描比做索引键的查找来得快多了.当表的记录总数小于10且记录长度比较短时通常这么做. 2>没有合适用于 ON 或 WHERE 分句的索引字段. 3>让索引字段和常量值比较,MySQL已经计算(基于索引树)到常量覆盖了数据表的很大部分,因此做全表扫描应该会来得更快. 4>通过其他字段使用了一个基数很小(很多记录匹配索引键值)的索引键.这种情况下,MySQL认为使用索引键需要大量查找,还不如全表扫描来得更快.

  • PostgreSQL 禁用全表扫描的实现

    PostgreSQL可以通过一些设置来禁用全表扫描(FULL SCAN/Seq Scan) 注意: 设置此功能后不是完全避免全表扫描,而是只要有不通过全表扫描能得出结果的就不走全表扫描. 如果什么路都不通,那肯定得全表扫描,不然怎么获取数据. 而且并不是不走全表扫描性能就一定好. 下面展示下这个功能: 查询表结构: highgo=# \d test Table test Column | Type | Modifiers -------------+-----------------------

  • MySQL中的全表扫描和索引树扫描 的实例详解

    目录 引言 实例 引言 在学习mysql时,我们经常会使用explain来查看sql查询的索引等优化手段的使用情况.在使用explain时,我们可以观察到,explain的输出有一个很关键的列,它就是type属性,type表示的是扫描方式,代表 MySQL 使用了哪种索引类型,不同的索引类型的查询效率是不一样的. 在type这一列,有如下一些可能的选项: system:系统表,少量数据,往往不需要进行磁盘IOconst:常量连接eq_ref:主键索引(primary key)或者非空唯一索引(u

  • 导致MySQL做全表扫描的几种情况

    这两天看到了两种可能会导致全表扫描的sql,这里给大家看一下,希望可以避免踩坑: 情况1: 强制类型转换的情况下,不会使用索引,会走全表扫描. 举例如下: 首先我们创建一个表  CREATE TABLE `test` (   `id` int(11) NOT NULL AUTO_INCREMENT,   `age` int(11) DEFAULT NULL,   `score` varchar(20) NOT NULL DEFAULT '',   PRIMARY KEY (`id`),   KE

  • MySQL查询优化:LIMIT 1避免全表扫描提高查询效率

    在某些情况下,如果明知道查询结果只有一个,SQL语句中使用LIMIT 1会提高查询效率. 例如下面的用户表(主键id,邮箱,密码): 复制代码 代码如下: create table t_user( id int primary key auto_increment, email varchar(255), password varchar(255) ); 每个用户的email是唯一的,如果用户使用email作为用户名登陆的话,就需要查询出email对应的一条记录. SELECT * FROM t

  • SQL中WHERE变量IS NULL条件导致全表扫描问题的解决方法

    复制代码 代码如下: SET @SQL = 'SELECT * FROM Comment with(nolock) WHERE 1=1    And (@ProjectIds Is Null or ProjectId = @ProjectIds)    And (@Scores is null or Score =@Scores)' 印象中记得,以前在做Oracle开发时,这种写法是会导致全表扫描的,用不上索引,不知道Sql Server里是否也是一样呢,于是做一个简单的测试1.建立测试用的表结

  • 详解MySql中InnoDB存储引擎中的各种锁

    目录 什么是锁 InnoDB存储引擎中的锁 锁的算法 行锁的3种算法 幻像问题 锁的问题 脏读 不可重复读 丢失更新 死锁 什么是锁 现实生活中的锁是为了保护你的私有物品,在数据库中锁是为了解决资源争抢的问题,锁是数据库系统区别于文件系统的一个关键特性.锁机制用于管理对共享资源的并发访. 数据库系统使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性 InnoDB存储引擎区别于MyISAM的两个重要特征就是:InnoDB存储引擎支持事务和行级别的锁,MyISAM只支持表级别的锁 In

  • MySQL中Innodb的事务隔离级别和锁的关系的讲解教程

    前言: 我们都知道事务的几种性质,数据库为了维护这些性质,尤其是一致性和隔离性,一般使用加锁这种方式.同时数据库又是个高并发的应用,同一时间会有大量的并发访问,如果加锁过度,会极大的降低并发处理能力.所以对于加锁的处理,可以说就是数据库对于事务处理的精髓所在.这里通过分析MySQL中InnoDB引擎的加锁机制,来抛砖引玉,让读者更好的理解,在事务处理中数据库到底做了什么. 一次封锁or两段锁? 因为有大量的并发访问,为了预防死锁,一般应用中推荐使用一次封锁法,就是在方法的开始阶段,已经预先知道会

  • LINQ to SQL:处理char(1)字段的方式会引起全表扫描问题

    如果表中的字段类型为 char(1) 时,Linq to SQL生成char (System.Char)的属性,如下图 表定义 生成的实体 2. 如果要查询LineCode=='A'的记录,可以这样定义Linq查询语句 var test1 = from p in db.ProductLines             where p.LineCode =='A'             select p; 生成的SQL语句是这样的 SELECT [t0].[LineCode], [t0].[Li

随机推荐