这些天看了一篇微软官方发布的MS SQL Server2008性能问题处理及优化的英文文档,里面知识点介绍地很详细,在现实工作中也很实用,遂产生了想把它翻译一下的念头。翻译的过程,既可以帮助自己复习一下这些技术,也可以向其他还不熟悉这一块的朋友介绍一些新的知识,何乐而不为呢。只是这篇文章有点长,我会分成几篇随笔去介绍,所以,不光是对我耐性的考验,也是对你的考验哦!
--------------------------------------------
运行缓慢或者持续运行很长时间的查询会导致过度的资源消耗。它们可以导致查询阻塞的严重后果。
过度的资源消耗不局限于只占用CPU资源,也会增加I/O带宽以及内存带宽。尽管SQL Server查询可以通过where谓词的限制避免进行全表扫描,但是由于缺少支持这个特定查询所需要的索引,所以他们可能不会按照预期的方式执行。同样,where谓词页可以由应用程序动态进行构建,或者依赖于用户的输入。综上,已经存在的索引不能覆盖所有可能出现的限制。T-SQL语句可能会导致的过度的CPU,I/O,以及内存消耗的情况,在前面的章节中已经有所介绍。
此外,没有使用索引的话,可能是因为存在索引只是没有被使用。因为所有的索引都不得不被维护,这可能不会影响到查询的性能,但是会影响到那些DML的查询。
查询运行缓慢也可以由逻辑锁等待或者系统资源阻塞查询而导致。导致阻塞的原因可以是设计不好的应用程序,不好的查询计划,缺失有效的索引,或者是相对于当前负载配置不足的sql 实例。
这一章专注于导致运行缓慢的两种原因:阻塞和索引问题。
阻塞
阻塞主要是为了等待逻辑锁,例如等待获取加在资源上的X锁(排他锁)或者等待来在于较低级别同步基元的结果,例如闩锁。
逻辑锁等待发生在某个请求想要在一个已经被加过锁的资源上加上另一个不兼容的锁时。尽管这需要在这个特定的正在运行的查询语句中添加保证数据一致性的事务隔离级别,但是这会让用户有一种SQL Server运行很缓慢的感觉。当一个查询阻塞之后,它就不会在消耗任何的系统资源了,所以你会发现运行速度很慢,但是占用的系统资源很少。
如果你的系统没有被配置成可以处理当前的系统负载,那么会导致等待较低级别的同步基元。
阻塞及等待的通用场景如下:
--确定阻塞者
--确定长的阻塞
--每个对象对应的阻塞
--页闩问题
--使用SQL Server等待造成的阻塞对性能影响的纵览
如果当前系统资源不能为请求提供服务时,一个SQL Server会话被放置在一个等待状态里。换句话说,等待会在一队请求都在请求资源时出现。DMVs可以提供任何等待资源的会话信息。
SQL Server 2008提供详细且连续的等待信息,报告大概会有125中等待类型。这些DMVs从sys.dm_os_wait_statistics及sys.dm_os_waiting_tasks中获取这些信息,sys.dm_os_wait_statistics显示SQL Server全部或者逐渐积累的等待信息,sys.dm_os_waiting_tasks是指定在会话中的,它会按照会话阻断等待。下面的DMV提供了等待队列中的所有正在等待资源的任务。它会即时显示系统中等待队列中的所有任务。例如,你可以使用下面查询查找56号阻塞会话的详细信息.
select * from sys.dm_os_waiting_tasks where session_id=56
下面的结果显示出了56号会话正在被53号会话阻塞,并且56号会话已经耗费了1,03,500毫秒去等待一个锁。
- waiting_task_address: 0x022A8898
- session_id: 56
- exec_context_id: 0
- wait_duration_ms: 1103500
- wait_type: LCK_M_S
- resource_address: 0x03696820
- blocking_task_address: 0x022A8D48
- blocking_session_id: 53
- blocking_exec_context_id: NULL
- resource_description: ridlock fileid=1 pageid=143 dbid=9 id=lock3667d00 mode=X
associatedObjectId=72057594038321152
为了去查找有哪些会话已经被授权了锁或者正在等待锁,你可以使用sys.dm_tran_locks视图。所有的行展示了当前所有活动的请求,哪些已经被授权锁,哪些因为资源上已经加了锁所以正在等待授权锁。对于有规律的锁而言,一个已经被授权的请求会显示出已经为这个请求在这个资源上授权了锁。一个等待的请求说明还没有在资源上为这个请求授权锁。例如,下面的查询及输出显示了56号会话在资源1:143:3上的请求被53号会话已经授权的X锁阻塞了。
select request_session_id as spid, resource_type as rt, resource_database_id as rdb, (case resource_type WHEN 'OBJECT' then object_name(resource_associated_entity_id) WHEN 'DATABASE' then ' ' ELSE (select object_name(object_id) from sys.partitions where hobt_id=resource_associated_entity_id) END) as objname, resource_description as rd, request_mode as rm, request_status as rs from sys.dm_tran_locks
下面是示例输出:
spid rt rdb objname rd rm rs
----------------------------------------------------------------------------- 56 DATABASE 9 S GRANT
53 DATABASE 9 S GRANT
56 PAGE 9 t_lock 1:143 IS GRANT
53 PAGE 9 t_lock 1:143 IX GRANT
53 PAGE 9 t_lock 1:153 IX GRANT
56 OBJECT 9 t_lock IS GRANT
53 OBJECT 9 t_lock IX GRANT
53 KEY 9 t_lock (a400c34cb X GRANT
53 RID 9 t_lock 1:143:3 X GRANT
56 RID 9 t_lock 1:143:3 S WAIT
锁粒度及锁升级
理解阻塞的一个关键点是理解事务隔离级别及锁升级。在所有事务隔离级别下,事务隔离级别只会干预处于S锁模式下的事务却不会去影响处于X锁模式的事务。锁的粒度决定了锁的影响级别,是在行上,还是在页级别,还是在表级别。很明显,锁的粒度越高,并发性也就越低。例如,如果一个事务在表级别上使用X锁无修改某一行的数据,它会阻塞其它想要读取或修改其它行的事务。反之,高粒度锁的好处是SQL Server无需获得如此多的锁。锁粒度是由SQL Server使用启发模式自行管理的,但是这里也会有一些情况可能会与预期的不一致。例如这种情况,用户可以使用sp_tableoption或者alter index ddl去控制对象上的锁粒度或者使用锁定提示(locking hints)。
--对于锁,还有另外一个很有意思的扭转。如果在运行时,SQL Server发现语句中加载到某个对象上的锁的数量已经超过了阈值,那么可以对这个表上的锁进行升级。锁升级会在下面的情况发生时被触发:
- 在事务的某一单独语句中,需要在两个索引或者两个堆上分别使用2500个锁
- 在事务的某一单独语句中,需要在非聚集索引上加2500个锁并且需要在对应的基表上加2500个锁
- 在一个语句中,同一个堆或者索引被引用超过了两次;在同一个实例上,像这样的锁实际上会被分开计数,造成了重复。所以比如,要在table1上使用self-join,如果每个实例在这个语句中会有3000个锁,锁升级就不会被触发
--如果锁配置选项被设定成了默认值0,那么被锁定的资源会比non-AWE(32-bit)或者正常的(64-bit)的多占用40%的内存。在这种情况下,锁定的内存是动态进行分配的。
当锁升级被触发以后,SQL Server尝试去升级锁到表级别,但是如果有冲突的锁存在时,这种尝试就会失败。所以比如,如果S锁需要升级至表级别并且当前同时在行上或者页上存在着X锁,锁尝试升级就会失败。然而,SQL Server会周期性地,每当有1250(通常情况下)个新的锁被锁所有者(也就是说,事务初始化这些锁)申请之后,就会尝试性地去升级锁。如果锁升级成功的话,SQL Server就会释放较低粒度的锁,以及索引及堆所对应锁定的内存。因为在锁升级期间,不能有任何冲突性质的访问,一个成功的锁升级可能会影响接下来锁模式相冲突的事务处理,并导致出现阻塞。也就意味着,锁升级并不总是一个针对所有应用程序的好办法。
禁用锁升级
SQL Server提供了下面的追踪标识去禁用锁升级:
--TraceFlag-1211:在每一个索引,每一个对或者每一个语句的基础上,当超过了阈值(5000)时,锁升级就会被禁用掉。如果这个追踪标识起作用的话,锁就永远不会被升级。这个追踪标识也会指引SQL Server忽略掉锁管理器已经申请到的内存,从而使内存达到一个最大的静态分配内存或non-AWE(32-bit)的60%,或者是正常的(64-bit)动态分配的内存。这时,一个out-of-lock-memory错误会被生成。这些是存在着潜在威胁的,因为一个应用程序会申请大量的锁紧而耗费掉SQL Server的内存。这也就是说,在这种最坏的情况,会使SQL Server的性能处于一个非正常的水平。对于这些因素,你必须非常谨慎地使用追踪标识。
--TraceFlag-1224:这个追踪标识跟1211号追踪标识很像,但也存在着一个很关键的不同。当锁管理器申请40%的静态分配内存或者40%的non-AWE(32-bit)或者正常的动态分配分配内存(64-bit)时,它就会启用锁升级。此外,如果这些内存因为其它的组件正在使用更多的内存而不能被分配,锁升级就会被提前触发。SQL Server会生成一个内存溢出的错误,当内存超过了静态分配内存或者60%的nonAWE(32-bit)或者正常的动态分配内存(64-bit)。
如果两个追踪标识(1211和1224)同时被设定,1211会被优先使用。你可以使用DBCC TRACESTATUS(-1)命令去查找SQL Server中所有被启用的追踪标识的状态。这些追踪标识的限制是它们的粒度很粗(也就是说,它们是在SQL Server实例的级别的)。SQL Server通过在表级别提供一个选项去控制启用或者禁用锁升级。第二,SQL Server2008在分区表级别提供锁升级,这样做的目的是防止DML操作不会影响其他的分区。