有个项目网站访问异常缓慢卡顿,有时候甚至报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
太长了。那就往这个方向分析吧。
MySQL 数据表存储引擎不同,锁机制也不同。如:MyISAM 和 MEMORY 存储引擎采用的是表级锁table-level locking
;BDB 存储引擎采用的是页面锁 page-level locking
,但也支持表级锁;InnoDB 存储引擎既支持行级锁 row-level locking
,也支持表级锁,但默认情况下是采用行级锁。
所以 MyISAM 的锁是表级锁,一旦锁定就是整张表锁定。MyISAM 有读锁和写锁,具体的锁定机制如下图:
MyISAM 有 表共享读锁 table read lock
和表独占写锁 table write lock
两种,有以下几点特性:
通过语句 SHOW STATUS LIKE 'table%';
可以查看到如下图的查询结果:
Table_locks_immediate
:能够立即获得表级锁的锁请求次数Table_locks_waited
:不能立即获取表级锁而需要等待的锁请求次数如果 Table_locks_waited
值较高,且存在性能问题,则说明存在着较严重的表级锁争用情况
当初执行以上语句时,确实看到
Table_locks_waited
非常大,好像有几千~~~
这个语句的作用是 列举在表缓存中当前被打开的非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
表的表锁很严重啊。。。
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 table
、sorting result
、sending data
等状态才可以完成;Info
:显示这个 sql 语句,是判断问题语句的一个重要依据
State
的状态有很多,可以根据具体值去网上查找相关的信息
我当时执行这个语句时,竟然有一大堆
State
为Waiting for table metadata lock
的记录,而且都是A
表的。那说明A
表的表锁真的相当严重啊。。。
通过以上的查看与分析,可以知道网站访问缓慢卡顿的原因就是 A
表的表锁太严重了,至于为什么表锁严重,有以下两点原因:
A
表的存储引擎为 MyISAM
A
表的读写太频繁(因为业务需要,该表有时候会有增删操作)既然知道是 A
表的表锁太严重,而且 A
表的存储引擎为 MyISAM
。那就往这方面解决就是了,主要有两个大方向:
InnoDB
通常来说,在 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
,如:
SET GLOBAL max_write_lock_count = 1;
有了这样的设置,当系统处理一个写操作后,就会暂停写操作,给读操作执行的机会。
我们还可以更干脆点,直接降低写操作的优先级,给读操作更高的优先级。
SET GLOBAL low-priority-updates=1
综合来看,concurrent_insert=2
是绝对推荐的,至于 max_write_lock_count=1
和 low-priority- updates=1
,则视情况而定,如果可以降低写操作的优先级,则使用 low-priority-updates=1
,否则使用 max_write_lock_count=1
。
我尝试把设置
concurrent_insert=2
和max_write_lock_count=1
后,网站访问缓慢卡顿的问题并没有得到改善,不知道是因为我操作设置不当,还是因为这样操作对当前问题没有效果
既然无法在保持 MyISAM
引擎不变的情况下解决问题,那我只好把存储引擎修改成 InnoDB
了。因为是正式环境的数据,修改的时候要谨慎点。以下是应该的步骤:
A
表的数据A
表,并新建 A
表,新建时记得修改存储引擎A
表整个排查的过程是非常迷茫与痛苦的,但是通过这个过程还是学到了很多,所以在此记录一下。