3
3
4
1
专栏/.../

一次元数据锁MDL故障排查经历

 Jellybean  发表于  2024-04-30

背景

业务侧同学要在生产环境执行一些发版任务,在有业务流量不断访问原有库表的情况下,执行一系列相关的 DDL 操作,如 Truncate Table、CREATE Table、Add Column、Modify Column 等。

在执行的过程中出现了修改列的 DDL 长时间没执行完成的现象,正常情况下该操作几秒钟就可以完成,进而导致 DDL 队列不断阻塞,业务侧出现大量 DDL 等待现象。

故障现象

业务侧在集群通过脚本的方式,提交了一批 DDL 任务,但是提交的任务出现一直阻塞、无法执行成功的现象,及时开发同学手动尝试 Cancel 具体的 DDL 任务也无法正常取消。导致后续的发版安排无法正常推进,在发版窗口期越来越小的时候,赶紧升级了问题过来。

问题分析

DDL 执行原理

首先,在开始分析问题之前,确认集群的版本情况 select version(),是比较新的 v7.1.3 版本。

image.png

在当前这个版本里,考虑到加索引是一个耗时比较久的重操作,其他调整表结构属性的 DDL 是元数据信息变更的轻量操作。官方在新版本里优化了这里的逻辑,把 DDL 队列主要分为了下面两种:

  • 队列1:索引队列

    • 主要负责加索引、删除索引等索引的变更操作。
  • 队列2:普通 DDL 队列

    • 主要负责建库建表、删库删表、加列减列、调整列属性等 DDL 变更操作。

具体来说就是在 v6.2.0 及之后,TiDB 引入了并行 DDL 处理流程:

  • DDL Owner 能够并行执行 DDL 任务。
  • 改善了 DDL Job 队列先入先出的问题。DDL Owner 不再是 FIFO,而是选择当前可以执行的 DDL Job。
  • 因为 TiDB 中的 DDL 都是在线变更,通过 Owner 即可对新的 DDL Job 进行相关性判断,并根据相关性结果进行 DDL 任务的调度,从而使分布式数据库实现了和传统数据库中 DDL 并发相同的效果。

image.png

如上图,并行 DDL 的设计是将 add index job 放到新增的 add index job queue 中去,其它类型的 DDL job 还是放在原来的 job queue。

相应的,也增加一个 add index worker 来处理 add index job queue 中的 job。新的并发 DDL 执行框架的实现进一步加强了 DDL 语句的执行能力,并更符合用户的使用习惯。

是否加大表索引阻塞DDL?

根据执行 Admin show ddl jobs 得到的输出结果,如下面图所示,我们可以看到只有 modify column、truncate table 和 drop table 这三类普通的 DDL,没有耗时比较久的加索引操作。不属于大表加索引阻塞 DDL 任务的情况

image.png

modify column、truncate table 和 drop table 这类 DDL 同属于普通队列,如果最前面的 DDL 任务没有及时执行完毕,则会阻塞住后面的任务。

是否常规 DDL 阻塞问题?

查看 DDL 任务队列,第一个 55296 的任务是 modify column 的操作,其状态是 done,说明已经在存储层执行完毕操作,在准备完成状态的变更操作。queueing 表示正处在任务队列中,待执行。synced 表示 DDL 任务已经正式完成。

这里尝试把把第一行的任务 Cancel ,查看效果:

image.png

