【科普】三大数据库运维脚本合集(建议收藏)

一. 大纲

在日常的数据库运维管理中,数据库脚本是排查问题必不可少的利器,好的运维脚本可以让我们快速排查出问题所在,并在最短时间内解决问题。

但是在实际项目中,很多项目组人员面对数据库方面的突发问题,特别是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脚本,排查问题必备。

SELECT
trx_mysql_thread_id as id,concat(t.PROCESSLIST_USER,'@',t.PROCESSLIST_HOST,':',t.PROCESSLIST_DB) As connection,
trx_state,trx_started,concat(timestampdiff(SECOND,trx_started,CURRENT_TIMESTAMP()),'s') AS duration,
trx_query,t.processlist_state as state,sum(h.rows_examined) as trx_rows_examined,trx_rows_locked,trx_rows_modified,trx_isolation_level,
group_concat(h.sql_text order by h.TIMER_START DESC SEPARATOR ';\n') As thd_last_query,t.thread_id
FROM
information_schema.INNODB_TRX b
LEFT JOIN performance_schema.threads t on t.processlist_id = b.trx_mysql_thread_id
LEFT JOIN performance_schema.events_statements_history h USING(thread_id)
where (trx_operation_state != 'sleeping before entering InnoDB' or trx_operation_state is null)
group by thread_id order by trx_started

