破防了,谁懂啊家人们:记一次mysql问题排查

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS PostgreSQL,集群系列 2核4GB
简介: 某天用户反馈线上产品报错,本文记录了这次mysql问题排查和修复的过程,希望给大家参考。

一、前言

谁懂啊家人们,作为一名java开发,原来以为mysql这东西,写写CRUD,不是有手就行吗;你说DDL啊,不就是设计个表结构,搞几个索引吗。


键盘撒一把冻干,我家猫也能来上班。——粥师傅

结果一次线上出问题了,一环接一环,不仅猫上不了班,晚上还得等我加班回家,眼巴巴吃不到冻干。

image.png


1.1 表结构


简单介绍一下出问题的表。


一张元数据表,提取出重点部分,抽象出来的结构如下,


(id, group, code, name,property1, property2, ...)

id group code name property
1 业务1 事件1 吃冻干
2 业务1 事件2 喂猫粮
3 业务2 事件1 睡觉
4 业务3 事件10086 下班
... ... ... ...


主键primary key:id


唯一键unique key:group + code,


也就是说在该group内,code是唯一的。


此外,我们有一个dataworks离线任务,每天会往该表中写入记录,采用insert ignore into的方式,如果遇到重复的group+code,就不写入。


整体逻辑比较清晰明了。数据量级也比较小,每个group大约几百上千条数据,总数据量不到10w。


二、问题排查和修复过程

2.1 最初的问题

某天用户反馈线上产品报错,迅速排查发现,上述表中新接入了一个业务:在dataworks接入了一个新的group(假设名字叫bad_group),同步任务在当天异常往mysql表里导了千万量级数据(其中实际有效的只有几千条,其余为脏数据),导致线上产品查询缓慢、报错。定位到问题以后,第一反应是把错误的bad_group的数据先全部清掉,保留其他group的数据,恢复上线查询,然后再慢慢想办法重新导入正确数据。


顺带一提,以下SQL执行等全程都使用弹内DMS平台进行操作。

2.2 初步思路

清理错误数据v1


DELETE FROM MY_TABLE 
WHERE group = 'bad_group';

直接执行上面这个SQL进行普通数据变更可行吗?显示不行,有经验的同学都知道,在千万量级下,清理大量数据会超过binlog限制,导致SQL无法被执行。


因此我们直接用的是另一个方案,无锁数据变更,SQL依旧和上面保持一致,关于无锁变更的描述可见平台的介绍:

image.png

本以为用无锁变更差不多就能解决问题了,然而执行过程中发现由于数据量比较大,无锁变更分批执行SQL效率非常低,估算大概要2h以上来清空这几千万的脏数据,不能接受这个方案,执行了几分钟果断放弃。


2.3 另辟蹊径


于是只能换一种方式。重新考虑这个问题,我们需要保留的数据仅仅只有千万中的不到10万条非bad_group的数据,因此除了删除bad_group数据这种方法,更简单的是将有效数据先copy到一张临时表中,然后drop原表,再重新创建表,将临时表中数据拷贝回来。为什么drop表会比delete数据快呢,这也是一个重要知识点。



DROP TRUNCATE DELETE
删除内容 删除整张表数据,表结构以及表的索引、约束和触发器 删除全部数据 删除部分数据(可带where条件)
语句类型 DDL DDL DML
效率 最高 较高 较低
回滚 无法回滚 无法回滚 可以回滚
自增值 - 重置 不重置


举个不那么恰当的例子,好比房东把房子租给别人,到期后发现房子里全都是垃圾,DELETE语句是将这些垃圾一件一件清理出来,只保留原来干净的家具。TRUNCATE相当于一把火把房子里所有东西都烧了,DROP语句就是房子直接不要了。


这里drop和truncate的方案都可以选择,我们采用了房子不要了的方案,直接drop表:


清理错误数据v2


-- 将正常数据复制到临时表
CREATE TABLE TEMP_TABLE AS SELECT * FROM MY_TABLE WHERE group <> 'bad_group';

-- 删除原表
DROP TABLE MY_TABLE;

-- 将临时表重命名为原表
RENAME TABLE TEMP_TABLE TO MY_TABLE;

执行成功后,count(*)了一把数据量级,发现确实回到正常水准,于是问题就那么初步解决了。然而如果问题那么容易就解决了,那就不会记录在ATA。上面的SQL留下了一个巨坑,有经验的同学可能一眼就看出来了😭😭😭,如果没有看出来的话,继续下文。


2.4 表坏了


当天一切正常。然而好景不长,第二天,有同学往表里导数时发现了问题,在没有指定id的情况下,灌入的所有行id=0。我一脸黑人问号?


id不是默认主键吗,怎么会这样,重新打开表结构一看,所有的索引都消失了!


此时心里凉了半截,马上回想到一定是这个语句有问题:



-- 将正常数据复制到临时表
CREATE TABLE TEMP_TABLE AS SELECT * FROM MY_TABLE WHERE group <> 'bad_group';

赶紧问了下GPT:

image.png

果不其然,create table as 只会复制表的列信息结构和数据,不会复制表索引、主键等信息。

