mysql服务治理

一、数据库性能指标

1.QPS

一台 MySQL 数据库,大致处理能力的极限是,每秒一万条左右的简单 SQL,这里的“简单 SQL”,指的是类似于主键查询这种不需要遍历很多条记录的 SQL。

根据服务器的配置高低,可能低端的服务器只能达到每秒几千条,高端的服务器可以达到每秒钟几万条,所以这里给出的一万 TPS 是中位数的经验值。考虑到正常的系统不可能只有简单 SQL,所以实际的 TPS 还要打很多折扣。

我的经验数据,一般一台 MySQL 服务器,平均每秒钟执行的 SQL 数量在几百左右,就已经是非常繁忙了,即使看起来 CPU 利用率和磁盘繁忙程度没那么高,你也需要考虑给数据库“减负”了。

1.QPS 突增问题

有时候由于业务突然出现高峰,或者应用程序 bug,导致某个语句的 QPS 突然暴涨,也可能导致 MySQL 压力过大,影响服务。

我之前碰到过一类情况,是由一个新功能的 bug 导致的。当然,最理想的情况是让业务把这个功能下掉,服务自然就会恢复。

而下掉一个功能,如果从数据库端处理的话,对应于不同的背景,有不同的方法可用。我这里再和你展开说明一下。

  1. 一种是由全新业务的 bug 导致的。假设你的 DB 运维是比较规范的,也就是说白名单是一个个加的。这种情况下,如果你能够确定业务方会下掉这个功能,只是时间上没那么快,那么就可以从数据库端直接把白名单去掉。

  2. 如果这个新功能使用的是单独的数据库用户,可以用管理员账号把这个用户删掉,然后断开现有连接。这样,这个新功能的连接不成功,由它引发的 QPS 就会变成 0。

  3. 如果这个新增的功能跟主体功能是部署在一起的,那么我们只能通过处理语句来限制。这时,我们可以使用上面提到的查询重写功能,把压力最大的 SQL 语句直接重写成"select 1"返回。

当然,这个操作的风险很高,需要你特别细致。它可能存在两个副作用:

  1. 如果别的功能里面也用到了这个 SQL 语句模板,会有误伤;

  2. 很多业务并不是靠这一个语句就能完成逻辑的,所以如果单独把这一个语句以 select 1 的结果返回的话,可能会导致后面的业务逻辑一起失败。

所以,方案 3 是用于止血的,跟前面提到的去掉权限验证一样,应该是你所有选项里优先级最低的一个方案。

同时你会发现,其实方案 1 和 2 都要依赖于规范的运维体系:虚拟化、白名单机制、业务账号分离。由此可见,更多的准备,往往意味着更稳定的系统。

2.IOPS

1.定义

在数据库性能优化的过程中,了解IOPS(Input/Output Operations Per Second,即每秒输入/输出操作数)是至关重要的。

IOPS是衡量数据库磁盘性能的重要指标之一,可以帮助我们了解数据库在处理输入输出操作时的效率和吞吐量。

IOPS的计算公式如下所示:

IOPS = 请求的IO总数 / 测试时间

IOPS的数值受多个因素的影响,主要包括以下几个方面:

1.硬件设备

数据库的硬件设备对IOPS有着直接的影响。例如,硬盘类型(机械硬盘还是固态硬盘)、硬盘容量、硬盘转速等都会影响IOPS的数值。一般来说,固态硬盘的IOPS要远高于机械硬盘。

2.数据库参数配置

数据库的参数配置也会对IOPS产生一定的影响。例如,innodb_io_capacity参数可以用来控制MySQL的IOPS性能,适当调整该参数的值可以提升数据库的IOPS表现。

那么如果当数据库的io负载过高,会带来哪些影响?

1.响应时间延长

因为没有io资源读写磁盘了,自然响应时间会延长

2.系统崩溃

极端情况超过系统io能力会导致系统崩溃重启等。

2.IOPS监控

可以使用一些专门的监控工具来监控数据库的IOPS。例如,MySQL自带的Performance Schema可以提供详细的性能统计信息,包括IOPS的数值。另外,还有一些第三方监控工具如pt-diskstats、mpstat等也可以提供有关IOPS的监控信息。

3.提升IOPS

思考一个问题:如果你的 MySQL 现在出现了性能瓶颈,而且瓶颈在 IO 上,可以通过哪些方法来提升性能呢?

针对这个问题,基于将io压力转移到内存的思路,可以考虑以下三种方法:

  1. 设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 参数,减少 binlog 的写盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。

  2. 将 sync_binlog 设置为大于 1 的值(比较常见是 100~1000)。这样做的风险是,主机掉电时会丢 binlog 日志,但也总比io负载过高好

  3. 将 innodb_flush_log_at_trx_commit 设置为 2。这样做的风险是,主机掉电的时候会丢数据。

我不建议你把 innodb_flush_log_at_trx_commit 设置成 0。因为把这个参数设置成 0,表示 redo log 只保存在内存中,这样的话 MySQL 本身异常重启也会丢数据,风险太大。

而 redo log 写到文件系统的 page cache 的速度也是很快的,所以将这个参数设置成 2 跟设置成 0 其实性能差不多,但这样做 MySQL 异常重启时就不会丢数据了,相比之下风险会更小,因为page cache是操作系统层面的概念,它被所有进程共享,只有当操作系统物理内存不足等原因才会进行刷盘操作,但如果主机掉电或者关机后page cache会丢失。

3.连接池资源

1.连接池大小设置

先说结论

下面公式由 PostgreSQL 提供,它适用于市面上绝大部分数据库产品。还有,你应该模拟预期的访问量,并通过下面的公式先设置一个偏合理的值,然后在实际的测试中,通过微调来寻找最合适的连接数大小。

连接数 = (核心数 *2) + 有效磁盘数

按照这个公式,如果你的服务器 CPU 是 4 核的,连接池大小应该为(4 * 2)+ 1 = 9,取个整我们就设置为 10。如果你跑个性能测试就可以看到,它能轻松支撑 3000 用户以 6000TPS 的速率并发执行简单查询的场景。

那么我们思考一下下面的问题

连接数设置过大会怎样?

1.资源浪费

设置过大的最大连接数可能导致过多的数据库连接被创建,占用过多的系统资源(如内存、线程等),从而浪费资源。这可能会导致应用程序运行缓慢,并使其他系统组件的性能受到影响。

2.负载过高

如果最大连接数设置过大,可能导致连接池无法管理和处理如此多的连接,从而导致连接泄露、内存泄漏或系统崩溃

连接数设置过小会怎样?