脚本正常输出如下,事务都比较短(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解决。

SELECT
concat('kill ',id,';') as kill_sql,concat(user,'@',host,':',db) As connection,command,time,state,info,trx_started
FROM
INFORMATION_SCHEMA.processlist p left join INFORMATION_SCHEMA.INNODB_TRX trx on p.id = trx.trx_mysql_thread_id
WHERE (TO_SECONDS(now())-TO_SECONDS(trx_started) >= (SELECT MAX(Time) FROM INFORMATION_SCHEMA.processlist
WHERE STATE like 'Waiting for%' and command != 'Daemon') or STATE like 'Waiting for%') and command != 'Daemon'
order by trx_started desc,time desc

输出字段解释:

  • 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行锁等待超时问题。

SELECT
r.trx_mysql_thread_id waiting_id,b.trx_mysql_thread_id blocking_id,
concat(timestampdiff(SECOND,r.trx_wait_started,CURRENT_TIMESTAMP()),'s') AS duration,
t.processlist_command state,r.trx_query waiting_query,b.trx_query blocking_current_query,
group_concat(h.sql_text order by h.TIMER_START DESC SEPARATOR ';\n') As thd_last_query
FROM
information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id
LEFT JOIN performance_schema.threads t on t.processlist_id = b.trx_mysql_thread_id
LEFT JOIN performance_schema.events_statements_history h USING(thread_id)
group by thread_id order by r.trx_wait_started

输出字段解释:

  • 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. 长事务脚本

脚本介绍:在项目中经常会遇到前台系统页面卡死、流程周转时间长、页面无响应等场景,公司项目开发通常会不假思索的认为数据库有“死锁”,然而开发人员往往忽略了数据库存在事务的概念,如下脚本可以为运维或者开发人员查询当前数据库执行的事务信息,方便排查问题。

select
xid,sql_id,username,inst_id,sid,serial#,status,start_time,el_second,sql_text,prev_sql_text
from
(with ltr as (select to_char(sysdate,'YYYYMMDDHH24MISS') TM, s.inst_id, s.username,
s.sid,s.serial#,s.status,s.sql_id,s.sql_child_number,s.prev_sql_id,xid,
t.start_scn,to_char(t.start_date,'YYYY-MM-DD HH24:MI:SS') start_time,
e.TYPE,e.block,e.ctime/3600 runtime_Hour,
decode(e.CTIME, 0, (sysdate - t.start_date) * 3600*24, e.ctime) el_second
from
gv$transaction t, gv$session s,gv$transaction_enqueue e
where t.start_date <= sysdate - interval '1' second and t.addr = s.taddr and t.addr = e.addr(+) )
select ltr.* ,(select q1.sql_text from gv$sql q1 where ltr.prev_sql_id = q1.sql_id(+) and rownum = 1) prev_sql_text ,
(select q1.sql_text from gv$sql q1 where ltr.sql_id = q1.sql_id(+)
and ltr.sql_child_number = q1.CHILD_NUMBER(+) and rownum=1) sql_text from ltr ltr)
  • 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会话:

--kill语法
alter system kill session 'sid,serial#,@inst_id' immediate;
 
--单实例下kill会话,可以隐藏实例号
alter system kill session '206,3237' immediate;
 
--rac环境下kill会话,需要标注实例号
alter system kill session '206,3237,@1' immediate;
alter system kill session '206,3237,@2' immediate;
3.2. 行锁等待脚本

脚本介绍:在很多情况下经常会遇到DML语句被阻塞,导致前台挂起,流程无法流转下去,以下脚本能够查询因为行锁等待而引起的SQL阻塞。

select s1.username blocking_user,s1.machine blocking_machine,
s1.sid blocking_sid,s1.serial# blocking_serialnum,
chr(10),
s2.username wait_user,s2.machine wait_machine,
s2.sid wait_sid,s2.serial# wait_serialnum,
substr(w1.sql_text,1,50) wait_sql,
do.object_name obj_name,
'ALTER SYSTEM KILL SESSION ''' || s1.sid || ',' || s1.serial# ||
''' IMMEDIATE;' Kill_Command
from v$lock l1,
v$session s1,
v$lock l2,
v$session s2,
v$locked_object lo,
v$sqlarea b1,
v$sqlarea w1,
dba_objects do
where s1.sid=l1.sid
and s2.sid=l2.sid
and l1.id1=l2.id1
and s1.sid=lo.session_id
and lo.object_id=do.object_id
and l1.block =1
and s1.prev_sql_addr=b1.address
and s2.sql_address=w1.address
and l2.request >0;
  • 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以上

select session_id,(select text from sys.dm_exec_sql_text(sql_handle)) as SQL,
(select DB_NAME(database_id)) as db_name,blocking_session_id,
(select top 1 client_net_address from sys.dm_exec_connections where session_id = sys.dm_exec_requests.session_id) as client_net_address,
total_elapsed_time as duration,cpu_time as cpu,reads as physical_reads ,logical_reads as logical_reads,writes,command,
status,wait_type,wait_time,last_wait_type,open_transaction_count,transaction_isolation_level,
percent_complete,estimated_completion_time from sys.dm_exec_requests
where session_id > 50 and status != 'background' order by duration desc,cpu desc,reads desc

输出字段解释:

  • 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的信息,即阻塞源头,只有找到源头,我们才可以解决阻塞问题。

SELECT R1.session_id AS WaitingSessionID
,S.session_id AS BlockingSessionID
,Q1.TEXT AS WaitingSession_TSQL
,Q2.TEXT AS BlockingSession_TSQL
,R1.wait_time AS WAIT_DURATION_MS
,P.blocked AS is_blocked
,P.STATUS
,R2.wait_time
,R2.wait_type
,R2.total_elapsed_time
,P.open_tran
,P.cmd
,(SELECT db_name(R1.database_id)) AS DBName
,S.original_login_name AS BlockingSession_LoginName
,S.program_name AS BlockingSession_ApplicationName
,S.host_name AS BlockingSession_HostName
FROM sys.dm_exec_requests AS R1
INNER JOIN sys.dm_exec_sessions AS S ON R1.blocking_session_id = S.session_id
INNER JOIN sys.dm_exec_connections AS C1 ON R1.session_id = C1.most_recent_session_id
INNER JOIN sys.dm_exec_connections AS C2 ON S.session_id = C2.most_recent_session_id
INNER JOIN sys.sysprocesses AS P ON S.session_id = P.spid
LEFT JOIN sys.dm_exec_requests AS R2 ON R2.session_id = R1.blocking_session_id
CROSS APPLY sys.dm_exec_sql_text(C1.most_recent_sql_handle) AS Q1
CROSS APPLY sys.dm_exec_sql_text(C2.most_recent_sql_handle) AS Q2
WHERE P.blocked = 0

输出字段解释:

  • 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条件去掉可以看到当前数据库中的所有阻塞,加上的话只能看到阻塞源头,下面模拟输出下:

--A会话处于事务挂起状态
1) begin transaction;
2) update a set id = 100 where id = 1;
3) Sleep;
 
--B会话被A会话阻塞
update a set id = 200 where id = 1;
 
--C会话被B会话阻塞
delete a where id = 1;
  1. 不带where P.blocked = 0条件输出如下:

  1. where P.blocked = 0条件输出如下:

  1. 通过SQL状态脚本输出如下,与不带where P.blocked = 0类似,但是无法看到处于挂起的事务状态:

4.3. 表统计脚本

脚本介绍:通过脚本,可以看到某个库下各个表的实际大小,比较实用。大家可以按照实际需求排序输出,因为数据大小都是自动带单位的,所以没法直接排序,这里习惯以data长度降序输出。

create table #tmp(
name varchar(50),
rows int,
reserved varchar(50),
data varchar(50),
index_size varchar(50),
unused varchar(50)
);
 
insert into #tmp (
name, rows, reserved, data, index_size, unused
) exec sp_MSforeachtable @command1="sp_spaceused '?'";
 
select * from #tmp where name <> 'tmp' order by len(data) desc ;
 
drop table #tmp ;
  • rows:表实际行数。

  • reserved:数据库为该表分配的空间。

  • data:表数据占用的空间。

  • index_size:表索引占用的空间。

  • unused:表上未使用的空间,大致等于reserved - data - index_size的值。

posted @ 2021-06-17 15:42  时光与我恰经过  阅读(782)  评论(0编辑  收藏  举报