也就是说,这张表已经被玩坏了!现在回看这个问题,当时至少有两种方式避免这个问题:


  1. 不使用drop语句。使用truncate语句,保留原表结构。


清理错误数据v3


-- 将正常数据复制到临时表
CREATE TABLE TEMP_TABLE AS SELECT * FROM MY_TABLE WHERE group <> 'bad_group';

-- 清空原表数据,但不删除表
TRUNCATE TABLE MY_TABLE;

-- 将临时表数据插入到原表
INSERT INTO MY_TABLE SELECT * FROM TEMP_TABLE;
  1. 使用CREATE TABLE LIKE 语句创建临时表,复制原表结构。

清理错误数据v4


-- 创建和原表结构一样的临时表
CREATE TABLE TEMP_TABLE LIKE MY_TABLE;

-- 将正常数据复制到临时表
INSERT INTO TEMP_TABLE SELECT * FROM MY_TABLE WHERE group <> 'bad_group';

-- 删除原表
DROP TABLE MY_TABLE;

-- 将临时表重命名为原表
RENAME TABLE TEMP_TABLE TO MY_TABLE;


2.5 我觉得还能抢救一下

情况就是这么个情况,只能看看怎么抢救!

id group code name property
1 业务1 事件1 吃冻干
2 业务1 事件2 喂猫粮
3 业务2 事件1 睡觉
4 业务3 事件10086 下班
... ... ... ...
0(新导入) 业务1(重复数据) 事件1(重复数据) 吃冻干
0(新导入) 业务1(重复数据) 事件2(重复数据) 喂猫粮
0(新导入) 业务1 事件3 吃罐头
... ... ... ...

主键缺失导致插入了许多条id为0的数据,但应用不依赖mysql的自增id,暂时不影响线上应用查询结果;group+code的unique key缺失导致可能插入了重复数据,但应用侧做了去重兜底逻辑。也就是说不幸中的万幸,产品侧暂时无感,赶紧想办法挽回。


该表同步数据的方式是:如果唯一键冲突则忽略,否则就导入成功。新导入的这批数据由于缺失主键和唯一键,id全部为0且有重复,但其实只有一部分是需要保留的,另一部分需要根据唯一键去重。


此时我需要完成两件事:


  1. 保留原有数据的同时,将表的主键、唯一键和查询索引进行重建。


  1. 将今天新导入的id=0的数据根据原唯一键的规则重新导入。


但我们知道,执行添加唯一键的语句时,会检查此时表里是否有不满足唯一的数据,如果有的话该语句会被拒绝执行。因此这批带有重复的新数据的干扰,不能直接alter table add unique key。


灵机一动,采取和昨日一样的临时表方案,即先将id=0的数据复制到临时表,删除原表中所有id=0的数据,然后重建索引,再将id=0的数据使用insert ignore into语句导回来。对应的SQL:


重建表


-- 1.复制id=0的数据到临时表,
CREATE TABLE TEMP_TABLE AS SELECT * FROM MY_TABLE WHERE id = 0;

-- 2.删除源表中id=0的记录
DELETE FROM MY_TABLE WHERE id = 0;

-- 3.重建索引
ALTER TABLE MY_TABLE ADD INDEX ...;

-- 4.导回id=0的新数据
INSERT IGNORE INTO MY_TABLE SELECT * FROM TEMP_TABLE;

仔细思考,这次使用CREATE TABLE AS是没有问题的,因为这张临时表并不重要。DELETE由于数据量不大也没有性能问题。出于谨慎,上述4个SQL也是通过4个工单一个个提交执行的,便于中间过程观察。思路清晰,这次应该ok!


当执行完上面第2条语句,删除id=0的数据后,执行了select count(*)简单确认了一下,没想到这一确认还真出了问题,delete过后数据条数没有变?!经过紧张的思考🤔,新机子哇伊自摸一刀子:猜测大概率是主备没有实时同步。关于这一点,我们线上用的MYSQL是主库,工单执行的SQL也是在主库执行,但DMS控制台为了不影响线上正常使用,是在备库进行查询,正常情况下主备库会实时同步。但当一些耗时SQL执行时,就会出现同步延迟。为了验证这一点,可以在主库select count(*),DMS也提供了切换选项,只是默认会选备库。

image.png

这张截图是后来我咨询了DBA后帮忙查询到的结果,确实是有延迟。

image.png

继续重建索引,包括主键primary key、唯一键unique key、普通索引key。没有问题。


最后一步,将id=0的数据从临时表导回原表,就可以回家喂🐱了,然而工单一直执行报错。


[ERROR] Duplicate entry '0' for key 'PRIMARY'【解决方法】:https://help.aliyun.com/document_detail/198139.html
TraceId : 0b8464d617047224212725080d867f

百思不得其解,按理想情况,重新导回数据后,id应该是从此刻的最大id开始自增才对(假设表中有10000条数据,那么新插入的数据理应id=10001),为什么还是0,并且还重复了?难道是之前的CREATE TABLE AS语句导致auto increment被清为0了?


