33

MySQL DBA如何"土土"地利用源码解决没有遇到过的错误?

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MjM5NzAzMTY4NQ%3D%3D&%3Bmid=2653933243&%3Bidx=1&%3Bsn=4d46cdf36cda56348ce6c9399c9153c4
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

7JzUbm7.png!web

点击 蓝字 关注我们

本篇 文章记录的是遇到一个未知错误的排查过程,由于本人水平有限,如有描述不正确的欢迎指正。

问题描述

开发报错

FJ3ymuq.jpg!web
MySQL error code 1615 (ER_NEED_REPREPARE): Prepared statement needs to be re-prepared

排查过程

乍一看,没见过这个错误啊,用大腿想了下这个应该是php程序为了防止SQL注入用的prepare执行的。赶紧官方bug搜了一下,一通操作以后路由到了如下地址: https://dev.mysql.com/doc/refman/5.5/en/statement-repreparation.html

简单看了一下,大概意思就是after prepare before execute的阶段,对应的表进行了DDL或者FLUSH TABLES以后table definition cache里面的metadata信息发生了改变,需要reprepare。

接着我搜了一下源码,关键字 re-prepare ,然后我看到官方test套件里有相关的测试。

Q7Z3IjE.jpg!web

可以看到对应的worklog为4166

拿到worklog id以后,我赶紧去官方的work log下搜,在High Level Architecture标签下,我注意到了下面几行:

Prepared_statement::execute_loop() -- try to execute

a statement, reprepare in case of validation error, and try

again until MAX_REPREPARE_ATTEMPTS has been reached.

mysql_{sql_}stmt_execute() are changed to invoke this

method instead of plain execute().

Prepared_statement::reprepare() -- reprepare a prepared

statement, called from execute_loop() in case of ER_NEED_REPREPARE

error.

找到了对应的入口函数:

Prepared_statement::execute_loop()

主要抛出错误位置如下:

if ((sql_command_flags[lex->sql_command] & CF_REEXECUTION_FRAGILE) &&

error && !thd->is_fatal_error && !thd->killed &&

// reprepare观察者发现invalidated,尝试MAX_REPREPARE_ATTEMPTS后报错ER_NEED_REPREPARE

reprepare_observer.is_invalidated() &&

reprepare_attempt++ < MAX_REPREPARE_ATTEMPTS)

{

DBUG_ASSERT(thd->get_stmt_da()->mysql_errno() == ER_NEED_REPREPARE);

thd->clear_error();

error= reprepare();

if (! error) /* Success */

goto reexecute;

}

注意一下观察者 Reprepare_observer 定义

/**

Reprepare_observer观察者是用来观察某个表从上一次执行后的版本变化

这里的"table"可以是MySQL表、临时表、视图或者information schema的表

当我们执行prepared SQL进行打开表并加锁的时候,必须要确认表没有发生改变(DML除外)。因为如果从上次prepare后表发生了改变,那么解析树可能就失效了,例如它可能包含了基于表metadata的优化。

@sa check_and_update_table_version() 获取版本跟踪算法

*/

class Reprepare_observer

{

public:

/**

检查meatadata是否有变化

*/

bool report_error(THD *thd);

bool is_invalidated() const { return m_invalidated; }

void reset_reprepare_observer() { m_invalidated= FALSE; }

private:

bool m_invalidated;

};

注释里写的非常明显,

check_and_update_table_version() for details of the

version tracking algorithm

所以我们主要目光聚集在函数 check_and_update_table_version ,其定义如下:

/**

这里需要比较table definition cache中的元数据版本和之前prepare生成的parse tree中的node进行版本对比


*/

static bool

check_and_update_table_version(THD *thd,

TABLE_LIST *tables, TABLE_SHARE *table_share)

{

// 如果table_id != prepare时的table id,抛出错误,如果是prepare时期,虽然也不匹配,但是这个时候并没有观察者,也就不会抛出错误,但是到execute时,已经有了观察者,这个时候不匹配的话,就会抛出错误了

if (! tables->is_table_ref_id_equal(table_share))

{

Reprepare_observer *reprepare_observer= thd->get_reprepare_observer();

if (reprepare_observer &&

reprepare_observer->report_error(thd))

{

/*

版本不匹配,抛出错误,返回TRUE

*/

DBUG_ASSERT(thd->is_error());

return TRUE;

}

/* 根据table definition cache中的table id更新,总是维护最新的 */

tables->set_table_ref_id(table_share);

}

DBUG_EXECUTE_IF("reprepare_each_statement", return inject_reprepare(thd););

return FALSE;

}