结果是无法取消 DDL 队列的第一个任务,原因是任务已经完成执行了,直接报错:[ddl:8225lThis job:55296 is finished, so can't be cancelled

既然任务已经执行完毕,但是状态无法正常更新,现象着实诡异!

元数据锁问题定位

正在排查问题的时候,开发一波灵活拷问:“这些 DDL 很简单怎么会阻塞?为什么会阻塞?如何解决?” !确实如此,只是很简单的 modify column 语句,目前已经阻塞了数小时,确实不应该。没有头绪的时候,去搜查官方文档和社区论坛,主要获得了下面的几点关键信息:

  • 考虑到当前版本是比较新的 v7.1.3 版本,就去查看了最近这几个大版本的发布 Change Log情况,发现 TiDB 在 v6.3.0 中引入元数据锁,并在 v6.5.0 及之后的版本默认打开。
  • 其次,在 TiDB 集群问题导图里有一个章节讨论到 DDL 阻塞的问题,TiDB DDL job 卡住不动/执行很慢(通过 admin show ddl jobs 可以查看 DDL 进度),如果 DDL 涉及的表与当前未提交事务涉及的表存在交集,则会阻塞 DDL 操作,直到事务提交或者回滚。
  • asktug 社区论坛有不少用户在 v6.5.x 版本有遇到过 DDL 阻塞的问题,而原因都和元数据锁 MDL 有或多或少的关联。

综上信息,怀疑当前问题是元数据锁的问题。有了个问题方向,就开始验证。具体如下

确认元数据锁数量情况

 SELECT count(0) FROM mysql.tidb_mdl_view;
 SELECT * FROM mysql.tidb_mdl_view\G

查看集群发现元数据锁系统表,竟然有47行内容!说明当前集群,此时此刻有不少的 MDL 锁!

验证元数据锁和当前问题相关

通过下面语句获取到最早的元数据锁信息,可以看到是 modify column 的 DDL 被一条 Select 语句所阻塞,这条业务 Select 语句是一个大SQL,由于是慢查询所以一直没有执行完毕,导致持续持有该表的 MDL,导致后续的对于该表的 DDL 都被阻塞了。后续需要业务优化该 SQL !

 SELECT * FROM mysql.tidb_mdl_view order by TxnStart limit 1 \G

尝试取消这个 Select 语句的持有锁情况

 kill 6023665656678139457; 
 SELECT * FROM mysql.tidb_mdl_view order by TxnStart limit 1 \G

取消第一把锁以后,发现第一个被阻塞的 DDL 可以正常执行!找到了该问题的原因,就是 MDL!

元数据锁问题!

访问Grafana通过监控 TiDB -> DDL > DDL META OPM 查看问题时间段的 Owner 所在的 tidb-server 节点。

image.png

如上图,找到了 DDL owner节点,同时也可以看到 DDL META OPM 操作比较频繁,侧面验证了问题所在。

如果是 MDL 元数据锁的问题,可以在 ddl owner 节点的日志中搜索下 old running transaction block DDL 关键字看看,类似于下面的日志:

 [INFO] [session.go:4323] ["old running transaction block DDL"] ["table ID"=59840] [jobID=59901] ["connection ID"=3371080660529549211] ["elapsed time"=6h12m17.659493018s] 

经过查看确认,确实在 DDL Owner 节点找到了类型的关键日志,石锤了就是 MDL 的问题。

image.png

元数据锁说明

我们都知道,在 TiDB 中对表元数据对象的更改,采用的是 Online DDL 在线异步变更算法。SQL 语句在执行时会获取开始时对应的元数据快照,如果事务执行过程中所涉及的表上发生了元数据的更改,为了保证数据的一致性,之前的 TiDB 会返回 Information schema is changed 的错误,导致用户事务提交失败,用户体验不好。

所以官方为了解决这个问题,在 TiDB v6.3.0 中,online DDL 算法中引入了元数据锁特性。通过协调表元数据变更过程中 DML 语句和 DDL 语句的优先级,让执行中的 DDL 语句等待持有旧版本元数据的 DML 语句提交,尽可能避免 DML 语句报Information schema is changed 的错误而执行失败。

在 v6.5.0 及之后的版本中,TiDB 默认开启元数据锁。当集群从 v6.5.0 之前的版本升级到 v6.5.0 及之后的版本时,TiDB 会自动开启元数据锁功能。如果要关闭元数据锁,可以将系统变量 tidb_enable_metadata_lock 设置为 OFF。元数据锁适用于所有的 DDL 语句,包括但不限于:

使用元数据锁的影响:

  • 对于 DML 语句来说,元数据锁不会导致 DML 语句被阻塞,因此也不会存在死锁的问题。
  • 开启元数据锁后,事务中某个元数据对象的元数据信息在第一次访问时确定,之后不再变化。
  • 对于 DDL 语句来说,在进行元数据状态变更时,会被涉及相关元数据的旧事务所阻塞。

解决方法

定位到元数据锁的问题后,只要正常去掉持有的锁,就可以解决问题。

具体为:

  • 方案1:不做操作,就是等。

    • 由于这里执行锁是正常的操作,所以可以等待正常执行流程,继续等待这些select继续执行完,自动恢复。
    • 等待恢复正常后,业务需要优化执行的DML SQL,避免长时间执行。
  • 方案2:取消 DML执行,释放 MDL。

    • 通过 tidb_mdl_view 系统表可以获取到全部持有 MDL 的 DML 语句,然后一一执行 kill 操作,释放持有的锁,DDL 就可以正常执行。
    • 手动 kill 持有 MDL 的语句,通过这个方式拿到id,SELECT concat('kill ',session_id,';') FROM mysql.tidb_mdl_view order by TxnStart; 然后再 kill。
  • 方案3:重启 tidb-server 集群。

    • 相当于关闭全部 DML 的链接,相当于 kill Query 释放 MDL。
    • 在中控机执行:tiup cluster restart {集群名} -R tidb

方案1明显不行,已经等待太久了,现在正是需要处理的时候。

方案3太过粗暴,不够灵活。

相比之下,方案2比较优雅,采取该方式进行恢复。需要做的是遍历整个tidb_mdl_view 表,构造 kill 语句来执行。 image.png

如上图是执行后,DDL 任务队列全部任务都正常执行完毕,处于 synced 状态,一切恢复正常!业务得以在最后时刻完成发版!

总结

针对此次故障排查经历,事后反思和复盘,可以有几点感悟:

  • 导致此次根因的问题,应该是持有元数据锁 MDL 的 DML 语句执行太久,导致长时间持有元数据锁 MDL,进而阻塞了 DDL 队列。这里需要协助和督促业务方及时进行 SQL 的优化,以缩短 MDL 的持有时长。
  • 在问题恢复后,如果宁愿出现 DML 语句报Information schema is changed 的错误而失败,也不愿意 DDL 卡主毫无提示,可以通过将系统变量 tidb_enable_metadata_lock 设置为 OFF来关闭该功能。
  • 官方需要针对该场景进一步完善 MDL 的机制,起码在等待超过一段时间,需要实现回滚或者给出明显的告警提示。
  • 线上故障问题错综复杂,在毫无头绪的时候,尽可能去官网和社区收集相关的资料和类似 case,往往会有会启发!

希望这一次的故障排查案例,能够帮助到后面有需要的同学!

3
3
4
1

版权声明:本文为 TiDB 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
暂无评论