TokuDB · 捉虫动态 · MRR 导致查询失败

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云原生数据库 PolarDB 分布式版,标准版 2核8GB
简介: 问题背景最近有用户在使用 TokuDB 时,遇到了一个查询报错的问题,这里给大家分享下。具体的报错信息是这样的:mysql> select * from t2 where uid > 1 limit 10;ERROR 1030 (HY000): Got error 1 from storage engine表结构如下:CREATE TABLE `t2` ( `

问题背景

最近有用户在使用 TokuDB 时,遇到了一个查询报错的问题,这里给大家分享下。

具体的报错信息是这样的:

mysql> select * from t2 where uid > 1 limit 10;
ERROR 1030 (HY000): Got error 1 from storage engine

表结构如下:

CREATE TABLE `t2` (
  `id` bigint(20) NOT NULL,
  `uid` bigint(20) DEFAULT NULL,
  `post` text,
  `note` text,
  PRIMARY KEY (`id`),
  KEY `idx_uid` (`uid`)
) ENGINE=TokuDB DEFAULT CHARSET=utf8

问题分析

从报错信息来看,是引擎层返回错误的,难道是 TokuDB 数据出问题了么,我们首先要确认的是用户数据是否还能访问。

从表结构来看,出错的语句应该走了二级索引,那么我们强制走 PK 是否能访问数据呢。

select * from t2 force index(primary) where uid > 1 limit 3;
xxx
xxx
xxx
3 rows in set (0.00 sec)

上面的测试可以说明走 PK 是没问题呢,那么问题可能在二级索引。

同时我们在观察用户的其它 SQL 时发现,二级索引也是可以访问数据的。

比如下面这种:

select * from t2  where uid > 1 order by uid limit 3;
xxx
xxx
xxx
3 rows in set (0.00 sec)

都是走二级索引,为什么有的会报错呢,这 2 条语句有啥区别呢,explain 看下:

mysql> explain select * from t2  where uid > 1 limit 3;
+----+-------------+-------+-------+---------------+---------+---------+------+--------+-----------------------------------------------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref  | rows   | Extra                                         |
+----+-------------+-------+-------+---------------+---------+---------+------+--------+-----------------------------------------------+
|  1 | SIMPLE      | t2    | range | idx_uid       | idx_uid | 9       | NULL | 523677 | Using index condition; Using where; Using MRR |
+----+-------------+-------+-------+---------------+---------+---------+------+--------+-----------------------------------------------+
1 row in set (0.00 sec)

mysql> explain select * from t2  where uid > 1 order by uid limit 3;
+----+-------------+-------+-------+---------------+---------+---------+------+--------+------------------------------------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref  | rows   | Extra                              |
+----+-------------+-------+-------+---------------+---------+---------+------+--------+------------------------------------+
|  1 | SIMPLE      | t2    | range | idx_uid       | idx_uid | 9       | NULL | 523677 | Using index condition; Using where |
+----+-------------+-------+-------+---------------+---------+---------+------+--------+------------------------------------+
1 row in set (0.00 sec)

可以看到出错的语句,用到了 MRR(不了解 MRR 的可以看下我们之前的月报 优化器 MRR & BKA),这是优化器在走二级索引时,为了减少回表的磁盘 IO 的一个优化。

把这个优化关掉呢?

set optimizer_switch='mrr=off';
mysql> explain select id from t2  where uid > 1 limit 3;
+----+-------------+-------+-------+---------------+---------+---------+------+--------+--------------------------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref  | rows   | Extra                    |
+----+-------------+-------+-------+---------------+---------+---------+------+--------+--------------------------+
|  1 | SIMPLE      | t2    | range | idx_uid       | idx_uid | 9       | NULL | 523677 | Using where; Using index |
+----+-------------+-------+-------+---------------+---------+---------+------+--------+--------------------------+
1 row in set (0.00 sec)

select * from t2  where uid > 1 limit 3;
xxx
xxx
xxx
3 rows in set (0.00 sec)

可以看到,关掉优化器的 MRR 后,语句就返回正常了。因此基本可以判断是 MRR 导致的。

下面我们从源码层面分析下看,到底是怎么回事。

根据报错信息,来 gdb 跟踪,发现导致报错的栈是这样的,可以看到是在 mrr 执行初始化阶段:

#0  DsMrr_impl::dsmrr_init()
#1  ha_tokudb::multi_range_read_init()
#2  QUICK_RANGE_SELECT::reset()
#3  join_init_read_record()
#4  sub_select()
#5  do_select()
#6  JOIN::exec()
#7  mysql_execute_select()
#8  mysql_select()
#9  handle_select()
#10 execute_sqlcom_select()
#11 mysql_execute_command()
...

具体在 DsMrr_impl::dsmrr_init 中的逻辑是这样的:

// Transfer ICP from h to h2
if (mrr_keyno == h->pushed_idx_cond_keyno)
{
  if (h2->idx_cond_push(mrr_keyno, h->pushed_idx_cond))
  {
    retval= 1;
    goto error;
  }
}

我们对应看下 TokuDB 里条件下推接口实现:

// we cache the information so we can do filtering ourselves,
// but as far as MySQL knows, we are not doing any filtering,
// so if we happen to miss filtering a row that does not match
// idx_cond_arg, MySQL will catch it.
// This allows us the ability to deal with only index_next and index_prev,
// and not need to worry about other index_XXX functions
Item* ha_tokudb::idx_cond_push(uint keyno_arg, Item* idx_cond_arg) {
    toku_pushed_idx_cond_keyno = keyno_arg;
    toku_pushed_idx_cond = idx_cond_arg;
    return idx_cond_arg;
}

可以看到 ha_tokudb::idx_cond_push 是会将原条件在返回给 server 的。因此就导致了 DsMrr_impl::dsmrr_init 返回错误码 1 (Got error 1 from storage engine)。

handler:idx_cond_push() 接口是允许引擎层返回非 NULL 值的,引擎层认为自己没有完全过滤结果集,那么是可以返回条件给 server 层,让 server 层再做一次过滤的:

/**
  Push down an index condition to the handler.

  The server will use this method to push down a condition it wants
  the handler to evaluate when retrieving records using a specified
  index. The pushed index condition will only refer to fields from
  this handler that is contained in the index (but it may also refer
  to fields in other handlers). Before the handler evaluates the
  condition it must read the content of the index entry into the
  record buffer.

  The handler is free to decide if and how much of the condition it
  will take responsibility for evaluating. Based on this evaluation
  it should return the part of the condition it will not evaluate.
  If it decides to evaluate the entire condition it should return
  NULL. If it decides not to evaluate any part of the condition it
  should return a pointer to the same condition as given as argument.

  @param keyno    the index number to evaluate the condition on
  @param idx_cond the condition to be evaluated by the handler

  @return The part of the pushed condition that the handler decides
          not to evaluate
 */

virtual Item *idx_cond_push(uint keyno, Item* idx_cond) { return idx_cond; }

因此这个问题是 MRR 在实现上的一个 bug,没有考虑引擎在ICP时返回非 NULL 的情况。

另外我们在查问题时发现,如果 mysqld 重启或者通过 flush table 关闭表的话,查询是不会出错的:

mysql> flush table t2;
mysql> explain  select * from t2  where uid > 1 limit 3;
+----+-------------+-------+-------+---------------+---------+---------+------+--------+------------------------------------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref  | rows   | Extra                              |
+----+-------------+-------+-------+---------------+---------+---------+------+--------+------------------------------------+
|  1 | SIMPLE      | t2    | range | idx_uid       | idx_uid | 9       | NULL | 523677 | Using index condition; Using where |
+----+-------------+-------+-------+---------------+---------+---------+------+--------+------------------------------------+

从 explain 结果看,是因为没有用到 MRR,这又是为什么呢?

我们看下优化器是如何选择是否用MRR优化的,在 DsMrr_impl::choose_mrr_impl() 这个函数里是有这样的逻辑的:

 /*
   If @@optimizer_switch has "mrr_cost_based" on, we should avoid
   using DS-MRR for queries where it is likely that the records are
   stored in memory. Since there is currently no way to determine
   this, we use a heuristic:
   a) if the storage engine has a memory buffer, DS-MRR is only
      considered if the table size is bigger than the buffer.
   b) if the storage engine does not have a memory buffer, DS-MRR is
      only considered if the table size is bigger than 100MB.
   c) Since there is an initial setup cost of DS-MRR, so it is only
      considered if at least 50 records will be read.
 */
 if (thd->optimizer_switch_flag(OPTIMIZER_SWITCH_MRR_COST_BASED))
 {
   /*
     If the storage engine has a database buffer we use this as the
     minimum size the table should have before considering DS-MRR.
   */
   longlong min_file_size= table->file->get_memory_buffer_size();
   if (min_file_size == -1)
   {
     // No estimate for database buffer
     min_file_size= 100 * 1024 * 1024;    // 100 MB
   }

   if (table->file->stats.data_file_length <
       static_cast<ulonglong>(min_file_size) ||
       rows <= 50)
     return true;                 // Use the default implementation
 }

可以看到,MRR 选择条件是这样的:

  1. 如果引擎的 cache 比表大的话,是不会用 MRR 优化的;
  2. 如果引擎没有 cache,默认用 100M,用于自己不管理 cache 引擎,如 MyISAM;
  3. 如果要查询的行数不超过50的话,也是不会用 MRR 优化的;

这个 cache 对 InnoDB 来说,就是 innodb_buffer_pool_size;对 TokuDB 来说,就是 tokudb_cache_size。但是 TokuDB handler 层没有实现 get_memory_buffer_size() 这个接口,导致一直用 100M 做为 cache 来判断,这个是 TokuDB handler 实现的上的一个bug。

data_file_length 这个是值是内存信息,在表刚关闭重新打开的时候,是0,所以不会用MRR优化。

另外还有一个判断条件时,如果要求排序的话,也是不会用 MRR 优化的,这也就是为什么我们刚开始发现的,语句中用了 order by 后,explain 结果中就没有 MRR了。

问题影响和解决

从上面的分析来看,满足下面条件语句会被影响:

  1. 语句访问的是 TokuDB 表,并且走的二级索引,有回表操作;
  2. 表大小超过 100M;

简单的判断方法是,explain 结果中有 Using index condition; Using where; Using MRR,并且语句报错 Got error 1 from storage engine。

临时的解决方法是关闭优化器的 MRR 或者 ICP:

set optimizer_switch='mrr=off';
or
set optimizer_switch='index_condition_pushdown=off';

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
存储 负载均衡 NoSQL
MongoDB·最佳实践·count不准原因分析
背景 一般来说,除了由于secondary延迟可能造成查询secondary节点数据不准以外,关于count的准确性问题,在MongoDB4.0官方文档中有这么一段话On a sharded cluster, db.
|
SQL 监控 Go
MSSQL · 应用案例 · 日志表设计优化与实现
摘要 这篇文章从日志表问题引入、日志表的共有特性、日志表的设计需求、设计思路以及设计详细实现的角度,阐述了在SQL Server数据库中如何最优化设计日志表来降低系统资源的占用和提高系统吞吐量。问题引入 在平时与客户服务与交流过程中,我们不止一次的被客人问及这样的场景:我们现在面临如何设计SQL Server日志表方案,如何最优化设计数据库日志记录表。
1525 0
|
SQL XML 存储
MSSQL · 应用案例 · 构建死锁自动收集系统
摘要 这篇文章介绍SQL Server的一个典型的应用案例,即如何利用Event Notification与Service Broker技术相结合来实现死锁信息自动收集系统。通过这个系统,我们可以全面把控SQL Server数据库环境中所有实例上发生的死锁详细信息,供我们后期分析和解决死锁场景。 死锁自动收集系统需求分析 当 SQL Server 中某组资源的两个或多个线程或进程之间存在循环的依
2530 0
|
AliSQL 关系型数据库 索引
AliSQL · 特性介绍 · 动态加字段
背景 加字段作为业务需求变更中最常见的需求,InnoDB引擎表的加字段功能一直以来被运维人员所诟病, 虽然支持了online方式,但随着表空间越来越大,copy整张表的代价也越来越大。 AliSQL版本在InnoDB的compact记录格式的基础上,设计了新的记录格式comfort,支持动态加字段。 使用方法 使用的实例如下: CREATE TABLE test( id int primar
2362 0
|
关系型数据库 MySQL
MySQL · 捉虫动态 · 备库1206错误问题说明
问题背景 一个用户自建MySQL,出现备库复制中断的问题,报错为slave sql thread 错误,The total number of locks exceeds the lock table size。 报错代码 这个报错在代码中的抛错逻辑为: if UT_LIST_GET_LEN(buf_pool-&gt;free) + UT_LIST_GET_LEN(buf_pool-&gt;L
1592 0
|
关系型数据库 MySQL 索引
MySQL · 捉虫状态 · bug分析两例
BUG 1 IN查询结果不对 背景 在mysql5.6.16版本下,构建如下测试用例 CREATE TABLE `a` ( `c1` varchar(512) NOT NULL DEFAULT '' ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `a` VALUES ('i-28s18atup'),('i-2850jdoa2'),('i-
1375 0
|
SQL 缓存 关系型数据库
MySQL · 特性分析 · 执行计划缓存设计与实现
Plan Cache背景知识 一条SQL语句输入到MySQL服务器后,一般要经历:词法语法解析(parse),优化(optimize),生成执行计划(plan)和执行(execute)的过程。词法语法分析,优化以及生成执行计划,这三个阶段的主要输出是SQL语句的执行计划(plan),当SQL语句存在多种执行计划的时候,优化器会从这许多的执行计划中挑选出一个它认为最优的(通常是占用系统资源最少的,
2240 0