1.增加访问时间

最小连接数设置过低,当应用程序需要建立新的数据库连接时,可能需要等待连接池中的连接被释放或新的连接被创建。这将增加连接的延迟时间,并降低应用程序的响应性能,并且连接线程频繁的创建和消耗也会消耗操作系统资源。

下面分享一个案例

某个团队对 Oracle 数据库进行了压力测试,模拟 9600 个并发线程来操作数据库,每两次数据库操作之间 sleep 550ms,最开始的时候设置的线程池大小为 2048。测试结果是:每个请求要在连接池队列里面等待 33ms,获得连接之后,执行 SQL 需要耗时 77ms,CPU 消耗维持在 95%左右。

接下来将连接池大小改小点,设置成 1024, 其他测试参数不变,测试结果:获取连接等待时长基本不变,但是 SQL 的执行耗时降低了。

接下来再设置小一些,连接池大小降低到 96, 并发数等其他参数不变,测试结果:每个请求再连接池队列中的平均等待时间为 1ms,SQL 执行耗时为 2ms。

我们没有调整任何东西,仅仅只是将数据库连接池的大小降低了,这样就能把之前平均 100ms 的相应时间缩短到了 3ms,吞吐量指数级上升。

然后我们在看一下一些常见的命令

1.查看当前连接线程数

SHOW STATUS LIKE 'Threads%'; -- 显示线程状态信息

从图中我们看到建立连接的线程数为19个

然后执行show full processlist命令,可以看到已连接线程的详细信息

从图中我们发现,在业务低谷的时候,其实大部分连接线程都是处于空闲状态的,它会占据一定的cpu资源。

一般建议5 至 20 之间

2.查看当前最大连接数

SHOW VARIABLES LIKE 'max_connections'; -- 显示最大连接数

MySQL默认的最大连接数为100,MySQL服务器允许的最大连接数16384。、

常见的配置范围为 20 至 200 之间。

可以通过下面命令,临时调大连接池的大小

 set global max_connections=300

最大连接数是连接池能申请的最大连接数。如果数据连接请求超过此数,后面的数据连接请求将被加入到等待队列中,这会影响之后的数据库操作。如果最小连接数与最大连接数相差太大,那么,最先的连接请求将会获利,之后超过最小连接数量的连接请求等价于建立一个新的数据库连接。

不过,这些大于最小连接数的数据库连接在使用完不会马上被释放,它将被放到连接池中等待重复使用或是空闲超时后被释放。

MySQL服务器的连接数并不是要达到最大的100%为好,我一般看到超过70%就要引起注意了,还是要具体问题具体分析。

2.短连接风暴

正常的短连接模式就是连接到数据库后,执行很少的 SQL 语句就断开,下次需要的时候再重连。如果使用的是短连接,在业务高峰期的时候,就可能出现连接数突然暴涨的情况。

MySQL 建立连接的过程,成本是很高的。除了正常的网络连接三次握手外,还需要做登录权限判断和获得这个连接的数据读写权限。

在数据库压力比较小的时候,这些额外的成本并不明显。

但是,短连接模型存在一个风险,就是一旦数据库处理得慢一些,连接数就会暴涨。max_connections 参数,用来控制一个 MySQL 实例同时存在的连接数的上限,超过这个值,系统就会拒绝接下来的连接请求,并报错提示“Too many connections”。对于被拒绝连接的请求来说,从业务角度看就是数据库不可用。

在机器负载比较高的时候,处理现有请求的时间变长,每个连接保持的时间也更长。这时,再有新建连接的话,就可能会超过 max_connections 的限制。

碰到这种情况时,一个比较自然的想法,就是调高 max_connections 的值。但这样做是有风险的。因为设计 max_connections 这个参数的目的是想保护 MySQL,如果我们把它改得太大,让更多的连接都可以进来,那么系统的负载可能会进一步加大,大量的资源耗费在权限验证等逻辑上,结果可能是适得其反,已经连接的线程拿不到 CPU 资源去执行业务的 SQL 请求。

那么这种情况下,你还有没有别的建议呢?我这里还有两种方法,但要注意,这些方法都是有损的。

第一种方法:先处理掉那些占着连接但是不工作的线程。

max_connections 的计算,不是看谁在 running,是只要连着就占用一个计数位置。对于那些不需要保持的连接,我们可以通过 kill connection 主动踢掉。这个行为跟事先设置 wait_timeout 的效果是一样的。设置 wait_timeout 参数表示的是,一个线程空闲 wait_timeout 这么多秒之后,就会被 MySQL 直接断开连接。

但是需要注意,在 show processlist 的结果里,踢掉显示为 sleep 的线程,可能是有损的。我们来看下面这个例子。

图 1 sleep 线程的两种状态

在上面这个例子里,如果断开 session A 的连接,因为这时候 session A 还没有提交,所以 MySQL 只能按照回滚事务来处理;而断开 session B 的连接,就没什么大影响。所以,如果按照优先级来说,你应该优先断开像 session B 这样的事务外空闲的连接。

但是,怎么判断哪些是事务外空闲的呢?session C 在 T 时刻之后的 30 秒执行 show processlist,看到的结果是这样的。

图 2 sleep 线程的两种状态,show processlist 结果

图中 id=4 和 id=5 的两个会话都是 Sleep 状态。而要看事务具体状态的话,你可以查 information_schema 库的 innodb_trx 表。

图 3 从 information_schema.innodb_trx 查询事务状态

这个结果里,trx_mysql_thread_id=4,表示 id=4 的线程还处在事务中。

因此,如果是连接数过多,你可以优先断开事务外空闲太久的连接;如果这样还不够,再考虑断开事务内空闲太久的连接。

从服务端断开连接使用的是 kill connection + id 的命令, 一个客户端处于 sleep 状态时,它的连接被服务端主动断开后,这个客户端并不会马上知道。直到客户端在发起下一个请求的时候,才会收到这样的报错“ERROR 2013 (HY000): Lost connection to MySQL server during query”。

从数据库端主动断开连接可能是有损的,尤其是有的应用端收到这个错误后,不重新连接,而是直接用这个已经不能用的句柄重试查询。这会导致从应用端看上去,“MySQL 一直没恢复”。

你可能觉得这是一个冷笑话,但实际上我碰到过不下 10 次。

所以,如果你是一个支持业务的 DBA,不要假设所有的应用代码都会被正确地处理。即使只是一个断开连接的操作,也要确保通知到业务开发团队。

第二种方法:减少连接过程的消耗。

有的业务代码会在短时间内先大量申请数据库连接做备用,如果现在数据库确认是被连接行为打挂了,那么一种可能的做法,是让数据库跳过权限验证阶段。

跳过权限验证的方法是:重启数据库,并使用–skip-grant-tables 参数启动。这样,整个 MySQL 会跳过所有的权限验证阶段,包括连接过程和语句执行过程在内。

但是,这种方法特别符合我们标题里说的“饮鸩止渴”,风险极高,是我特别不建议使用的方案。尤其你的库外网可访问的话,就更不能这么做了。

在 MySQL 8.0 版本里,如果你启用–skip-grant-tables 参数,MySQL 会默认把 --skip-networking 参数打开,表示这时候数据库只能被本地的客户端连接。可见,MySQL 官方对 skip-grant-tables 这个参数的安全问题也很重视。

除了短连接数暴增可能会带来性能问题外,实际上,我们在线上碰到更多的是查询或者更新语句导致的性能问题。其中,查询问题比较典型的有两类,一类是由新出现的慢查询导致的,一类是由 QPS(每秒查询数)突增导致的。

4.innodb_thread_concurrency

innodb_thread_concurrency表示innodb允许的最大并发查询数。

通过下面命令可以查看对应配置值

SHOW VARIABLES LIKE 'innodb_thread_concurrency';


在 InnoDB 中,innodb_thread_concurrency 这个参数的默认值是 0,表示不限制并发线程数量。但是,不限制并发线程数肯定是不行的。因为,一个机器的 CPU 核数有限,线程全冲进来,上下文切换的成本就会太高。

所以,通常情况下,我们建议把 innodb_thread_concurrency 设置为 64~128 之间的值。这时,你一定会有疑问,并发线程上限数设置为 128 够干啥,线上的并发连接数动不动就上千了。

产生这个疑问的原因,是搞混了并发连接和并发查询。

并发连接和并发查询,并不是同一个概念。你在 show processlist 的结果里,看到的几千个连接,指的就是并发连接。而“当前正在执行”的语句,才是我们所说的并发查询。

并发连接数达到几千个影响并不大,就是多占一些内存而已。我们应该关注的是并发查询,因为并发查询太高才是 CPU 杀手。这也是为什么我们需要设置 innodb_thread_concurrency 参数的原因。

然后,如果把 innodb_thread_concurrency 设置为 128 的话,那么出现同一行热点更新的问题时,是不是很快就把 128 消耗完了,这样整个系统是不是就挂了呢?

实际上,在线程进入锁等待以后,并发线程的计数会减一,也就是说等行锁(也包括间隙锁)的线程是不算在 128 里面的。

MySQL 这样设计是非常有意义的。因为,进入锁等待的线程已经不吃 CPU 了;更重要的是,必须这么设计,才能避免整个系统锁死。

为什么呢?假设处于锁等待的线程也占并发线程的计数,你可以设想一下这个场景:

  1. 线程 1 执行 begin; update t set c=c+1 where id=1, 启动了事务 trx1, 然后保持这个状态。这时候,线程处于空闲状态,不算在并发线程里面。

  2. 线程 2 到线程 129 都执行 update t set c=c+1 where id=1; 由于等行锁,进入等待状态。这样就有 128 个线程处于等待状态;

  3. 如果处于锁等待状态的线程计数不减一,InnoDB 就会认为线程数用满了,会阻止其他语句进入引擎执行,这样线程 1 不能提交事务。而另外的 128 个线程又处于锁等待状态,整个系统就堵住了。

下图 2 显示的就是这个状态。

图 2 系统锁死状态(假设等行锁的语句占用并发计数)

这时候 InnoDB 不能响应任何请求,整个系统被锁死。而且,由于所有线程都处于等待状态,此时占用的 CPU 却是 0,而这明显不合理。所以,我们说 InnoDB 在设计时,遇到进程进入锁等待的情况时,将并发线程的计数减 1 的设计,是合理而且是必要的。

6.表大小阀值

你在编写一条查询语句的时候,可以依据你要查询数据表的数据总量,估算一下这条查询大致需要遍历多少行数据。

如果遍历行数在百万以内的,只要不是每秒钟都要执行几十上百次的频繁查询,可以认为是安全的。

遍历数据行数在几百万的,查询时间最少也要几秒钟,你就要仔细考虑有没有优化的办法。

遍历行数达到千万量级和以上的,我只能告诉你,这种查询就不应该出现在你的系统中。当然我们这里说的都是在线交易系统,离线分析类系统另说。遍历行数在千万左右,是 MySQL 查询的一个坎儿。MySQL 中单个表数据量,也要尽量控制在一千万条以下,最多不要超过二三千万这个量级。原因也很好理解,对一个千万级别的表执行查询,加上几个 WHERE 条件过滤一下,符合条件的数据最多可能在几十万或者百万量级,这还可以接受。但如果再和其他的表做一个联合查询,遍历的数据量很可能就超过千万级别了。

所以,每个表的数据量最好小于千万级别。

二、数据库可用性监控

1.客户端监控

1.select 1 判断

我们今天要讨论的问题:怎么判断一个主库出问题了?

你一定会说,这很简单啊,连上 MySQL,执行个 select 1 就好了。但是 select 1 成功返回了,就表示主库没问题吗?

实际上,select 1 成功返回,只能代表能够和数据库建立连接成功,只能说明这个库的进程还在,并不能说明主库没问题。现在,我们来看一下这个场景。

set global innodb_thread_concurrency=3;
 
CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;
 
 insert into t values(1,1)

图 1 查询 blocked

我们设置 innodb_thread_concurrency 参数的目的是,控制 InnoDB 的并发线程上限。也就是说,一旦并发线程数达到这个值,InnoDB 在接收到新请求的时候,就会进入等待状态,直到有线程退出。

这里,我把 innodb_thread_concurrency 设置成 3,表示 InnoDB 只允许 3 个线程并行执行。而在我们的例子中,前三个 session 中的 sleep(100),使得这三个语句都处于“执行”状态,以此来模拟大查询。

你看到了, session D 里面,select 1 是能执行成功的,但是查询表 t 的语句会被堵住。也就是说,如果这时候我们用 select 1 来检测实例是否正常的话,是检测不出问题的。

比如我们上面例子中前三个事务中的 select sleep(100) from t,还是要算进并发线程的计数的。

在这个例子中,同时在执行的语句超过了设置的 innodb_thread_concurrency 的值,这时候系统其实已经不行了,但是通过 select 1 来检测系统,会认为系统还是正常的。

你看完后可能会觉得,select 1 这样的方法是不是已经被淘汰了呢,但实际上使用非常广泛的 MHA(Master High Availability),默认使用的就是这个方法。

MHA 中的另一个可选方法是只做连接,就是 “如果连接成功就认为主库没问题”。不过据我所知,选择这个方法的很少。

因此,我们使用 select 1 的判断逻辑要修改一下。

2.查表判断

为了能够检测 InnoDB 并发线程数过多导致的系统不可用情况,我们需要找一个访问 InnoDB 的场景。一般的做法是,在系统库(mysql 库)里创建一个表,比如命名为 health_check,里面只放一行数据,然后定期执行:

mysql> select * from mysql.health_check;

使用这个方法,我们可以检测出由于并发线程过多导致的数据库不可用的情况。

但是,我们马上还会碰到下一个问题,即:空间满了以后,这种方法又会变得不好使。

我们知道,更新事务要写 binlog,而一旦 binlog 所在磁盘的空间占用率达到 100%,那么所有的更新语句和事务提交的 commit 语句就都会被堵住。但是,系统这时候还是可以正常读数据的。

因此,我们还是把这条监控语句再改进一下。接下来,我们就看看把查询语句改成更新语句

3.更新判断(推荐使用)

既然要更新,就要放个有意义的字段,常见做法是放一个 timestamp 字段,用来表示最后一次执行检测的时间。这条更新语句类似于:

mysql> update mysql.health_check set t_modified=now();

节点可用性的检测都应该包含主库和备库。如果用更新来检测主库的话,那么备库也要进行更新检测。

但,备库的检测也是要写 binlog 的。由于我们一般会把数据库 A 和 B 的主备关系设计为双 M 结构,所以在备库 B 上执行的检测命令,也要发回给主库 A。

但是,如果主库 A 和备库 B 都用相同的更新命令,就可能出现行冲突,也就是可能会导致主备同步停止。所以,现在看来 mysql.health_check 这个表就不能只有一行数据了。

为了让主备之间的更新不产生冲突,我们可以在 mysql.health_check 表上存入多行数据,并用 A、B 的 server_id 做主键。

由于 MySQL 规定了主库和备库的 server_id 必须不同(否则创建主备关系的时候就会报错),这样就可以保证主、备库各自的检测命令不会发生冲突。

更新判断是一个相对比较常用的方案了,不过依然存在一些问题。其中,“判定慢”一直是让 DBA 头疼的问题。

这里涉及到的是服务器 IO 资源分配的问题。

首先,所有的检测逻辑都需要一个超时时间 N。执行一条 update 语句,超过 N 秒后还不返回,就认为系统不可用。

你可以设想一个日志盘的 IO 利用率已经是 100% 的场景。这时候,整个系统响应非常慢,已经需要做主备切换了。

但是你要知道,IO 利用率 100% 表示系统的 IO 是在工作的,每个请求都有机会获得 IO 资源,执行自己的任务。而我们的检测使用的 update 命令,需要的资源很少,所以可能在拿到 IO 资源的时候就可以提交成功,并且在超时时间 N 秒未到达之前就返回给了检测系统。

检测系统一看,update 命令没有超时,于是就得到了“系统正常”的结论。

也就是说,这时候在业务系统上正常的 SQL 语句已经执行得很慢了,但是 DBA 上去一看,HA 系统还在正常工作,并且认为主库现在处于可用状态。

之所以会出现这个现象,根本原因是我们上面说的所有方法,都是基于外部检测的。外部检测天然有一个问题,就是随机性。

因为,外部检测都需要定时轮询,所以系统可能已经出问题了,但是却需要等到下一个检测发起执行语句的时候,我们才有可能发现问题。而且,如果你的运气不够好的话,可能第一次轮询还不能发现,这就会导致切换慢的问题。

2.服务端监控

接下来我要再和你介绍一种在 MySQL 内部发现数据库问题的方法。

针对磁盘利用率这个问题,如果 MySQL 可以告诉我们,内部每一次 IO 请求的时间,那我们判断数据库是否出问题的方法就可靠得多了。

其实,MySQL 5.6 版本以后提供的 performance_schema 库,就在 file_summary_by_event_name 表里统计了每次 IO 请求的时间。

至于统计的原理请参考《极客时间》第29章

但我们每一次操作数据库,performance_schema 都需要额外地统计这些信息,所以我们打开这个统计功能是有性能损耗的。

我的测试结果是,如果打开所有的 performance_schema 项,性能大概会下降 10% 左右。所以,我建议你只打开自己需要的项进行统计,至于具体要怎么操作可以联系你的DBA。

其实大部分场景都是通过客户端更新判断和服务端内部统计结合的方式来判断数据库的可用性

三、删库跑路数据备份治理

我们介绍了 MySQL 的高可用架构。当然,传统的高可用架构是不能预防误删数据的,因为主库的一个 drop table 命令,会通过 binlog 传给所有从库和级联从库,进而导致整个集群的实例都会执行这个命令。

1.误删后解决方案

为了找到解决误删数据的更高效的方法,我们需要先对和 MySQL 相关的误删数据,做下分类:

  1. 使用 delete 语句误删数据行;

  2. 使用 drop table 或者 truncate table 语句误删数据表;

  3. 使用 drop database 语句误删数据库;

  4. 使用 rm 命令误删整个 MySQL 实例。

1.误删行

如果是使用 delete 语句误删了数据行,可以用 Flashback 工具通过闪回把数据恢复回来。

Flashback 恢复数据的原理,是修改 binlog 的内容,拿回原库重放。而能够使用这个方案的前提是,需要确保 binlog_format=row 和 binlog_row_image=FULL。

具体恢复数据时,对单个事务做如下处理:

  1. 对于 insert 语句,对应的 binlog event 类型是 Write_rows event,把它改成 Delete_rows event 即可;

  2. 同理,对于 delete 语句,也是将 Delete_rows event 改为 Write_rows event;

  3. 而如果是 Update_rows 的话,binlog 里面记录了数据行修改前和修改后的值,对调这两行的位置即可。

如果误操作不是一个,而是多个,会怎么样呢?比如下面三个事务:

(A)delete ...

(B)insert ...

(C)update ...

现在要把数据库恢复回这三个事务操作之前的状态,用 Flashback 工具解析 binlog 后,写回主库的命令是:

(reverse C)update ...

(reverse B)delete ...

(reverse A)insert ...

也就是说,如果误删数据涉及到了多个事务的话,需要将事务的顺序调过来再执行。

需要说明的是,我不建议你直接在主库上执行这些操作。

恢复数据比较安全的做法,是恢复出一个备份,或者找一个从库作为临时库,在这个临时库上执行这些操作,然后再将确认过的临时库的数据,恢复回主库。

为什么要这么做呢?

这是因为,一个在执行线上逻辑的主库,数据状态的变更往往是有关联的。可能由于发现数据问题的时间晚了一点儿,就导致已经在之前误操作的基础上,业务代码逻辑又继续修改了其他数据。所以,如果这时候单独恢复这几行数据,而又未经确认的话,就可能会出现对数据的二次破坏。

当然,我们不止要说误删数据的事后处理办法,更重要是要做到事前预防。我有以下两个建议:

  1. 把 sql_safe_updates 参数设置为 on。这样一来,如果我们忘记在 delete 或者 update 语句中写 where 条件,或者 where 条件里面没有包含索引字段的话,这条语句的执行就会报错。

  2. 代码上线前,必须经过 SQL 审计。

你可能会说,设置了 sql_safe_updates=on,如果我真的要把一个小表的数据全部删掉,应该怎么办呢?

如果你确定这个删除操作没问题的话,可以在 delete 语句中加上 where 条件,比如 where id>=0。

但是,delete 全表是很慢的,需要生成回滚日志、写 redo、写 binlog。所以,从性能角度考虑,你应该优先考虑使用 truncate table 或者 drop table 命令。

使用 delete 命令删除的数据,你还可以用 Flashback 来恢复。

2.误删库表

而使用 truncate /drop table 和 drop database 命令删除的数据,就没办法通过 Flashback 来恢复了。为什么呢?

这是因为,即使我们配置了 binlog_format=row,执行这三个命令时,记录的 binlog 还是 statement 格式。binlog 里面就只有一个 truncate/drop 语句,这些信息是恢复不出数据的。

那么,如果我们真的是使用这几条命令误删数据了,又该怎么办呢?

这种情况下,要想恢复数据,就需要使用全量备份,加增量日志的方式了。这个方案要求线上有定期的全量备份,并且实时备份 binlog。

在这两个条件都具备的情况下,假如有人中午 12 点误删了一个库,恢复数据的流程如下:

  1. 取最近一次全量备份,假设这个库是一天一备,上次备份是当天 0 点;

  2. 用备份恢复出一个临时库;

  3. 从日志备份里面,取出凌晨 0 点之后的日志;

  4. 把这些日志,除了误删除数据的语句外,全部应用到临时库。

这个流程的示意图如下所示:

图 1 数据恢复流程 -mysqlbinlog 方法

关于这个过程,我需要和你说明如下几点:

  1. 为了加速数据恢复,如果这个临时库上有多个数据库,你可以在使用 mysqlbinlog 命令时,加上一个–database 参数,用来指定误删表所在的库。这样,就避免了在恢复数据时还要应用其他库日志的情况。

  2. 在应用日志的时候,需要跳过 12 点误操作的那个语句的 binlog:

    • 如果原实例没有使用 GTID 模式,只能在应用到包含 12 点的 binlog 文件的时候,先用–stop-position 参数执行到误操作之前的日志,然后再用–start-position 从误操作之后的日志继续执行;
    • 如果实例使用了 GTID 模式,就方便多了。假设误操作命令的 GTID 是 gtid1,那么只需要执行 set gtid_next=gtid1;begin;commit; 先把这个 GTID 加到临时实例的 GTID 集合,之后按顺序执行 binlog 的时候,就会自动跳过误操作的语句。

不过,即使这样,使用 mysqlbinlog 方法恢复数据还是不够快,主要原因有两个:

  1. 如果是误删表,最好就是只恢复出这张表,也就是只重放这张表的操作,但是 mysqlbinlog 工具并不能指定只解析一个表的日志;

  2. 用 mysqlbinlog 解析出日志应用,应用日志的过程就只能是单线程。我们在第 26 篇文章中介绍的那些并行复制的方法,在这里都用不上。

一种加速的方法是,在用备份恢复出临时实例之后,将这个临时实例设置成线上备库的从库,这样:

  1. 在 start slave 之前,先通过执行
    change replication filter replicate_do_table = (tbl_name) 命令,就可以让临时库只同步误操作的表;

  2. 这样做也可以用上并行复制技术,来加速整个数据恢复过程。

这个过程的示意图如下所示。

图 2 数据恢复流程 -master-slave 方法

可以看到,图中 binlog 备份系统到线上备库有一条虚线,是指如果由于时间太久,备库上已经删除了临时实例需要的 binlog 的话,我们可以从 binlog 备份系统中找到需要的 binlog,再放回备库中。

假设,我们发现当前临时实例需要的 binlog 是从 master.000005 开始的,但是在备库上执行 show binlogs 显示的最小的 binlog 文件是 master.000007,意味着少了两个 binlog 文件。这时,我们就需要去 binlog 备份系统中找到这两个文件。

把之前删掉的 binlog 放回备库的操作步骤,是这样的:

  1. 从备份系统下载 master.000005 和 master.000006 这两个文件,放到备库的日志目录下;

  2. 打开日志目录下的 master.index 文件,在文件开头加入两行,内容分别是 “./master.000005”和“./master.000006”;

  3. 重启备库,目的是要让备库重新识别这两个日志文件;

  4. 现在这个备库上就有了临时库需要的所有 binlog 了,建立主备关系,就可以正常同步了。

不论是把 mysqlbinlog 工具解析出的 binlog 文件应用到临时库,还是把临时库接到备库上,这两个方案的共同点是:误删库或者表后,恢复数据的思路主要就是通过备份,再加上应用 binlog 的方式。

也就是说,这两个方案都要求备份系统定期备份全量日志,而且需要确保 binlog 在被从本地删除之前已经做了备份。

但是,一个系统不可能备份无限的日志,你还需要根据成本和磁盘空间资源,设定一个日志保留的天数。如果你的 DBA 团队告诉你,可以保证把某个实例恢复到半个月内的任意时间点,这就表示备份系统保留的日志时间就至少是半个月。

另外,我建议你不论使用上述哪种方式,都要把这个数据恢复功能做成自动化工具,并且经常拿出来演练。为什么这么说呢?

这里的原因,主要包括两个方面:

  1. 虽然“发生这种事,大家都不想的”,但是万一出现了误删事件,能够快速恢复数据,将损失降到最小,也应该不用跑路了。

  2. 而如果临时再手忙脚乱地手动操作,最后又误操作了,对业务造成了二次伤害,那就说不过去了。

3.rm 删除数据

其实,对于一个有高可用机制的 MySQL 集群来说,最不怕的就是 rm 删除数据了。只要不是恶意地把整个集群删除,而只是删掉了其中某一个节点的数据的话,HA 系统就会开始工作,选出一个新的主库,从而保证整个集群的正常工作。

这时,你要做的就是在这个节点上把数据恢复回来,再接入整个集群。

当然了,现在不止是 DBA 有自动化系统,SA(系统管理员)也有自动化系统,所以也许一个批量下线机器的操作,会让你整个 MySQL 集群的所有节点都全军覆没。

应对这种情况,我的建议只能是说尽量把你的备份跨机房,或者最好是跨城市保存。

2.误删前预防方案

1.延迟复制备库

虽然我们可以通过利用并行复制来加速恢复数据的过程,但是这个方案仍然存在“恢复时间不可控”的问题。

如果一个库的备份特别大,或者误操作的时间距离上一个全量备份的时间较长,比如一周一备的实例,在备份之后的第 6 天发生误操作,那就需要恢复 6 天的日志,这个恢复时间可能是要按天来计算的。

那么,我们有什么方法可以缩短恢复数据需要的时间呢?

如果有非常核心的业务,不允许太长的恢复时间,我们可以考虑搭建延迟复制的备库。这个功能是 MySQL 5.6 版本引入的。

一般的主备复制结构存在的问题是,如果主库上有个表被误删了,这个命令很快也会被发给所有从库,进而导致所有从库的数据表也都一起被误删了。

延迟复制的备库是一种特殊的备库,通过 CHANGE MASTER TO MASTER_DELAY = N 命令,可以指定这个备库持续保持跟主库有 N 秒的延迟。

比如你把 N 设置为 3600,这就代表了如果主库上有数据被误删了,并且在 1 小时内发现了这个误操作命令,这个命令就还没有在这个延迟复制的备库执行。这时候到这个备库上执行 stop slave,再通过之前介绍的方法,跳过误操作命令,就可以恢复出需要的数据。

这样的话,你就随时可以得到一个,只需要最多再追 1 小时,就可以恢复出数据的临时实例,也就缩短了整个数据恢复需要的时间。

2.定期备份

3.预防误删库表

虽然常在河边走,很难不湿鞋,但终究还是可以找到一些方法来避免的。所以这里,我也会给你一些减少误删操作风险的建议。

第一条建议是,账号分离。这样做的目的是,避免写错命令。比如:

  • 我们只给业务开发同学 DML 权限,而不给 truncate/drop 权限。而如果业务开发人员有 DDL 需求的话,也可以通过开发管理系统得到支持。
  • 即使是 DBA 团队成员,日常也都规定只使用只读账号,必要的时候才使用有更新权限的账号。

你可以用 show grants 命令查看账户的权限,如果权限过大,可以建议 DBA 同学给你分配权限低一些的账号;

第二条建议是,制定操作规范。这样做的目的,是避免写错要删除的表名。比如:

  • 在删除数据表之前,必须先对表做改名操作。然后,观察一段时间,确保对业务无影响以后再删除这张表。
  • 改表名的时候,要求给表名加固定的后缀(比如加 _to_be_deleted),然后删除表的动作必须通过管理系统执行。并且,管理系删除表的时候,只能删除固定后缀的表。
  • 四个脚本,这四个脚本分别是:备份脚本、执行脚本、验证脚本和回滚脚本。如果能够坚持做到,即使出现问题,也是可以很快恢复的,一定能降低出现故障的概率。

四、慢sql治理

1、什么是慢 SQL

什么是慢SQL?顾名思义,运行时间较长的 SQL 语句即为慢 SQL!

那问题来了,多久才算慢呢?

这个慢其实是一个相对值,不同的业务场景下,标准要求是不一样的。

我们都知道,我们每执行一次 SQL,数据库除了会返回执行结果以外,还会返回 SQL 执行耗时,以 MySQL 数据库为例,当我们开启了慢 SQL 监控开关后,默认配置下,当 SQL 的执行时长大于 10 秒,会被记录到慢 SQL 的日志文件中

2、慢 SQL 危害

这里要从慢 SQL 的危害谈起,以 MySQL 数据库为例,总结起来有以下几点:

1.当出现慢查询,DDL 操作都会被阻塞,也就是说创建表、修改表、删除表、执行数据备份等操作都需要等待,这对实时备份重要数据的系统来说是不可容忍的。

2.慢查可能会占用 mysql 的大量内存,严重的时候会导致服务器直接挂掉,整个系统直接瘫痪

3.慢 SQL 的执行时间过长,可能会导致应用的进程因超时被 kill,无法返回结果给到客户端

4.造成数据库幻读、不可重复读的概率更大,假设该慢 SQL 是一个更新操作但因执行时间过长未提交,而另一条 SQL 也在更新数据并且已提交,用户再次查询的时候,看到的数据可能与实际结果不符

5.严重影响用户体验,SQL 的执行时间越长,页面加载数据耗时也就越长

6.直接造成大量的数据库连接超时,服务无法正常响应。
如常见的连接池被耗尽的情况:
在这里插入图片描述
遇到这种问题快速的解决方案有

1.kill掉慢sql的进程
2.重启应用服务器

根本上解决方案则是定位慢sql并优化解决。

3.如何定位慢 SQL

1.开启慢sql监控

以 MySQL 为例,我们可以通过如下方式,查询是否开启慢 SQL 的监控。

show variables like 'slow_query_log%';

在这里插入图片描述
通过如下命令,开启慢 SQL 监控,执行成功之后,客户端需要重新连接才能生效。

-- 开启慢 SQL 监控
set global slow_query_log = 1;

在这里插入图片描述
以上的操作,当服务器不重启会一直有效,但是当服务器一单重启之后,配置就会失效,如果想永久生效,可以通过修改全局配置文件my.cnf使之永久生效。

以 CentOS 为例,打开my.cnf配置文件,添加如下配置变量。

[mysqld]
slow_query_log = ON
slow_query_log_file = /var/lib/mysql/ecs-203056-slow.log
long_query_time = 1

最后,重启 mysql 服务器

2.配置慢 SQL 阀值

默认的慢 SQL 阀值是10秒,可以通过如下语句查询慢 SQL 的阀值。

-- 查询慢 SQL 的阀值
show variables like "long_query_time";
-- 修改慢 SQL 的阀值
set global long_query_time = 3;

3.查看慢sql

假如慢sql的阀值是3s,慢sql日志文件是在/mnt/cp/mysql/log/下面。

则执行下面命令即可抓起到对应的慢sql
在这里插入图片描述
日志内容详解:

Time:表示客户端查询时间
root[root]:表示客户端查询用户和IP
Query_time:表示查询耗时
Lock_time:表示等待 table lock 的时间,注意InnoDB的行锁等待是不会反应在这里的
Rows_sent:表示返回了多少行记录(结果集)。
Rows_examined:表示检查了多少条记录。

4.分析慢 SQL的产生原因

其中影响成本开销值的计算,主要是I/O成本和CPU成本这两个指标。

从I/O成本视角看:

当表的数据量越大,需要的 I/O 次数也就越多
从磁盘读取数据比从缓存读取数据,I/O 消耗的时间更多
全表扫描比通过索引快速查找,I/O 消耗的时间和次数更多

从CPU成本视角看:

当 SQL 中有排序、子查询等复杂的操作时,CPU 需要先把数据存到临时表中,再对数据进行加工,需要的 CPU 资源更多
全表扫描相比于通过索引快速查找,需要的 CPU 资源也更多

因此我们不难发现,在没有开启缓存的情况下,当表的数据量越大,如果 SQL 又没有走索引,很容易发生查询慢的问题。

从引发性能问题的角度看,大体有以下三种可能:

  1. 索引没有设计好;

  2. SQL 语句没写好;

  3. MySQL 选错了索引。

接下来,我们就具体分析一下这三种可能,以及对应的解决方案。

导致慢查询的第一种可能是,索引没有设计好。

这种场景一般就是通过紧急创建索引来解决。MySQL 5.6 版本以后,创建索引都支持 Online DDL 了,对于那种高峰期数据库已经被这个语句打挂了的情况,最高效的做法就是直接执行 alter table 语句。

导致慢查询的第二种可能是,语句没写好。

比如,我们犯了导致语句没有使用上索引。

这时,我们可以通过改写 SQL 语句来处理。MySQL 5.7 提供了 query_rewrite 功能,可以把输入的一种语句改写成另外一种模式。

比如,语句被错误地写成了 select * from t where id + 1 = 10000,你可以通过下面的方式,增加一个语句改写规则。

mysql> insert into query_rewrite.rewrite_rules(pattern, replacement, pattern_database) values ("select * from t where id + 1 = ?", "select * from t where id = ? - 1", "db1");

call query_rewrite.flush_rewrite_rules();

这里,call query_rewrite.flush_rewrite_rules() 这个存储过程,是让插入的新规则生效,也就是我们说的“查询重写”。你可以用图 4 中的方法来确认改写规则是否生效。

图 4 查询重写效果

导致慢查询的第三种可能,MySQL 选错了索引。

这时候,应急方案就是给这个语句加上 force index。

同样地,使用查询重写功能,给原来的语句加上 force index,也可以解决这个问题。

上面我和你讨论的由慢查询导致性能问题的三种可能情况,实际上出现最多的是前两种,即:索引没设计好和语句没写好。而这两种情况,恰恰是完全可以避免的。

比如,通过下面这个过程,我们就可以预先发现问题。

  1. 上线前,在测试环境,把慢查询日志(slow log)打开,并且把 long_query_time 设置成 0,确保每个语句都会被记录入慢查询日志;

  2. 在测试表里插入模拟线上的数据,做一遍回归测试;

  3. 观察慢查询日志里每类语句的输出,特别留意 Rows_examined 字段是否与预期一致。

不要吝啬这段花在上线前的“额外”时间,因为这会帮你省下很多故障复盘的时间。

如果新增的 SQL 语句不多,手动跑一下就可以。而如果是新项目的话,或者是修改了原有项目的 表结构设计,全量回归测试都是必要的。这时候,你需要工具帮你检查所有的 SQL 语句的返回结果。比如,你可以使用开源工具 pt-query-digest(pt-query-digest — Percona Toolkit Documentation)。

5.慢查询优化思路

在业务系统中,除了使用主键进行的查询,其他的都会在测试库上测试其耗时,慢查询的统计主要由运维在做,会定期将业务中的慢查询反馈给我们。

慢查询的优化首先要搞明白慢的原因是什么?是查询条件没有命中索引?是load了不需要的数据列?还是数据量太大?

所以优化也是针对这三个方向来的。

1.首先分析语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写。

2.分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引。

3.如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵向的分表。

参考资料

慢SQL,压垮团队的最后一根稻草!https://blog.csdn.net/dxflqm_pz/article/details/126360562

五、数据库运维

1. kill 命令

在 MySQL 中有两个 kill 命令:一个是 kill query + 线程 id,表示终止这个线程中正在执行的语句;一个是 kill connection + 线程 id,这里 connection 可缺省,表示断开这个线程的连接,当然如果这个线程有语句正在执行,也是要先停止正在执行的语句的。

不知道你在使用 MySQL 的时候,有没有遇到过这样的现象:使用了 kill 命令,却没能断开这个连接。再执行 show processlist 命令,看到这条语句的 Command 列显示的是 Killed。

你一定会奇怪,显示为 Killed 是什么意思,不是应该直接在 show processlist 的结果里看不到这个线程了吗?

今天,我们就来讨论一下这个问题。

其实大多数情况下,kill query/connection 命令是有效的。比如,执行一个查询的过程中,发现执行时间太久,要放弃继续查询,这时我们就可以用 kill query 命令,终止这条查询语句。

还有一种情况是,语句处于锁等待的时候,直接使用 kill 命令也是有效的。我们一起来看下这个例子:

图 1 kill query 成功的例子

可以看到,session C 执行 kill query 以后,session B 几乎同时就提示了语句被中断。这,就是我们预期的结果。

1.kill实现原理

但是,这里你要停下来想一下:session B 是直接终止掉线程,什么都不管就直接退出吗?显然,这是不行的。

当对一个表做增删改查操作时,会在表上加 MDL 读锁。所以,session B 虽然处于 blocked 状态,但还是拿着一个 MDL 读锁的。如果线程被 kill 的时候,就直接终止,那之后这个 MDL 读锁就没机会被释放了。

这样看来,kill 并不是马上停止的意思,而是告诉执行线程说,这条语句已经不需要继续执行了,可以开始“执行停止的逻辑了”。

其实,这跟 Linux 的 kill 命令类似,kill -N pid 并不是让进程直接停止,而是给进程发一个信号,然后进程处理这个信号,进入终止逻辑。只是对于 MySQL 的 kill 命令来说,不需要传信号量参数,就只有“停止”这个命令。

实现上,当用户执行 kill query thread_id_B 时,MySQL 里处理 kill 命令的线程做了两件事:

  1. 把 session B 的运行状态改成 THD::KILL_QUERY(将变量 killed 赋值为 THD::KILL_QUERY);

  2. 给 session B 的执行线程发一个信号。