按照这个思路,回忆起之前在日常环境写假数据的时候,如果指定了一个比较大的id,那么后续所有新数据都会在这个id基础上生成(比如当前表中只有10条记录,id=10,插入一条id=100的数据,后续数据就会接着id=101继续生成。)尝试过后发现依旧报错。


我有点汗流浃背了。

image.png

为什么不管用?又用GPT查询了设置表auto increment值的方法:

ALTER TABLE MY_TABLE AUTO_INCREMENT = 10001;

然而仍然报这个错误。


绝望。


此时已经夜里快十点,周围没有什么人了,本来空调澎湃吹动热气的声音也不知不觉趋于安静,我望向对面楼栋,灯光明灭可见。一月小寒的夜晚有些冷,我突然想起李清照的那句“冷冷清清,凄凄惨惨戚戚”,不就在描绘这个场景吗?


最后的最后,再次对比日常库的正常表结构,发现原来是id的auto increment也消失了。原来还是create table as 留下来的坑,难怪之前重新设置auto increment也不生效。为什么没有第一时间发现到这一点,因为按上面gpt的回答,该语句对"列结构"是可以正常复制的,只有索引、主键等信息会丢失,原以为"AUTO_INCREMENT"是属于id这一列的列信息,看起来并不是。

image.png

重新设置id使用自增:


MODIFY COLUMN `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID';

至此问题解决。


三、总结

一切的问题源自对create table as这个语句的不熟悉,这个语句建表导致的表主键、索引、auto_increment的丢失。


不熟悉的SQL不能乱跑😭😭😭


后面也在反思在线上使用drop和truncate有些激进。不过当时考虑到是内部应用并且查询已经不可用了。也欢迎读者同学们思考和反馈,针对这样的场景是否有更好处理建议。


 顺便说明:后续我们针对odps导入mysql源头就做了限制,防止这类事情再次发生。





来源  |  阿里云开发者公众号

作者  |  温粥

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
SQL 关系型数据库 MySQL
Java 最常见的面试题:mysql 问题排查都有哪些手段?
Java 最常见的面试题:mysql 问题排查都有哪些手段?
|
1月前
|
SQL 关系型数据库 MySQL
MySQL如何排查和删除重复数据
该文章介绍了在MySQL中如何排查和删除重复数据的方法,包括通过组合字段生成唯一标识符以及使用子查询和聚合函数来定位并删除重复记录的具体步骤。
116 2
|
2月前
|
SQL 关系型数据库 MySQL
遇到mysql数据库死锁,你会怎么排查?
遇到mysql数据库死锁,你会怎么排查?
172 0
|
2月前
|
SQL 关系型数据库 MySQL
(十八)MySQL排查篇:该如何定位并解决线上突发的Bug与疑难杂症?
前面《MySQL优化篇》、《SQL优化篇》两章中,聊到了关于数据库性能优化的话题,而本文则再来聊一聊关于MySQL线上排查方面的话题。线上排查、性能优化等内容是面试过程中的“常客”,而对于线上遇到的“疑难杂症”,需要通过理性的思维去分析问题、排查问题、定位问题,最后再着手解决问题,同时,如果解决掉所遇到的问题或瓶颈后,也可以在能力范围之内尝试最优解以及适当考虑拓展性。
161 3
|
3月前
|
消息中间件 关系型数据库 MySQL
实时计算 Flink版操作报错合集之运行mysql to doris pipeline时报错,该如何排查
在使用实时计算Flink版过程中,可能会遇到各种错误,了解这些错误的原因及解决方法对于高效排错至关重要。针对具体问题,查看Flink的日志是关键,它们通常会提供更详细的错误信息和堆栈跟踪,有助于定位问题。此外,Flink社区文档和官方论坛也是寻求帮助的好去处。以下是一些常见的操作报错及其可能的原因与解决策略。
|
5月前
|
SQL 运维 关系型数据库
记一次 MySQL 主从同步异常的排查记录,百转千回!
这篇文章主要讲述了在 MySQL 主从同步过程中遇到的一个问题,即从库的 SQL 线程因 Relay Log 损坏导致同步停止。作者首先介绍了现象,从库的 Slave_IO_Running 正常,但 Slave_SQL_Running 停止,报错信息提示可能是 binlog 或 relay log 文件损坏。
161 6
|
11月前
|
SQL 存储 关系型数据库
mysql 利用 performance_schema 排查 qps 过高过程记录
mysql 利用 performance_schema 排查 qps 过高过程记录
217 0
|
11月前
|
Java 关系型数据库 MySQL
记一次mysql线上问题排查
背景是这样的,我们有个系统每天都会调起多个定时任务,首先quartz每分钟会调起一次检查时间的任务,如果发现时间到达设定的任务执行时间,java代码会向数据库里写入一条记录,然后有另外一个系统就会根据这条记录执行相应的任务,有天有同事反馈说有条定时任务没执行。。
80 0
|
SQL 监控 Oracle
MySQL发现sql语句执行很慢排查建议
MySQL发现sql语句执行很慢排查建议
494 0
|
SQL 关系型数据库 MySQL
mysql线上连接超时和字段过长排查思路
mysql线上连接超时和字段过长排查思路