接着上一章: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