接着上一章:CPU瓶颈 I/O瓶颈(I/O Bottlenecks): SQLServer的性能严重依赖I/O子系统。除非你的数据库完全加载到物理内存中,否则SQLServer会不断地把数据库文件从缓存池中搬进搬出,这会引起大量的I/O传输。同样地,日志记录在事务被声明为已提交前必须写入磁盘。最后,SQLServer基于许多原因使用tempdb,比如存储临时结果、排序和保持行版本。所以一个好的I/O子系统是SQLServer性能关键。 除非数据文件包括tempdb需要回滚事务,否则日志文件是顺序访问的。而数据文件和tempdb是随机访问的。所以作为常规规则,你应该把日志文件与数据文件分离到独立的磁盘中。本文不是关注于如何配置你的I/O设备,但关注于如何识别你的系统是否有I/O瓶颈。在I/O瓶颈被识别之后,你应该重新配置你的I/O子系统。 如果你的I/O子系统很慢,你的用户将体验得到性能问题,响应时间过慢和因为超时而导致任务失败。 可以使用以下的性能计数器去识别I/O瓶颈。但是要注意,如果你的收集间隔过短,那么平均值会趋向倾斜于低值那段。比如,很难说明为什么I/O会每60秒涨跌。同时,你也不能仅仅根据一个计数器的值来确定是否有瓶颈。需要通过多个值来反复验证你的想法: PhysicalDisk Object:Avg.Disk Queue:物理读写请求锁等待的平均队列值。当该值长期超过2时,你的系统可能存在I/O瓶颈了。 Avg.Disk Sec/Read:是一个平均秒数,是每秒从磁盘上读取数据的次数,下面是值及其代表意思: • 小于10ms ——非常好 • 10~20ms——OK • 20~50ms——慢,需要重视 • 大于50ms——严重的I/O瓶颈。 Avg.Disk Sec/Write:与Avg.Disk Sec/Read相对应。 Physical Disk:%Disk Time:是针对被选定的磁盘忙于读写请求所运行时间的百分数。一般的指标线是大于50%就意味着有I/O瓶颈。 Avg.Disk Reads/Sec:是读操作在磁盘上的频率。确保这个频率低于磁盘极限的85%,当超过了85%后,访问时间就会以指数速度增加。 Avg.Disk Writes/Sec:于Avg.Disk Reads/Sec相对应。 当你使用这些计数器时,你需要对于RAID作出调整,可以使用以下公式: Raid 0 -- I/Os per disk = (reads + writes) / number of disks Raid 1 -- I/Os per disk = [reads + (2 * writes)] / 2 Raid 5 -- I/Os per disk = [reads + (4 * writes)] / number of disks Raid 10 -- I/Os per disk = [reads + (2 * writes)] / number of disks 比如,如果你有一个RAID-1,使用两个物理磁盘,计数器值为: Disk Reads/sec 80 Disk Writes/sec 70 Avg.Disk Queue length 5 这样通过公式计算:(80 + (2 * 70))/2 = 110 I/Os 每个磁盘,而你的磁盘等待队列长度等于5/2=2.5。意味着已经到达了I/O瓶颈边界。 你也可以检查lacth等待来识别I/O瓶颈。这种等待说明当一些页面用于读或者写访问时,同时这些页面在缓冲池中不可用(或者不存在)而造成的物理I/O等待。当页面在缓冲池中找不到时。就会产生一个异步的I/O,然后检查这个I/O的状态。当I/O状态已经被标注为已完成时,此时工作负载趋于平衡。否则,将会等待PAGEIOLATCH_EX 或者PAGEIOLATCH_SH,这根据请求类型而定。可以使用一下DMV来发现I/O闩锁的等待统计信息: [sql] view plain copy print? 1. Select wait_type, 2. waiting_tasks_count, 3. wait_time_ms 4. from sys.dm_os_wait_stats 5. where wait_type like 'PAGEIOLATCH%' 6. order by wait_type 下面是结果的例子: wait_type waiting_tasks_count wait_time_ms signal_wait_time_ms ----------------------------------------------------------------------- PAGEIOLATCH_DT 0 0 0 PAGEIOLATCH_EX 1230 791 11 PAGEIOLATCH_KP 0 0 0 PAGEIOLATCH_NL 0 0 0 PAGEIOLATCH_SH 13756 7241 180 PAGEIOLATCH_UP 80 66 0 当I/O完成时,工作线程将被至于可运行队列。I/O完成到工作线程确实被排程的时间在signal_wait_time_ms列中可以看到,如果你的waiting_task_counts和wait_time_ms有偏离常值,证明有I/O问题。对于这种情况,有必要在SQLServer运行正常时,建立性能基线和关键的DMV查询输出。这些等待类型能显示出你的I/O子系统是否有严重的瓶颈。但它们不提供任何可见的物理磁盘问题 你可以通过下面的DMV查询来找到目前正在挂起的I/O请求。你可以定期执行下面语句来检查I/O子系统的健康情况和隔离那些有I/O瓶颈的物理磁盘: [sql] view plain copy print? 1. select 2. database_id, 3. file_id, 4. io_stall, 5. io_pending_ms_ticks, 6. scheduler_address 7. from sys.dm_io_virtual_file_stats(NULL, NULL)t1, 8. sys.dm_io_pending_io_requests as t2 9. where t1.file_handle = t2.io_handle 下面是一个输出例子,是对特定的数据库输出,在运行查询的时刻,有3个被挂起的I/O请求。你可以使用database_id 和file_id列来查找文件所映射的物理磁盘。Io_pending_ms_ticks值表示单个I/O在挂起队列中等待的总时间。 Database_id File_Id io_stallio_pending_ms_ticksscheduler_address ------------------------------------------------------------- 6 1 10804780x0227A040 6 1 10804780x0227A040 6 2 101451310x02720040 解决方案: 当你发现有I/O瓶颈时,你第一本能反应可能是升级I/O子系统,以应对目前的工作负载。这种方式当然有效,但是在此之前,你要考虑在硬件投入上的开销,要检查I/O瓶颈是否因为不正确的配置和/或查询计划导致的。我们建议你根据以下步骤去检查: 1、 配置(Configuration):检查SQLServer的内存配置。如果SQLServer配置中存在内存不足的问题,这会引起更多I/O开销。你可以检查下面的计数器来识别是否存在内存压力: Buffer Cache hit ratio Page Life Expectancy Checkpoint Pages/sec Lazywrites/sec 关于内存压力将在内存篇详细说明 2、 查询计划:检查执行计划和识别哪步导致了更多的I/O消耗。尽可能选择更好的方法比如索引来最小化I/O。如果存在丢失索引,可以使用DTA来找到。 下面的DMV查询可以用于发现批处理或者请求产生最多的I/O的查询。注意这里不统计物理写,如果你懂得数据库是如何运作的,就会知道为什么。在同一个请求中DML和DDL语句,不是直接把数据页写入磁盘,而只有已经提交的事务才会被写入磁盘。通常物理写只在checkpoint或者lazywriter发生时才出现。可以使用下面的DMV来查找产生最多I/O的5个查询,优化这些查询以便实现更少的逻辑读,并进一步缓解缓存池的压力。这样你能提高其他请求在缓存池中直接找到数据的机会(特别在重复执行时),从而替代物理I/O的性能。因此,这个系统的性能都能得到改进。 下面是通过hash join来做两表关联的例子: [sql] view plain copy print? 1. create table t1 (c1 int primary key, c2 int, c3 char(8000)) 2. create table t2 (C4 int, c5 char(8000)) 3. go 4. 5. 6. --load the data 7. declare @i int 8. select @i = 0 9. while (@i < 6000) 10. begin 11. insert into t1 values (@i, @i + 1000, 'hello') 12. insert into t2 values (@i,'there') 13. set @i = @i + 1 14. end 15. --now run the following query 16. select c1, c5 17. from t1 INNER HASH JOIN t2 ON t1.c1 = t2.c4 18. order by c2 19. 20. 21. Run another query so that there are two queries to look at for I/O stats 22. 23. 24. select SUM(c1) from t1 这两个查询在一个批处理中运行,接下来。使用下面的DMV来检查查询引起的I/O: [sql] view plain copy print? 1. SELECT TOP 5 2. (total_logical_reads/execution_count) AS avg_logical_reads, 3. (total_logical_writes/execution_count) AS avg_logical_writes, 4. (total_physical_reads/execution_count) AS avg_phys_reads, 5. execution_count, 6. statement_start_offset as stmt_start_offset, 7. (SELECT SUBSTRING(text, statement_start_offset/2 + 1, 8. (CASE WHEN statement_end_offset = -1 9. THEN LEN(CONVERT(nvarchar(MAX),text)) * 2 10. ELSE statement_end_offset 11. END - statement_start_offset)/2) 12. FROM sys.dm_exec_sql_text(sql_handle)) AS query_text, 13. (SELECT query_plan from sys.dm_exec_query_plan(plan_handle)) as query_plan 14. FROM sys.dm_exec_query_stats 15. ORDER BY (total_logical_reads + total_logical_writes)/execution_count DESC 你当然可以通过改变查询语句来获得不同的数据显示。比如你可以按(total_logical_reads + total_logical_writes)/execution_count 来排序。作为选择,你可能想去按物理I/O来排序等,但是,逻辑读写书对判断是否有I/O问题是很有用的。输出类似这样: avg_logical_reads avg_logical_writes avg_phys_reads ----------------- ------------------ --------------- 16639 10 1098 6023 0 0 execution_count stmt_start_offset --------------- ----------------- 1 0 1 154 Query_text Query_plan ----------------------------------- ----------- select c1, c5 from t1 INNER HASH JOIN … <link to query plan> select SUM(c1) from t1 <link to query plan> 这些输出告诉你一些重要的信息,第一,显示最多的I/O。你也可以通过SQL Text列来查看是否可以通过重写语句来降低I/O。验证这些执行计划是否已经最佳的。比如,一个新的索引可能有帮助。第二、第二个批处理不引起任何物理I/O因为所有需要的表的页面已经缓存到缓冲区。第三、执行次数能用于识别是否它是一个一次性查询或者它是否频繁执行,因此需要对此详细考量。 3、 数据压缩:从2008开始,你能使用数据压缩来降低表和索引的体积。压缩程度完全取决于架构和数据分布。一般情况下,可以达到50~60%的压缩率。一些特殊情况下可以达到90%。意味着当你能压缩到50%时,你已经比较有效地降低了I/O。数据压缩会引起CPU增加。这里有一些策略: 为什么不把整个数据库压缩?对此,给出一个极端的例子:如果你有一个大表,叫做T,有10页,而整个数据库有1000万页。压缩T没有多大好处。即使SQLServer能把10页压缩到1页,你努力减少数据库的大小,但你可能会造成CPU的负担增加。在现实的工作负载中,不能很明显地作出选择。但是这个例子只是你在压缩前要考虑的情况而已。我们的建议是:在你压缩一个对象之前,使用sp_estimate_data_compression_savings存储过程来评估它的大小、利用情况和预估压缩等信息。注意以下信息: 对象的大小是否比数据库总体大小小很多,这样的情况不会给你带来太多好处。 如果对象经常被用于DML或者SELECT操作,你将面临比较大的CPU消耗。特别是在CPU已经存在瓶颈时,你可以使用sys.dm_db_index_operational_stats去发现对象使用情况来判断表、索引、分区等等的命中情况。 压缩预留情况是基于架构和基于数据的。实际上,一些对象,压缩后可能会更大。或者节省的空间会微不足道。 如果你有一个分区表,且某些分区的数据不经常访问。你可以使用页压缩来压缩分区和重组索引。这适用在不长使用的分区上。相信信息可以看:(http://blogs.msdn.com/sqlserverstorageengine/archive/tags/Data+Compression/default.aspx) 4、 升级I/O子系统:如果你确保SQLServer的配置合理,并且检查执行计划后仍然存在I/O瓶颈,最后的选择就只能升级I/O带宽了: 增加更多的物理驱动或者更换更快的磁盘。 增加更快的I/O控制器。 下一章:tempdb