MySQL/GreatSQL 游标重解析后条件下推core缺陷深度排查
一、问题发现
在一次开发中发现当执行需要条件下推cursor重新解析语句之后,会导致数据库core。看下面复现步骤,这里用的是mysql8.4.4的debug版本代码。
1、创建表和View
greatsql> CREATE TABLE tb1 (EMPLOYEE_ID int, FIRST_NAME VARCHAR2(20), LAST_NAME VARCHAR(25),
EMAIL VARCHAR2(25), PHONE_NUMBER VARCHAR2(20) DEFAULT NULL, HIRE_DATE DATE DEFAULT NULL,
JOB_ID VARCHAR2(10), SALARY NUMBER(8,2) DEFAULT 1000, COMMISSION_PCT DECIMAL(2,2) DEFAULT NULL,
MANAGER_ID float(6,0), DEPARTMENT_ID NUMBER(4,0),PRIMARY KEY(EMPLOYEE_ID)) ;
greatsql> INSERT INTO tb1 VALUES (145,'John','Russell','JRUSSEL','011.44.1344.429268','2020-3-1','SA_MAN',14000,0.4,100,80);
greatsql> CREATE TABLE tb2 (
EMPLOYEE_ID int PRIMARY KEY,
FIRST_NAME VARCHAR2(20) DEFAULT NULL,
LAST_NAME VARCHAR(25) DEFAULT NULL,
EMAIL VARCHAR2(25) DEFAULT NULL,
PHONE_NUMBER VARCHAR2(20) DEFAULT NULL,
HIRE_DATE DATE DEFAULT NULL,
JOB_ID VARCHAR2(10) DEFAULT NULL,
SALARY NUMBER(8,2) DEFAULT NULL,
COMMISSION_PCT DECIMAL(2,2) DEFAULT NULL,
MANAGER_ID DECIMAL(6,0) DEFAULT NULL,
DEPARTMENT_ID NUMBER(4,0) DEFAULT NULL
);
greatsql> INSERT INTO tb2 (EMPLOYEE_ID, FIRST_NAME, LAST_NAME, JOB_ID, SALARY, DEPARTMENT_ID)
VALUES (145, 'John', 'Bonus Record', 'SA_MAN', 2000, 80);
greatsql> CREATE VIEW v_tb1 AS
SELECT * FROM ( SELECT * FROM tb1 UNION SELECT * FROM tb2) AS combined_tables
WHERE DEPARTMENT_ID = 80;
2、创建带有查询view的cursor的存储过程并执行sp
DROP PROCEDURE IF EXISTS `p1`;
DELIMITER $$
CREATE PROCEDURE `p1`(In p_dept_id integer)
BEGIN
declare cc cursor for SELECT EMPLOYEE_ID,DEPARTMENT_ID FROM v_tb1 WHERE department_id = p_dept_id;
open cc;
end $$
DELIMITER ;
-- 以下可以正常执行
greatsql> call p1(80);
-- 以下可以正常执行
greatsql> call p1(80);
3、改变view相关表的表结构并再次执行sp
greatsql> ALTER TABLE tb2 modify FIRST_NAME VARCHAR2(200);
-- 这里发现数据库core了
greatsql> CALL p1(80);
core的堆栈如下:
Thread 53 "connection" received signal SIGABRT, Aborted.
__pthread_kill_implementation (no_tid=0, signo=6, threadid=140736954115648) at ./nptl/pthread_kill.c:44
44 ./nptl/pthread_kill.c: 没有那个文件或目录.
(gdb) bt
#0 __pthread_kill_implementation (no_tid=0, signo=6, threadid=140736954115648)
at ./nptl/pthread_kill.c:44
#1 __pthread_kill_internal (signo=6, threadid=140736954115648) at ./nptl/pthread_kill.c:78
#2 __GI___pthread_kill (threadid=140736954115648, signo=signo@entry=6)
at ./nptl/pthread_kill.c:89
#3 0x00007ffff7442476 in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#4 0x00007ffff74287f3 in __GI_abort () at ./stdlib/abort.c:79
#5 0x00007ffff742871b in __assert_fail_base (
fmt=0x7ffff75dd130 "%s%s%s:%u: %s%sAssertion `%s' failed.\n%n",
assertion=0x55555bf8d54a "!field_info.empty()",
file=0x55555bf8d1d8 "sql/sql_derived.cc",
line=565, function=<optimized out>) at ./assert/assert.c:94
#6 0x00007ffff7439e96 in __GI___assert_fail (assertion=0x55555bf8d54a "!field_info.empty()",
file=0x55555bf8d1d8 "sql/sql_derived.cc",
line=565, function=0x55555bf8d4a0 "copy_field_info(THD*, Item*, Item*)::<lambda(Item*)>")
at ./assert/assert.c:103
#7 0x00005555591f0641 in operator() (__closure=0x7fffe027c6a0, inner_item=0x7fff28c890d0)
at sql/sql_derived.cc:565
#8 0x00005555591f4d1b in Item::walk_helper_thunk<copy_field_info(THD*, Item*, Item*)::<lambda(Item*)> >(uchar *) (this=0x7fff28c890d0, arg=0x7fffe027c6a0 "\300\306'\340\377\177")
at sql/item.h:2559
#9 0x0000555558c4de9c in Item::walk (this=0x7fff28c890d0,
processor=(bool (Item::*)(Item * const, unsigned char *)) 0x5555591f4cf4 <Item::walk_helper_thunk<copy_field_info(THD*, Item*, Item*)::<lambda(Item*)> >(uchar *)>, walk=enum_walk::PREFIX,
arg=0x7fffe027c6a0 "\300\306'\340\377\177")
at sql/item.h:2552
#10 0x0000555558d34d0d in Item_func::walk (this=0x7fff28c89258,
processor=(bool (Item::*)(Item * const, unsigned char *)) 0x5555591f4cf4 <Item::walk_helper_thunk<copy_field_info(THD*, Item*, Item*)::<lambda(Item*)> >(uchar *)>, walk=enum_walk::PREFIX,
argument=0x7fffe027c6a0 "\300\306'\340\377\177")
at sql/item_func.cc:660
#11 0x0000555558cc05ad in Item_cond::walk (this=0x7fff28c88658,
processor=(bool (Item::*)(Item * const, unsigned char *)) 0x5555591f4cf4 <Item::walk_helper_thunk<copy_field_info(THD*, Item*, Item*)::<lambda(Item*)> >(uchar *)>, walk=enum_walk::PREFIX,
arg=0x7fffe027c6a0 "\300\306'\340\377\177")
at sql/item_cmpfunc.cc:6100
#12 0x00005555591f4d6c in WalkItem<copy_field_info(THD*, Item*, Item*)::<lambda(Item*)> >(Item *, enum_walk, struct {...} &&) (item=0x7fff28c88658, walk=enum_walk::PREFIX, functor=...)
at sql/item.h:3838
#13 0x00005555591f07ce in copy_field_info (thd=0x7fff28001060, orig_expr=0x7fff28c87878,
cloned_expr=0x7fff28c88658)
at sql/sql_derived.cc:563
#14 0x00005555591f0ec1 in Query_block::clone_expression (this=0x7fff28c58840,
thd=0x7fff28001060, item=0x7fff28c87878)
从函数堆栈和调用初步发现是view里面的派生表执行了条件下推,拷贝查询语句outer条件的时候发生了core。
二、问题调查过程
从上面现象思考2个问题:
1、前两次执行 call p1(80)的时候为何没有core,只有第3次的时候core了?
2、第3次core是什么原因?
观察上面第三次的call,可以发现操作之前执行了表的alter操作,这个操作影响了第三次的运行。
分析代码之前我们先看一下这个view在查询的时候执行的条件下推是什么:
CREATE VIEW v_tb1 AS
SELECT * FROM ( SELECT * FROM tb1 UNION SELECT * FROM tb2) AS combined_tables
WHERE DEPARTMENT_ID = 80;
这个查询用了派生表,优化器为了执行的时候提升效率会先对每张表进行条件过滤,最后再做union操作,这样可以减少表联合时候查询的行数。
于是上述例子在prepare的时候将会变成:
SELECT * FROM ( SELECT * FROM tb1 WHERE DEPARTMENT_ID = 8
UNION SELECT * FROM tb2 AS combined_tables WHERE DEPARTMENT_ID = 8);
可以看到条件原来是一个,这里做条件下推的时候就需要拷贝条件分别下推给两张表,于是就需要执行Query_block::clone_expression。
下面我们看一下core的地方copy_field_info相关代码调用流程。
Item *Query_block::clone_expression(THD *thd, Item *item) {
// 从源Item解析出目标Item
Item *cloned_item = parse_expression(thd, item, this);
if (cloned_item == nullptr) return nullptr;
if (item->item_name.is_set())
cloned_item->item_name.set(item->item_name.ptr(), item->item_name.length());
// 从源Item拷贝相关信息到目标Item
if (copy_field_info(thd, item, cloned_item)) return nullptr;
}
bool copy_field_info(THD *thd, Item *orig_expr, Item *cloned_expr) {
// 以下遍历orig_expr,从orig_expr参数中抽取出FIELD_ITEM并且加入到field_info,本次例子抽取出的field是2个department_id
if (WalkItem(orig_expr, enum_walk::PREFIX,
[&field_info, &depended_from, &context](Item *inner_item) {
if (inner_item->type() == Item::FIELD_ITEM) {
Item_field *field = down_cast<Item_field *>(inner_item);
if (field_info.push_back(
Field_info(context, field->table_ref, depended_from,
field->cached_table, field->field)))
return true;
}
return false;
}))
return true;
// Copy the information to the fields in the cloned expression.
// 以下遍历cloned_expr,把从源表达式抽取出来的field信息拷贝到目标表达式cloned_expr对应的field。很显然,源和目标的field数量肯定是一样的。
WalkItem(cloned_expr, enum_walk::PREFIX, [&field_info](Item *inner_item) {
if (inner_item->type() == Item::FIELD_ITEM) {
// 这里要从源拷贝field信息,因此必须保证当前field_info还有剩余field信息,如果为空了说明源和目标的field数量不一致就有问题了。
// 本次core的地方在这里,就说明源和目标表达式的field数量不一致,因此需要调查为什么存在不一致。
assert(!field_info.empty());
Item_field *field = down_cast<Item_field *>(inner_item);
field->context = field_info[0].m_field_context;
field->table_ref = field_info[0].m_table_ref;
field->depended_from = field_info[0].m_depended_from;
field->cached_table = field_info[0].m_cached_table;
field->field = field_info[0].m_field;
field_info.pop_front();
}
return false;
});
}
分析copy_field_info函数执行时候拆解出来的具体Item元素,用下表显示:
| 执行次数 | orig_expr条件 | 条件包含的参数 | 参数包含的Item元素 |
|---|---|---|---|
| 第一次 | Item_cond_and | Item_func_eq (DEPARTMENT_ID = 80) Item_func_eq (DEPARTMENT_ID = p_dept_id) | DEPARTMENT_ID = 80 分别是Item_field和Item_int DEPARTMENT_ID = p_dept_id 分别是Item_field和Item_splocal |
| 第三次 | Item_cond_and | Item_func_eq (DEPARTMENT_ID = 80) Item_func_eq (DEPARTMENT_ID = p_dept_id) | DEPARTMENT_ID = 80 分别是Item_field和Item_int DEPARTMENT_ID = p_dept_id 分别是Item_field和Item_field(问题点) |
通过对比表可以很清楚的看到有问题的第三次的最后一个Item变为了Item_field,而正确执行的第一次是解析为Item_splocal。就是这个field数量差异导致了core。
第三次执行的时候为什么Item会跟第一次不一样呢?我们注意到第三次之前做了alter table的操作,这个操作导致表版本更新,于是触发了cursor的查询语句的reprepare操作。对应的代码如下:
bool sp_lex_instr::validate_lex_and_execute_core(THD *thd, uint *nextp,
bool open_tables) {
while (true) {
DBUG_EXECUTE_IF("simulate_bug18831513", { invalidate(); });
// 如果表版本有更新,就需要重新parse这个语句对应的查询语句
if (is_invalid() ||
((m_lex->has_udf()) &&
!m_first_execution)) {
free_lex();
LEX *lex = parse_expr(thd, thd->sp_runtime_ctx->sp);
if (!lex) return true;
set_lex(lex, true);
m_first_execution = true;
}
}
上面代码执行parse_expr的时候,在下面函数重新解析p_dept_id这个参数。可以发现就是因为lex->find_variable没有找到对应的参数最后一个元素才变为Item_field
bool PTI_simple_ident_ident::itemize(Parse_context *pc, Item **res) {
if (!is_with_func_ident &&
// 如果lex找到用户定义的参数就解析为Item_splocal
(spv = lex->find_variable(ident.str, ident.length, &rh))) {
*res = create_item_for_sp_var(
thd, ident, rh, spv, sp->m_parser_data.get_current_stmt_start_ptr(),
raw.start, raw.end);
lex->safe_to_cache_query = false;
} else
// 如果lex没找到用户定义的参数就解析为Item_field
*res = new (pc->mem_root) Item_field(POS(), NullS, NullS, ident.str);
}
GDB看一下find_variable为什么没有找到这个变量,打印下面spc的值发现是nullptr,因此发现是reparse查询语句以后lex->get_sp_current_parsing_ctx()变为空了,所以导致第三次没有找到对应的变量。
LEX::find_variable (this=0x7fffe047fc00, name=0x7fff30d9a918 "p_dept_id", name_len=9, ctx=0x7fffe047b7b0, rh=0x7fffe047b7f8, also_find_udt=true) at /home/wuyy/greatdb/gitmerge/percona-server/sql/sql_lex.cc:5955
5955 sp_pcontext *spc = get_sp_current_parsing_ctx();
(gdb) n
5957 if (spc && ((spv = spc->find_variable(name, name_len, false)) ||
(gdb) s
5967 sp_package *pkg = sphead ? sphead->get_package() : nullptr;
(gdb) p spc
$16 = (sp_pcontext *) 0x0
三、问题解决
从上面分析结论,我们发现是set_lex的时候没有把旧的sp_pcontext和sp_head值赋给新的lex,因此在set_lex的时候进行赋值就可以解决问题。
void sp_lex_instr::set_lex(LEX *lex, bool is_lex_owner) {
free_lex();
m_lex = lex;
m_is_lex_owner = is_lex_owner;
m_lex_query_tables_own_last = nullptr;
if (m_lex) {
m_lex->sp_lex_in_use = true;
// 添加下面的赋值代码,判断当前lex有变化的时候进行赋值
// It needs m_parsing_ctx and sphead in parse_expression to parse expr.
if (!m_first_execution) {
m_lex->set_sp_current_parsing_ctx(m_parsing_ctx);
m_lex->sphead = current_thd->sp_runtime_ctx->sp;
}
}
}
void sp_lex_instr::free_lex() {
if (!m_is_lex_owner || !m_lex) return;
/* Prevent endless recursion. */
m_lex->sphead = nullptr;
// 释放的时候需要注意不能把旧的sp_pcontext删了,因此需要先置空
if (!m_first_execution) m_lex->set_sp_current_parsing_ctx(nullptr);
lex_end(m_lex);
destroy(m_lex->result);
}
编译以后再次运行call p1,问题解决。
greatdb> alter table tb2 modify FIRST_NAME VARCHAR2(200);
greatdb> call p1(80);
Query OK, 0 rows affected (0.02 sec)
四、问题总结
针对 MySQL 8.4.4 调试版环境下,视图+游标联动场景触发数据库Core的问题做了完整排查与修复:修改视图关联表结构后,会触发游标语句重新解析,因重解析时存储过程上下文丢失,导致输入参数解析异常、字段数量不匹配,最终触发断言崩溃。
解决方案为补全语句重解析时的**存储过程上下文传递逻