从函数 check_and_update_table_version 中可以看出来,在prepare和execute之间这段时间内,如果table_ref_id(这里的table id其实就是binlog里面的table map event里面的table id,是可以变化的)如果发生了变化,那么需要reprepare。其中还有一点需要注意的是,在prepare之后,会释放对应的MDL锁,所以这个时候是可以进行DDL操作的。那么问题来了,什么情况下,这个table id会发生变化呢?

  1. DDL(包括ALTER/RENAME/TRUNCATE等)

  2. FLUSH TABLES显式地将表定义刷出缓存

  3. TABLE_DEFINITION_CACHE太小,导致对应的表定义缓存被刷出

以上根据自己的经验不完全统计。。。

关于table id

RRNFf2Y.jpg!web

用户查询一个表的数据时,首先会构造根据库名、表名等信息构造hash key,然后从table_def_cache这个hash map中找是否有对应表的缓存,如果存在的话,实例化TABLE_SHARE结构体为TABLE,跟用户交互。如果不存在的话,那么会获取库名、表名等信息存入TABLE_SHARE结构体,在这里生成table_id。

生成table_id的函数如下:

static Table_id last_table_id;

void assign_new_table_id(TABLE_SHARE *share)

{

DBUG_ENTER("assign_new_table_id");

/* Preconditions */

DBUG_ASSERT(share != NULL);

mysql_mutex_assert_owner(&LOCK_open);

DBUG_EXECUTE_IF("dbug_table_map_id_500", last_table_id= 500;);

DBUG_EXECUTE_IF("dbug_table_map_id_4B_UINT_MAX+501",

last_table_id= 501ULL + UINT_MAX;);

DBUG_EXECUTE_IF("dbug_table_map_id_6B_UINT_MAX",

last_table_id= (~0ULL >> 16););

share->table_map_id= last_table_id++;

DBUG_PRINT("info", ("table_id=%llu", share->table_map_id.id()));

DBUG_VOID_RETURN;

}

过程模拟

本模拟案例由重庆八怪提供,非常感谢

session 1 session2 prepare xxx iuYrM3V.jpg!webY3Yjmyz.jpg!web

调试过程:

Fnyae22.jpg!web

这里我们只需要将reprepare_attempt < MAX_REPREPARE_ATTEMPTS 改为不满足条件即可因此

修改reprepare_attempt变量为3则,reprepare_attempt < MAX_REPREPARE_ATTEMPTS 返回false

进入报错流程而不会重新加载table

总结:

这个问题的本质就是table share 在 prepare 和 execute 之间被重新加载了多次

伪代码逻辑如下:

prepare:reprepare_attempt=0 MAX_REPREPARE_ATTEMPTS=3 :

execute:

如果table table被重置过了那么 reprepare_attempt +=1

loop:

重新准备 reprepare

再次 execute

如果table table被重置过了继续循环 reprepare_attempt +=1,如果reprepare_attempt>=MAX_REPREPARE_ATTEMPTS 这报错

否则正常退出循环,正常执行

end loop

实际会进行3次执行尝试,如果都失败就会报错。 因此自己模拟的话还是比较难模拟的,除非直接gdb set 变量或者在线上高压力下才可能出现。

为解决上述的1615问题,可以通过以下办法:

  1. 增加table_definition_cache,防止表定义被刷出缓存

  2. 增加MAX_REPREPARE_ATTEMPTS次数,但是这个属于hard code,没法通过参数修改

  3. 没事别FLUSH TABLES...(如备份,包括extrabackup和mysqldump获取一致性位点都会做FTWRL,因此建议专门的从库做备份)

最后,广告是不能少滴。

叶老师新课程《 MySQL性能优化 》已经在腾讯课堂发布,本课程讲解读几个MySQL性能优化的核心要素: 合理利用索引,降低锁影响,提高事务并发度 。下面是报名小程序码,厚着脸皮请求大家推荐给需要的小伙伴们。

FNBnMbq.jpg!web

下面是本课程内容目录

vyaEfu7.jpg!web

扫码加入MySQL技术Q群

(群号: 650149401)

ba2EF3u.jpg!web

点“在看”给我一朵小黄花

r2yqyiV.png!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK