数据库引起的网站访问缓慢卡顿排查经历

问题描述

有个项目网站访问异常缓慢卡顿,有时候甚至报404,网站代码确定没有问题,那只好检查数据库,看数据库那边是否合理或有待优化。

数据库排查

排查之前基本确定是一张存储引擎为 MyISAM 的表的问题(在这里取表名为 A),所有的访问都卡在了 A 表的查询上面,那么为什么会卡在这里呢???接下来就开始分析排查

查看慢查询日志

一般正式环境中,都会开启慢查询日志,方便后期的维护。
因为表 A 有 100 万条左右的记录,所以虽然对应的列有做索引,但第一反应还是以为是数据太多查询慢导致的网站访问缓慢,所以去查看慢查询日志,结果发现基本上没有 A 表的慢查询记录。

基于慢查询日志查看结果的思考

这就有点离谱了,为什么明明卡在了 A 表,但关于 A 表的慢查询却没有呢??难道是慢查询记录有问题?还是说 A 表查询其实是不慢的?

排除疑惑,确定排查方向

有了上面的疑惑,我就在网上查看相关的慢查询资料,后来看到了下面这一段话:

只有当一个 SQL 的执行时间(不包括锁等待的时间 lock_time)大于 long_query_time 的时候,才会判定为慢查询 SQL;但是判定为慢查询 SQL 之后,输出的 Query_time 包括了(执行时间+锁等待时间),并且也会输出 Lock_time 时间。当一个 SQL 的执行时间(排除 lock_time)小于 long_query_time 的时候(即使他锁等待超过了很久),也不会记录到慢查询日志当中的。

看了上面这段话,我恍然大悟。既然卡在了 A 表,又没有关于 A 表的慢查询,那唯一的可能就是锁定时间 lock_time 太长了。那就往这个方向分析吧。

关于 MyISAM 表的锁机制与分析

MyISAM 表的锁机制

MySQL 数据表存储引擎不同,锁机制也不同。如:MyISAM 和 MEMORY 存储引擎采用的是表级锁table-level locking;BDB 存储引擎采用的是页面锁 page-level locking,但也支持表级锁;InnoDB 存储引擎既支持行级锁 row-level locking,也支持表级锁,但默认情况下是采用行级锁。
所以 MyISAM 的锁是表级锁,一旦锁定就是整张表锁定。MyISAM 有读锁和写锁,具体的锁定机制如下图:
MyISAM 表的锁机制
MyISAM 有 表共享读锁 table read lock 和表独占写锁 table write lock 两种,有以下几点特性:

  • MyISAM 表的读操作,不会阻塞其他用户对同一个表的读请求,但会阻塞对同一个表的写请求;
  • MyISAM 表的写操作,会阻塞其他用户对同一个表的读和写操作;
  • MyISAM 表的读、写操作之间、以及写操作之间是串行的

一些有助于排查的语句

SHOW STATUS LIKE ‘table%’

通过语句 SHOW STATUS LIKE 'table%'; 可以查看到如下图的查询结果:
查询结果

  • Table_locks_immediate:能够立即获得表级锁的锁请求次数
  • Table_locks_waited:不能立即获取表级锁而需要等待的锁请求次数

如果 Table_locks_waited 值较高,且存在性能问题,则说明存在着较严重的表级锁争用情况

当初执行以上语句时,确实看到 Table_locks_waited 非常大,好像有几千~~~

SHOW OPEN TABLES

这个语句的作用是 列举在表缓存中当前被打开的非TEMPORARY表,会返回一下字段:

  • Database:含有该表的数据库。
  • Table:表名称。
  • In_use:表当前被查询使用的次数。如果该数为零,则表是打开的,但是当前没有被使用。
  • Name_locked:表名称是否被锁定。名称锁定用于取消表或对表进行重命名等操作。

如果有很多表,执行该语句后一般会有好多条数据的,但是大部分表 In_use 都是 0,所以我们可以在语句上加个条件 show open tables where in_use >=1;,如果 In_use 有大于等于 1 的,结果会如下:

mysql> show open tables where in_use >=1;
+----------+-------+--------+-------------+
| Database | Table | In_use | Name_locked |
+----------+-------+--------+-------------+
| MyDB     | test  |      1 |           0 |
+----------+-------+--------+-------------+
1 row in set (0.00 sec)

我当时执行该语句后,只返回 A 表的相关记录,而且 In_use 的值竟然达到上百,Name_locked 的值具体是几忘了,但肯定不是 0!那就是说 A 表的表锁很严重啊。。。

SHOW PROCESSLIST

PROCESSLIST 命令的输出结果显示了有哪些线程在运行,不仅可以查看当前所有的连接数,还可以查看当前的连接状态帮助识别出有问题的查询语句等。
如果是 root 帐号,能看到所有用户的当前连接。如果是其他普通帐号,则只能看到自己占用的连接。SHOW PROCESSLIST只能列出当前 100 条;如果想全部列出,可以使用 SHOW FULL PROCESSLIST 命令。执行语句会有类似下面的结果:

mysql> SHOW PROCESSLIST;
+--------+-------------+-----------------------+-----------+-------------+-------+---------------------------------------------------------------+------------------+
| Id     | User        | Host                  | db        | Command     | Time  | State                                                         | Info             |
+--------+-------------+-----------------------+-----------+-------------+-------+---------------------------------------------------------------+------------------+
|      1 | system user |                       | NULL      | Connect     | 75478 | Waiting for master to send event                              | NULL             |
|      2 | system user |                       | NULL      | Connect     | 15681 | Slave has read all relay log; waiting for more updates        | NULL             |
| 154517 | dbtb        | 129.227.126.102:36766 | NULL      | Binlog Dump | 17682 | Master has sent all binlog to slave; waiting for more updates | NULL             |
| 256957 | root        | 61.104.40.211:58206   | db_name   | Query       |     0 | starting                                                      | SHOW PROCESSLIST |
+--------+-------------+-----------------------+-----------+-------------+-------+---------------------------------------------------------------+------------------+
4 rows in set

各字段的含义如下:

  • Id:用户登录 mysql 时,系统分配的 connection_id,可以使用函数 connection_id() 查看;
  • User:显示当前用户,如果不是 root,这个命令就只显示用户权限范围的 sql 语句;
  • Host:显示这个语句是从哪个 IP 的哪个端口上发的,可以用来跟踪出现问题语句的用户;
  • db:显示这个进程目前连接的是哪个数据库;
  • Command:显示当前连接的执行的命令,一般取值为休眠(sleep),查询(query),连接(connect)等;
  • Time:显示这个状态持续的时间,单位是
  • State:显示使用当前连接的 sql 语句的状态,很重要的列。state 描述的是语句执行中的某一个状态。一个 sql 语句,以查询为例,可能需要经过 copying to tmp tablesorting resultsending data 等状态才可以完成;
  • Info:显示这个 sql 语句,是判断问题语句的一个重要依据

State 的状态有很多,可以根据具体值去网上查找相关的信息

我当时执行这个语句时,竟然有一大堆 StateWaiting for table metadata lock 的记录,而且都是 A 表的。那说明 A 表的表锁真的相当严重啊。。。

确认问题所在

通过以上的查看与分析,可以知道网站访问缓慢卡顿的原因就是 A 表的表锁太严重了,至于为什么表锁严重,有以下两点原因:

  1. A 表的存储引擎为 MyISAM
  2. A 表的读写太频繁(因为业务需要,该表有时候会有增删操作)

解决方案

既然知道是 A 表的表锁太严重,而且 A 表的存储引擎为 MyISAM 。那就往这方面解决就是了,主要有两个大方向:

  1. 在存储引擎不变的情况下,尽量减少表锁的发生
  2. 因为有读也有写,有时候写操作也会很频繁,所以可以考虑修改存储引擎为 InnoDB

存储引擎不变

concurrent_insert

通常来说,在 MyISAM 里读写操作是串行的,但当对同一个表进行查询和插入操作时,为了降低锁竞争的频率,根据 concurrent_insert 的设置,MyISAM 是可以并行处理查询和插入的:

  • concurrent_insert=0 时,不允许并发插入功能。
  • concurrent_insert=1 时,允许对没有空洞的表使用并发插入,新数据位于数据文件结尾。
  • concurrent_insert=2 时,不管表有没有空洞,都允许在数据文件结尾并发插入。

所谓空洞,就是行记录被删除以后,只是被标记为“已删除”,其存储空间没有被回收,也就是说没有被物理删除。由另外一个进程,异步对这个数据进行删除。因为空间长度问题,删除以后的物理空间不能被新的记录所使用,从而形成了空洞。MyISAM 的空洞可以通过命令 OPTIMIZE TABLE table_name 来删除,但是该命令执行时会锁表,且效率较低,所以要谨慎使用。

根据上面的说明,把 concurrent_insert 设置为 2 是一个不错的选择,至于由此产生的数据空洞,可以定期使用 OPTIMIZE TABLE 语法优化。

max_write_lock_count

默认情况下,写操作的优先级要高于读操作的优先级,即便是先发送的读请求,后发送的写请求,此时也会优先处理写请求,然后再处理读请求。这就造成一个问题:一旦我发出若干个写请求,就会堵塞所有的读请求,直到写请求全都处理完,才有机会处理读请求。此时可以考虑设置 max_write_lock_count,如:

SET GLOBAL max_write_lock_count = 1;

有了这样的设置,当系统处理一个写操作后,就会暂停写操作,给读操作执行的机会。

low-priority-updates

我们还可以更干脆点,直接降低写操作的优先级,给读操作更高的优先级。

SET GLOBAL low-priority-updates=1

小结

综合来看,concurrent_insert=2 是绝对推荐的,至于 max_write_lock_count=1low-priority- updates=1,则视情况而定,如果可以降低写操作的优先级,则使用 low-priority-updates=1,否则使用 max_write_lock_count=1

我尝试把设置 concurrent_insert=2max_write_lock_count=1 后,网站访问缓慢卡顿的问题并没有得到改善,不知道是因为我操作设置不当,还是因为这样操作对当前问题没有效果

修改存储引擎

既然无法在保持 MyISAM 引擎不变的情况下解决问题,那我只好把存储引擎修改成 InnoDB 了。因为是正式环境的数据,修改的时候要谨慎点。以下是应该的步骤:

  • 找个用户操作比较少的时间点来进行修改
  • 修改前禁止掉所有的相关用户操作,如果可以的话直接关闭整个网站
  • 备份 A 表的数据
  • 删除 A 表,并新建 A 表,新建时记得修改存储引擎
  • 导入备份好的数据到 A

结语

整个排查的过程是非常迷茫与痛苦的,但是通过这个过程还是学到了很多,所以在此记录一下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章