为什么要发信号呢?

因为像图 1 的我们例子里面,session B 处于锁等待状态,如果只是把 session B 的线程状态设置 THD::KILL_QUERY,线程 B 并不知道这个状态变化,还是会继续等待。发一个信号的目的,就是让 session B 退出等待,来处理这个 THD::KILL_QUERY 状态。

上面的分析中,隐含了这么三层意思:

  1. 一个语句执行过程中有多处“埋点”,在这些“埋点”的地方判断线程状态,如果发现线程状态是 THD::KILL_QUERY,才开始进入语句终止逻辑;

  2. 如果处于等待状态,必须是一个可以被唤醒的等待,否则根本不会执行到“埋点”处;

  3. 语句从开始进入终止逻辑,到终止逻辑完全完成,是有一个过程的。

到这里你就知道了,原来不是“说停就停的”。

接下来,我们再看一个 kill 不掉的例子,也就是我们在前面提到的 innodb_thread_concurrency 不够用的例子。

首先,执行 set global innodb_thread_concurrency=2,将 InnoDB 的并发线程上限数设置为 2;然后,执行下面的序列:

图 2 kill query 无效的例子

可以看到:

  1. sesssion C 执行的时候被堵住了;

  2. 但是 session D 执行的 kill query C 命令却没什么效果,

  3. 直到 session E 执行了 kill connection 命令,才断开了 session C 的连接,提示“Lost connection to MySQL server during query”,

  4. 但是这时候,如果在 session E 中执行 show processlist,你就能看到下面这个图。

图 3 kill connection 之后的效果

这时候,id=12 这个线程的 Commnad 列显示的是 Killed。也就是说,客户端虽然断开了连接,但实际上服务端上这条语句还在执行过程中。

为什么在执行 kill query 命令时,这条语句不像第一个例子的 update 语句一样退出呢?

在实现上,等行锁时,使用的是 pthread_cond_timedwait 函数,这个等待状态可以被唤醒。但是,在这个例子里,12 号线程的等待逻辑是这样的:每 10 毫秒判断一下是否可以进入 InnoDB 执行,如果不行,就调用 nanosleep 函数进入 sleep 状态。

也就是说,虽然 12 号线程的状态已经被设置成了 KILL_QUERY,但是在这个等待进入 InnoDB 的循环过程中,并没有去判断线程的状态,因此根本不会进入终止逻辑阶段。

而当 session E 执行 kill connection 命令时,是这么做的,

  1. 把 12 号线程状态设置为 KILL_CONNECTION;

  2. 关掉 12 号线程的网络连接。因为有这个操作,所以你会看到,这时候 session C 收到了断开连接的提示。

那为什么执行 show processlist 的时候,会看到 Command 列显示为 killed 呢?其实,这就是因为在执行 show processlist 的时候,有一个特别的逻辑:

如果一个线程的状态是 KILL_CONNECTION,就把 Command 列显示成 Killed。

所以其实,即使是客户端退出了,这个线程的状态仍然是在等待中。那这个线程什么时候会退出呢?

答案是,只有等到满足进入 InnoDB 的条件后,session C 的查询语句继续执行,然后才有可能判断到线程状态已经变成了 KILL_QUERY 或者 KILL_CONNECTION,再进入终止逻辑阶段。

到这里,我们来小结一下。

这个例子是 kill 无效的第一类情况,即:线程没有执行到判断线程状态的逻辑。跟这种情况相同的,还有由于 IO 压力过大,读写 IO 的函数一直无法返回,导致不能及时判断线程的状态。

另一类情况是,终止逻辑耗时较长。这时候,从 show processlist 结果上看也是 Command=Killed,需要等到终止逻辑完成,语句才算真正完成。这类情况,比较常见的场景有以下几种:

  1. 超大事务执行期间被 kill。这时候,回滚操作需要对事务执行期间生成的所有新数据版本做回收操作,耗时很长。

  2. 大查询回滚。如果查询过程中生成了比较大的临时文件,加上此时文件系统压力大,删除临时文件可能需要等待 IO 资源,导致耗时较长。

  3. DDL 命令执行到最后阶段,如果被 kill,需要删除中间过程的临时文件,也可能受 IO 资源影响耗时较久。

这些“kill 不掉”的情况,其实是因为发送 kill 命令的客户端,并没有强行停止目标线程的执行,而只是设置了个状态,并唤醒对应的线程。而被 kill 的线程,需要执行到判断状态的“埋点”,才会开始进入终止逻辑阶段。并且,终止逻辑本身也是需要耗费时间的。

所以,如果你发现一个线程处于 Killed 状态,你可以做的事情就是,通过影响系统环境,让这个 Killed 状态尽快结束。

比如,如果是第一个例子里 InnoDB 并发度的问题,你就可以临时调大 innodb_thread_concurrency 的值,或者停掉别的线程,让出位子给这个线程执行。

而如果是回滚逻辑由于受到 IO 资源限制执行得比较慢,就通过减少系统压力让它加速。

做完这些操作后,其实你已经没有办法再对它做什么了,只能等待流程自己完成。

所以思考一下下面这个问题:如果一个事务被 kill 之后,持续处于回滚状态,从恢复速度的角度看,你是应该重启等它执行结束,还是应该强行重启整个 MySQL 进程?

因为重启之后该做的回滚动作还是不能少的,所以从恢复速度的角度来说,应该让它自己结束。

当然,如果这个语句可能会占用别的锁,或者由于占用 IO 资源过多,从而影响到了别的语句执行的话,就需要先做主备切换,切到新主库提供服务。

切换之后别的线程都断开了连接,自动停止执行。接下来还是等它自己执行完成。这个操作属于我们在文章中说到的,减少系统压力,加速终止逻辑。

之前有人问过我,如果直接在客户端通过 Ctrl+C 命令,是不是就可以直接终止线程呢?

答案是,不可以。

这里有一个误解,其实在客户端的操作只能操作到客户端的线程,客户端和服务端只能通过网络交互,是不可能直接操作服务端线程的。

而由于 MySQL 是停等协议,所以这个线程执行的语句还没有返回的时候,再往这个连接里面继续发命令也是没有用的。实际上,执行 Ctrl+C 的时候,是 MySQL 客户端另外启动一个连接,然后发送一个 kill query 命令。

所以,你可别以为在客户端执行完 Ctrl+C 就万事大吉了。因为,要 kill 掉一个线程,还涉及到后端的很多操作。

参考资料

1.极客时间《MySQL实战45讲》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值