一. 大纲
在日常的数据库运维管理中,数据库脚本是排查问题必不可少的利器,好的运维脚本可以让我们快速排查出问题所在,并在最短时间内解决问题。
但是在实际项目中,很多项目组人员面对数据库方面的突发问题,特别是SQL性能方面的问题,都无从下手,反馈给DBA的大多都是系统卡顿
或数据库存在问题
等内容,而DBA更想要知道的其实是一些更深层次的信息,比如当前数据库的SQL运行情况
,事务状态
以及是否存在阻塞
等有效的诊断信息
,以此来定位是否是数据库层面的问题。
有时候也许项目组反馈的一个截图就可以让DBA快速定位到问题原因,而省去远程排查的时间开销,大大提高工作效率,这中间缺的其实就是一个合适的脚本,一个便于项目组独立排查问题与节省沟通成本的工具。
本篇就来介绍一下三大数据库中一些非常实用的运维脚本,希望可以帮助大家更好的排查解决数据库方面的问题,也希望可以提高下项目组反馈数据库问题时的内容深度。
PS:本篇介绍的脚本主要用于SQL层面问题
的实时排查
,如页面卡顿
,CPU爆满
,行锁超时
,连接池或最大连接数爆满
等现象,一切你怀疑是因为SQL执行效率低而引发的性能问题,都可以通过对应的脚本进行排查,确认是否是SQL慢引发的问题,亦或是找到具体哪条SQL惹的祸。通常这类性能问题爆发时,通过脚本去进行实时排查
比事后通过慢日志分析
更容易精准定位问题
。
示例(1)
:某个页面每次打开都很慢,那么你可以刷新下页面,再通过脚本监控下SQL运行情况,如果发现有SQL运行很慢或者有阻塞,与开发确认后确实是这个页面上执行的SQL,那么问题锁定,就是SQL问题导致页面打开慢;如果未发现有执行很慢的SQL或是阻塞,那么基本可以证明页面打开慢和数据库没关系,需要排查其他组件是否存在问题。
示例(2)
:系统全面卡死,许多项目人员非常喜欢直接重启数据库或者应用程序
来临时解决问题
,事后再让DBA去查问题根因。虽然这种方式确实比较有效,但是对于DBA来说,就像第一杀人现场被破坏后再让你破案,比较困难。所以我想着是,大家可以在重启前执行下相关数据库脚本,尽量保留下当时的快照数据,方便后续DBA排查问题原因。很多人应该经历过事后客户领导要排查原因,但是又找不出的痛楚吧
。
二. MySQL篇
2.1. 事务状态脚本
脚本介绍:通过该脚本,可以看到当前MySQL中有哪些打开的事务,事务打开了多久,当前状态是什么,是在执行SQL,还是处于挂起等。
强烈推荐,这个脚本是我经常使用的一个MySQL脚本,排查问题必备。
脚本正常输出如下,事务都比较短(duration列值较小
),当然某些数据交换作业会跑比较大的事务,所以要根据具体SQL判断:
输出字段解释:
-
id:会话ID,可以使用
kill + id
结束该会话。 -
connection:连接信息,
user@ip:db
。 -
trx_state:事务执行状态,有RUNNING、LOCK WAIT、ROLLING BACK、COMMITTING四种状态。
-
trx_started:事务打开时间。
-
duration
:事务执行时长,单位秒。 -
trx_query
:事务当前运行SQL,如果为空,说明事务处于挂起状态
,可以结合thd_last_query
定位代码位置。 -
state
:线程会话当前状态。 -
thd_rows_examined:线程扫描行数。
-
trx_rows_locked:事务持有行锁数。
-
trx_rows_modified:事务影响行数。
-
trx_isolation_level:事务隔离级别。
-
thd_last_query
:线程包含的历史SQL(按照执行时间倒序
且默认最多存在10条SQL),如果事务处于挂起状态
,则可以根据历史执行SQL找出代码位置。 -
thread_id:线程id。
注意点:此脚本可以知道MySQL中所有活跃事务的当前状态。但是需要注意,如果事务执行的第一条SQL就被全局读锁或者元数据锁卡住,则无法从该脚本中看到,即INNODB_TRX表看不到这个未真正开始的事务
;如果第二条或后面的SQL执行被卡主,则可以看到。
问题样例(1)
:MySQL服务器cpu爆满,脚本输出如下,duration比较大,大量事务卡在同一条SQL执行上,分析该SQL发现缺少索引
,加上索引后cpu正常。
问题样例(2)
:业务系统访问超时,脚本输出如下,很多事务卡在commit上,登陆MySQL服务器检查后发现磁盘爆满
所致。
问题样例(3)
:事务挂起问题,事务挂起容易导致锁等待,挂起的原因有很多,最常见的就是接口问题。模拟问题环境脚本输出如下,可以看到事务运行了非常久,但是trx_query列为空
,说明会话此时并不在执行SQL,我们可以通过thd_last_quert
看下事务会话之前运行过哪些SQL,再根据历史SQL去代码中定位原因。
2.2. 表锁等待脚本
脚本介绍:用于查看当前MySQL中是否存在
表锁等待
,这里表锁包括:全局读锁,表级读写锁,元数据锁,这些锁通常出现在备份还原或DDL操作中。
当我们进行DDL操作
的时候,有可能因为无法拿到表的元数据锁(MDL)
出现等待,导致后续表上的操作都被卡住,这时候可以通过这个脚本找到可疑事务,临时KILL解决。
输出字段解释:
-
kill_sql
:拼接好的kill命令。 -
connection:线程连接信息。
-
command:线程执行命令。
-
time
:线程处于当前状态的时间,单位秒。 -
state
:线程状态,常见的有:-
Waiting for table flush
等待表关闭。 -
Waiting for global read lock
等待全局读锁。 -
Waiting for table metadata lock
等待表元数据锁。
-
-
info
:线程当前执行的SQL。 -
trx_started:事务打开时间。
如果MySQL中不存在表锁等待,那么输出结果为空。如果存在输出,则说明存在表锁等待,可以通过kill命令将可疑的事务终止来解决,例如:
这个例子输出可以明显的看出是292事务
的问题,但是绝大多数时候脚本输出都不会如此明显,我个人的建议是先把state状态不是Waiting for的长事务kill掉
,如果kill后还是存在表锁,则可以考虑将所有的事务都kill再观察。
2.3. 行锁等待脚本
脚本介绍:用于查看当前MySQL中是否存在
行锁等待
,个人用的不多,因为MySQL中行锁等待超过默认5s
就会超时报错,因此很少能正好查询监控到,更多的是放到后台监控脚本中循环执行,而非手动执行,详见如何有效排查解决MySQL行锁等待超时问题。
输出字段解释:
-
waiting_id:等待线程id。
-
blocking_id:阻塞线程id。
-
duration
:行锁等待时间。 -
state
:阻塞线程状态,sleep代表事务挂起。 -
waiting_query
:等待SQL。 -
blocking_current_query
:阻塞SQL,为空代表事务挂起。 -
thd_last_query
:阻塞线程历史SQL,事务挂起时可以通过该列排查。
2.4. show processlist
脚本介绍:严格来说,这并非是脚本,而是MySQL提供的管理命令,可以看到当前所有会话的连接信息,状态,SQL等,非常方便,但是输出信息不是很详细。
输出如下,可以较为清晰的展示MySQL的运行情况,但是遇到事务相关等待问题还是上述的脚本较为方便,当Info列包含的SQL较长时,show processlist命令可能无法完整展示,这时候可以通过show full processlist
完整展示SQL。
输出字段解释:
-
Id:会话ID。
-
User:用户。
-
Host:客户端IP。
-
db:数据库。
-
Command
:线程执行命令。 -
Time
:线程处于当前状态的时间,单位秒。 -
State
:线程的状态,和 Command 对应。 -
Info
:线程执行的SQL。
注意点:其中Time、Info列
比较重要,Info列
展示当前线程运行SQL
,如果为空,则说明当前线程为sleep状态,而Time列
则代表当前状态维持的时长(s)
,如果Info有具体的SQL,则代表该SQL运行时长,如果Info为空,则代表线程空闲等待时长,线程每次执行新的SQL,Time都会重新计时
。
很多同事经常会问,我的MySQL中出现很多sleep状态的线程且Time很大是有问题嘛?其实是很正常的,这些sleep线程其实是因为druid连接池的原因,所以一直并未真正关闭,处于空闲等待。
三. Oracle篇
3.1. 长事务脚本
脚本介绍:在项目中经常会遇到前台系统页面卡死、流程周转时间长、页面无响应等场景,公司项目开发通常会不假思索的认为数据库有“死锁”,然而开发人员往往忽略了数据库存在事务的概念,如下脚本可以为运维或者开发人员查询当前数据库执行的事务信息,方便排查问题。
-
sql_id:sql语句对应唯一id(将sql语句通过MD5以及hash算法计算出来的值)
-
username:sql语句执行的用户。
-
inst_id:数据库实例编号,单实例就是1,rac环境会有其他节点
-
sid:sid与serial#组成会话唯一标识。
-
serial#:同上。
-
status
:事务的执行状态,active代表正在执行,inactive代表空闲状态。 -
start_time:事务开始的时间。
-
el_second
:事务执行时间,单位是秒。 -
sql_text
:当前事务执行的sql语句文本。 -
prev_sql_text
:当前事务前一条执行的sql文本。
通常公司的OLTP系统中事务执行效率都是非常高的(几秒内完成),假如发现长事务可能就会出现问题,如下是长事务的几个例子:
问题样例(1)
:sql性能较差
,sql_id为4v4mcb0p5mxv3的会话正在执行更新操作,对应事务打开了40秒以上还没提交,明显存在性能问题,需要优化SQL。
问题样例(2)
:事务挂起
,如下2个长事务status为inactive且sql_text为空,事务已经打开了几百秒还未提交,需要检查下代码中是否因为接口问题而没有正常提交事务。
问题样例(3)
:BUG引起的长事务
,此案例是通过dblink远程插入表数据,但是由于Oracle bug,insert的事务被hang住,无法完成插入操作。
Oracle下可以根据脚本查询结果的sid,serial#,inst_id
来kill会话:
3.2. 行锁等待脚本
脚本介绍:在很多情况下经常会遇到DML语句被阻塞,导致前台挂起,流程无法流转下去,以下脚本能够查询因为行锁等待而引起的SQL阻塞。
-
blocking_user:阻塞源头的数据库用户名。
-
blocking_machine:阻塞源头的服务器机器名。
-
Blocking_SID:阻塞的源头会话信息。
-
blocking_serialnum:阻塞的源头会话信息。
-
wait_user:被阻塞的数据库用户名。
-
wait_machine:被阻塞的服务器机器名。
-
Wait_SID:被阻塞的会话。
-
wait_serialnum:被阻塞的会话。
-
Wait_Sql
:处于等待状态,被阻塞的sql语句。 -
object_name
:对象名称。 -
Kill_Command
:kill阻塞源头的命令。
以下是更新同一行数据被阻塞的场景,项目中查询之后可以使用Kill_Command那一列生成的命令,kill会话即可。
四. SQL Server篇
4.1. SQL状态脚本
脚本介绍:可以看到SQL Server中所有用户会话的活跃SQL执行情况,包括SQL执行时间,逻辑读,物理读,CPU开销等,也可以看到SQL是否被阻塞。
如果脚本执行报错:sql_handle 不是可识别的表提示选项。如果它要作为表值函数或 CHANGETABLE 函数的参数,请确保您的数据库兼容模式设置为 90。则需要修改数据库-属性-选项-兼容性级别到SQL Server 2005以上
。
输出字段解释:
-
session_id:会话id,可以通过
kill + session_id
结束会话。 -
SQL
:具体的执行SQL,存储过程等被调用时会直接显示其定义。 -
db_name:会话所在数据库名称。
-
blocking_session_id
:SQL被阻塞的会话,0表示没有被阻塞,非0表示被阻塞会话ID。 -
duration
:SQL执行时间,单位为毫秒(ms)。 -
client_net_address:客户端IP。
-
cpu、physicanl_reads、logical_reads、writes
:SQL产生的cpu开销、物理读、逻辑读、写入。 -
command:命令类型。
-
status
:SQL状态,running表示正在执行,runnable表示等待cpu调度,suspended
表示等待挂起。 -
wait_type
:等待事件。 -
wait_time
:等待时间。
注意点:sp_trace_getdata表示sqlprofile监控,无需关注。通常我们可以观察SQL执行时长(duration)来判断数据库是否存在性能问题,然后进一步排查SQL阻塞,等待事件找到问题原因。
问题样例
:服务器CPU较高,脚本排查发现部分SQL逻辑读、cpu开销较大,优化SQL后CPU正常。
4.2. 阻塞脚本
脚本介绍:可以看到SQL Server中阻塞的源头信息,例如发生级联阻塞,A阻塞B,B阻塞C,C阻塞D,那么通过脚本我们可以看到A阻塞B的信息,即阻塞源头,只有找到源头,我们才可以解决阻塞问题。
输出字段解释:
-
WaitingSessionID
:被阻塞会话ID。 -
BlockingSessionID
:阻塞会话ID。 -
WaitingSession_TSQL
:被阻塞SQL。 -
BlockingSession_TSQL
:阻塞SQL。 -
WAIT_DURATION_MS
:阻塞时长,单位ms。 -
is_blocked:阻塞会话本身是否被阻塞。
-
STATUS
:阻塞会话状态,sleeping为处于挂起状态。 -
wait_type:阻塞会话等待类型。
-
wait_time:阻塞会话等待事件。
注意点:WHERE P.blocked = 0
条件去掉可以看到当前数据库中的所有阻塞,加上的话只能看到阻塞源头,下面模拟输出下:
-
不带
where P.blocked = 0
条件输出如下:
-
带
where P.blocked = 0
条件输出如下:
-
通过
SQL状态脚本
输出如下,与不带where P.blocked = 0
类似,但是无法看到处于挂起的事务状态:
4.3. 表统计脚本
脚本介绍:通过脚本,可以看到某个库下各个表的实际大小,比较实用。大家可以按照实际需求排序输出,因为数据大小都是自动带单位的,所以没法直接排序,这里习惯以data长度降序输出。
-
rows:表实际行数。
-
reserved:数据库为该表分配的空间。
-
data:表数据占用的空间。
-
index_size:表索引占用的空间。
-
unused:表上未使用的空间,大致等于reserved - data - index_size的值。