前DB2实验室专家眼中的DB2性能调优
Posted twt企业IT社区
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前DB2实验室专家眼中的DB2性能调优相关的知识,希望对你有一定的参考价值。
以前在实验室工作的时候,总觉得DB2的优化是一个很难的话题。不仅是因为涉及存储、CPU、内存、系统参数、实例参数、数据库参数等各种数据库管理知识,更重要的是因为DB2里面分成了很多组件,每个组建都有几十甚至几百个参数或者监控项。做一个通用的优化方法几乎就是不可能的。
现在,作为数据库的使用者,看到生产系统上各种各样的性能问题。就不能因为方法难而不去做了。解决了一些问题以后,发现对终端客户来说,一些通用的方法还是有必要的。因为这些方法差不多能解决80%以上的性能问题。
反正也睡不着,总结一下这些年优化DB2性能的经验吧。
在我经历的这些问题的性能优化中,90%的性能问题发生在应用程序,而这90%的问题又基本上通过以下办法中的前两个解决80%以上的问题。
·保持准确的统计信息
统计信息时合理的执行计划的基础,没有准确的统计信息,就不一定能生成最优的执行计划。举个例子,一张表T中实际优1000万行数据,现在有根据某一列近似唯一键的索引去精确查询一条记录。如果统计信息准确,那执行计划就会选择这条索引去查询;但是如果如果统计信息很长时间没有更新了,而上一次更新统计信息的时候,表里面只有4条数据,那执行计划可能就是一个全表扫描了。
那怎样看统计信息相关的信息呢?其实,和统计信息相关的系统表有好几个。因为是简单总结,我只用syscat.tables做个例子。用以下语句可以看到这张表上一次更新统计信息的时间和上次更新统计信息时,这张表中数据的行数:
db2 "select substr(tabname,1,50), stats_time, card from siesta.tables where tabname='<tabname>' with ur"
更新统计信息的方法也有很多讲究,这里,我们简单粗暴的使用:
db2 "runstats on table <tabschema>.<tabname> with distribution and detailed indexes all table sample system(<per>)"
·建立合适的索引
虽然标题很简单,但是建立合适的索引却不是一件能简单做到的事情。简单来讲,有五条规则:
1. 在能过滤出最少数据集的过滤条件列上创建的索引。
2. 在关键的连接条件列上建立的索引。
3. 再排序、聚合的列上考虑使用索引。
4. 注意使用index only scan。
5. 设计能使用索引的数据模型和写能使用索引的语句。
例如,应用经常使用身份证号查询某个客户,而身份证号几乎能精确的定位到一个客户,那身份证号就是一个很好的索引列。
再例如,某一行的一列表示状态,所有的数据中,只有4种状态0,1,2,3。而状态是0的数据占特别少数,而我们的查询确经常查询状态是0的数据,那么,状态也是一个很好的索引列选项。
还有一种使用索引以后,性能提升很明显的案例,就是select count(*)这种语句,因为如果我们把这个语句中涉及的列全都加到索引中,这种语句可以使用index only scan。
最后简单说一下,有一些语句,例如substr(c1,1,20), func(c1,c2)这种用法,不太容易选择索引,即使选择了索引,效率也不一定高。
·写简单的语句
简单的语句更容易生成好的执行计划,遇到问题的时候也更容易诊断。简单不是说语句短,而是说可读性要好。曾经见到过一个语句,是5张表进行关联,where条件中类似于下面的写法:
where (a.c1=b.c1) or (a.c1=c.c1) or (a.c1=d.c1) or (a.c1=e.c1)
实际的语句条件比这复杂很多,后来我们改成了Union的形式以后,效率提高了很多。
·交易系统给用户返回尽量少的数据集
返回尽量少的数据有两个方法,一个是在查询中增加查询条件或选用过滤性强的条件,一个是使用fetch first n rows only的方式查出来前几行。
其实,对于交易系统来说,本来是不需要给前台返回太多数据的。例如,一个客户关系系统,即使查询中给客户经理返回2000条数据,他怎样从这里面找到需要的数据呢?
使用正确的rebind方式并且和合适的条件下对程序包进行rebind。
为什么要rebind呢?因为静态语句的执行计划在bind的时候就已经保存在数据库中了。随着业务数据的不断变化,静态语句的执行计划并不会因为这些数据的变化(即使进行了runstats)而重新优化。
为了让静态语句的执行计划更合理,应该选择好的时间进行runstats,并且进行rebind。
有些存储过程和静态语句使用的目的是为了封装业务逻辑,编译的开销对系统整体运行并没有太大的压力,或者说如果编译的时间和执行的时间比较起来非常短,其实可以对这些package使用reopt always的方式进行绑定,以保证每次执行都生成当前统计信息下的最优的执行计划。
·使用正确的数据迁移方式
我们的好多系统批处理的时候会涉及数据迁移。交易系统中,如果时间可以接受,采用insert into ......select的方式没有问题。但是理论上,最快的数据迁移方式是load......from cursor。
一、如何判断语句性能是否与统计信息有关
前面写了一下DB2经常遇到的性能问题和怎样解决这些性能问题。前面写到,runstats和create index能解决我遇到的大部分这样的性能问题,这篇,总结一下怎样判断如何判断一条语句的性能问题是不是和统计信息有关系。判断的方法其实就是一句话:比较一下正在使用的执行计划和当前应该使用的最优计划是不是一致。下面解释一下。
发生这种问题的时候,通常语句执行时间很长。如果这种语句又是很频繁的交易,那服务器的资源就很忙,甚至最终会导致系统无法响应服务。
这种情况下,使用db2 get snapshot for applications能看到很多处于UOW Executing状态的应用,并且,这些应用所执行的动态语句或者静态package中的section都是同一个。
这种问题还有一种表现,就是一些存储过程、静态程序执行时间大部分时间很短,但是某些时间执行时间很长。
1. 静态语句
静态语句的判断要直观一些,我们可以比较包含静态语句的package中的执行计划和最优的执行计划是不是一致。
首先使用db2 get snapshot for applications on <dbname>找到执行慢的应用(处于UOW Executing状态,而且有start time,但是没有end time的那些应用),从那个应用中,可以找到package名字和对应的section。
使用以下语句查看一下package中的执行计划:
db2expln -d <dbname> -t -g -c <schema> -p <pkgname>
根据执行计划和section号,也看一看到正在执行的语句是什么。
查看一下语句涉及的表的统计信息是不是及时:
db2 "select substr(tabname,1,30), card, stats_time from siesta.tables where tabname='<tabname>'"
如果统计信息不及时,更新一下统计信息:
db2 runstats on table <tabname> with distribution and detailed indexes all
然后生成动态语句的执行计划:
db2expln -d <dbname> -t -g -q "<SQL Statement>"
比较一下SQL语句的执行计划和package中的执行计划是不是一样。如果不一样,那就说明package中的计划已经不是最优的了。那就应该rebind一下,如果如果这个静态语句中没有循环等造成编译时间很长的情况,可以考虑使用reopt always选项。
2. 动态语句
动态语句稍微复杂一下。首先,也是通过
application snapshot找到运行时间长、消费资源多的SQL语句。
1)然后,我们找一下这条语句的的执行计划。
a)以下方法找到这条语句的 executable_id:
SELECT executable_id,STMT_EXEC_TIME,Total_cpu_time,Varchar(stmt_text,200) as stmt_text FROM TABLE(MON_GET_PKG_CACHE_STMT (NULL, NULL,NULL,-1)) AS T where stmt_text='<SQL Statement>'
b) 运行一下这个存储过程:
CALL EXPLAIN_FROM_SECTION( <executable_id>, 'M', NULL, 0, '<schema>', ?,
?, ?, ?, ? )
c) 执行db2exfmt输出执行计划到explain.out文件中:
db2exfmt -d <dbname> -g TIC -w -1 -n % -s % -# 0 -o explain.out
我们为什么没有直接用db2expln -q直接看语句的执行计划呢?因为很多从JDBC过来的语句,因为在客户端可能已经进行了某些转换、或者JDBC版本和服务器版本的兼容性不好、或者JDBC端使用了不同的选项(例如前翻、后翻等),这些情况下,db2expln显示出来的CLP语句的执行计划和实际从JDBC发过来语句的形式的执行计划是不一样的。
2)查看一下统计信息是不是及时,然后使用db2expln看一下动态语句现在的执行计划。
然后比较两次的执行计划是不是一样。
二、lock与latch
总结一下lock和latch吧。这两个是DB2数据库里面涉及应用等待的概念。lock是数据库中的概念,目标是为了保证数据的一致性,这个概念在数据库理论里面都有,用途就是保证只有一个transaction对数据进行修改;latch是DB2系统线程同步的机制,类似于操作系统中提供的信号灯的功能,其实latch也就是封装了信号灯等内容,目的是为了保证DB2系统本身运行的正确性。
我遇到的很多管理员遇到锁的问题就说死锁。所以,我还是普及一下这些基础该概念。
1. 锁等待:两个事务要去访问同一个资源,其中一个先锁定了,如果另一个需要等这个锁才能访问(比如过,A正在update某行记录,B也要update这行记录),那么第二个事务就处于锁等待状态。
2. 锁超时:如果我们不想等锁的那个事务一直等下去,可以设置一个超时时间,当那个事务等了这段时间以后,就不等了,直接roll back。这个现象是锁超时。
3. 死锁:两个事务,A事务首先在第一行上加了一个锁,B事务在第三行上加了一个锁,然后,A需要访问第三行才可以提交,而B事务需要访问第一行才可以提交。这种情况下,就发生了死锁。DB2为了避免两个应用一直互相等待,就会选择一个事务回滚,而让另一个事务提交。
锁超时和死锁对应用来说都会返回SQL0911N,但是reason code是不一样的。
latch不是数据库对象级别的概念,因为是线程同步的机制,所以,如果一个应用在某个阶段持有latch,那么别的应用对应的线程需要访问这个latch保护的资源时,也会等待。但是,DB2数据库不应该发生dead latch,或者说如果发生dead latch,那就是数据库的bug。
latch等待可以使用db2pd -latch来显示。这里面,Holder就是持有latch的线程(EDU),Waiter是等待latch的线程。
Latches:
Address Holder Waiter Filename LOC LatchType HoldCount
0x00000002018F0470 14 0 Unknown 1391 SQLO_LT_sqeWLDispatcher__m_tunerLatch 1
每个线程可以通过db2pd -edus看到。如果这个EDU对应的执行SQL语句的应用,使用db2 get snapshot for applications命令,在输出中查找长得这种模样的记录,就能查到这个EDU对应的应用:
Coordinator agent process or thread ID = 47
一般,如果latch等待用了很长时间,就要非常细致的分析EDU那个时候在做什么。如果持有latch的EDU是在执行某些语句,那就想办法让这些语句变快;但是如果这些应用在干什么并不太确定,就需要把db2pd -stack给IBM来分析了。
相对来讲,锁的问题就更容易诊断和解决。
1. 锁升级
一般来说,我们希望锁的粒度保持在行的级别,这样,就可以提高系统的并发性。但是,如果行级锁加了太多,导致licklist分配的内存到了某个阈值(locklist*4K*maxlock/100),那就要升级到表锁了。所以,为了保持系统的并发能力,避免锁升级,应该对locklist和maxlock有合理的设置。不能太小,小了太容易升级;不能太大,太大的话申请锁占用大量的内存,并且耗费时间,消耗系统性能。
2. 锁超时
我遇到的多数锁超时的问题,都是通过优化SQL语句,让应用以最快的速度执行结束,自然就降低了锁超时的概率。捕获锁等待我一般设置DB2_CAPTURE_LOCKTIMEOUT:设置上这个参数以后,每次发生锁超时,就会在db2dump下面生成一个文本文件,这个文件里面记录了持有锁和发生锁超时的应用及语句。这个参数的设置是动态的,不需要重启实例。
3. 锁等待
如果系统出现大量的锁等待,那毫无疑问,整个系统的资源使用率非常高,事务响应非常慢。发生这种问题的时候,使用db2pd -db <dbname> -wlock可以快速的判断出来是不是存在锁等待。如果确实有锁等待发生,马上运行db2 get snapshot for applications on <dbname>。
然后,在snapshot的输出中,查找处于UOW Lock-waiting状态的应用。在应用快照的最下方,能看到这个持有锁的应用。那个片段长得是这样的:
ID of agent holding lock = 20
Application ID holding lock = *LOCAL.instme.160123164720
Lock name = 0x02000600040000000000000052
Lock attributes = 0x00000000
Release flags = 0x00000000
Lock object type = Row
Lock mode = Exclusive Lock (X)
Lock mode requested = Update Lock (U)
Name of tablespace holding lock = USERSPACE1
Schema of table holding lock = INSTME
Name of table holding lock = T
Data Partition Id of table holding lock = 0
Lock wait start timestamp = 01/24/2016 00:47:55.155336
这样,就去找LOCAL.instme.160123164720这个应用。如果这个应用也处于lock waiting状态,就继续找下去,就可以找到最终持有锁的应用和这个应用这在执行的语句。然后对语句进行优化。
4. 死锁
我是这样觉得的,最好的应用不发生锁超时和死锁、次好的应用是即使发生了死锁和锁超时也有相应的异常处理逻辑、差的应用才会频繁发生锁超时和死锁。
为了避免死锁,从宏观上看,就是让语句快,从微观上看,要注意并发的事务访问表的顺序。但是无论怎样,可能都不能100%避免死锁。
如果发生了死锁,可以用死锁监控器来抓取数据(deadlock monitor),然后分析发生死锁的应用,从而进行优化。优化的方法就是提高语句的执行效率和调整事务访问表的顺序和方式(比如采用更低级别的隔离级别等方式)。因为死锁监控器的使用方式很容易查到,这就不重复了。
三、语句优化
这节,简单总结一下语句的优化吧。
前面,我们提到了,当应用有性能问题时,系统资源使用率一般比较高。这个时候,使用db2 get snapshot for applications on <dbname>可以抓到处于UOW Executing状态的应用,然后,可以从应用的快照中看到SQL语句。
当我们确定这些语句确实需要优化的时候,我一般的步骤是这样的(不一定最好,供参考)。
首先看这条语句的执行计划是不是明显不合理,比如说明显没有使用合适的索引,或者有明显有可以是语句性能提高的索引。
db2expln -d <dbname> -t -g -q "<SQL Statement>"
2. 看一下统计信息是否up to date。
db2 "select stats_time, card from syscat.tables where tabname='<tabname>'"
如果统计信息明显不及时,更新统计信息(表如果太大,就抽样更新)。
db2 "runstats on table <tabschema>.<tabname> with distribution and detailed indexes all"
3. 问题如果还没有解决,再次查看统计信息。找到开销最大的阶段。分析如何优化。
4. 如果语句太复杂,不能轻易的分析出最好的索引,运行db2advis看一下优化器的建议:
db2advis -d <dbname> [-s <statement> -f <file contain the SQL>]
如果优化器给出来的语句能提高90%以上,那就别犹豫了,索引一定能解决这个性能问题。通常,建议出来的需要创建的索引是比较好的,但是有时候,我们可以分析一下这些索引,从而发现更好的索引。
5. 到第4步,问题应该就能解决了。你可能在想,怎么没有reorg呢?我一般觉得表中的数据分布一般不会差到对性能影响太大,而reorg本身几乎让表处于不可访问状态。所以,我很少在业务在线的时候使用reorg。如果过了第四步,问题还没有解决,那麻烦了,这个时候可就得想各种各样的办法去优化了。
a) 语句改写能不能提高性能,如将or改成union all
b) 语句中的查询条件有没有函数,这些函数是不是可以取消
c) 能不能通过增加或者修改查询条件减少结果集
d) 我等着大家的评论....................
上面是在线的优化步骤,离线的优化方式就很灵活了,比如说分区表、MDC、生成列、reorg......
以上是关于前DB2实验室专家眼中的DB2性能调优的主要内容,如果未能解决你的问题,请参考以下文章
使用 like 谓词(模式匹配)对 DB2 Z/oS 的 SQL 查询进行性能调优