面向.Net程序員的後端性能優化實戰

最近2個月沒作什麼新項目 徹底是對於舊的系統進行性能優化 避免超時 死鎖 數據處理能力不夠等常見的性能問題html

這裏不從架構方面出發 畢竟動大手腳成本比較高 那麼咱們以實例爲前提 從細節開始 java


優化角度程序員

一.業務邏輯優化sql

二.DB優化數據庫

三.數據處理優化c#

四.鎖與性能後端

五.cpu飆高小結安全

六.crash現象分析性能優化


 業務邏輯優化

這一條不具備廣泛性 不一樣的業務不一樣的場景 若是概括起來 就是在不影響業務的前提下進行流程精簡服務器

1. 廢棄冗餘邏輯 

常見於各類基於數據庫的檢查 不少同窗在維護別人代碼的時候 沒有深刻理解別人的邏輯 也許別人在取數據的時候已經在查詢條件中已通過濾了相關邏輯 然後來維護的同窗又來了一次check

固然若是處於數據安全的角度 double check無可厚非,可是若是連鎖都沒有的double check 其實不作也罷。

畢竟 省一次dbcall 可能效果勝於你作的N多優化

2. 合併業務請求

出發點和上述一致 節省dbcall 可是存在一個矛盾的點 若是業務包在事務裏 這條須要慎重考慮 事務的設計原則裏 固然能小則小


DB優化

這個實際上是比較核心的點

1. 索引優化

這個點比較泛泛 可是作好的人很少 一個專攻於索引優化的人也能夠在運維方面獨當一面了

咱們拿實例來看一個索引優化例子

首先利其器 選中你須要的調試信息

而後打開自動統計信息更新 特別對於測試階段 數據以及數據量頻繁變動的時候 統計信息必定要記得刷新

  建立索引時,查詢優化器自動存儲有關索引列的統計信息。另外,當 AUTO_CREATE_STATISTICS 數據庫選項設置爲 ON(默認值)時, 數據庫引擎自動爲沒有用於謂詞的索引的列建立統計信息。隨着列中數據發生變化,索引和列的統計信息可能會過期,從而致使查詢優化器選擇的查詢處理方法不是 最佳的。 當 AUTO_UPDATE_STATISTICS 數據庫選項設置爲 ON(默認值)時,查詢優化器會在表中的數據發生變化時自動按期更新這些統計信息。 每當查詢執行計劃中使用的統計信息沒有經過針對當前統計信息的測試時就會啓動統計信息更新。 採樣是在各個數據頁上隨機進行的,取自表或統計信息所需列的最小非彙集索引。從磁盤讀取一個數據頁後,該數據頁上的全部行都被用來更新統計信息。 常規狀況是:在大約有 20% 的數據行發生變化時更新統計信息。可是,查詢優化器始終確保採樣的行數儘可能少。 對於小於 8 MB 的表,則始終進行完整掃描來收集統計信息。

最後執行 SET STATISTICS PROFILE ON,能夠看到更詳細的計劃

隨便拿個典型sql來做示例 相關值爲虛假值 僅供參考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SELECT DISTINCT TOP 1000 a.CustomerID FROM TravelTicket(nolock) a
WHERE  a.TicketChargeDate < GETDATE()
AND a.AvailableAmount > 999
AND a.[Status] <>999
AND a.IsInCome <>999
AND a.IsInCome <>998
AND NOT EXISTS
( SELECT TOP 1 1 FROM TicketCharge(NOLOCK) b
     WHERE   b.CustomerID = a.CustomerID
                 AND b.chargetype = 999
         AND b.IsSuccessful = 999
         AND b.IsDeleted != 999
         AND b.FeeMonth = '999'
 
)

在徹底沒有任何索引的前提下咱們查詢一遍看下各類io信息以及執行計劃

如圖中所示 存在2個彙集索引掃描 先介紹下基礎知識

【Table Scan】:遍歷整個表,查找全部匹配的記錄行。這個操做將會一行一行的檢查,固然,效率也是最差的。
【Index Scan】:根據索引,從表中過濾出來一部分記錄,再查找全部匹配的記錄行,顯然比第一種方式的查找範圍要小,所以比【Table Scan】要快。
【Index Seek】:根據索引,定位(獲取)記錄的存放位置,而後取得記錄,所以,比起前二種方式會更快。
   在有彙集索引的表格上,數據是直接存放在索引的最底層的,因此要掃描整個表格裏的數據,就要把整個彙集索引掃描一遍。在這裏,彙集索引掃描 【Clustered Index Scan】就至關於一個表掃描【Table Scan】。所要用的時間和資源與表掃描沒有什麼差異。並非說這裏有了「Index」這個字樣,就說明執行計劃比表掃描的有多大進步。固然反過來說,如 果看到「Table Scan」的字樣,就說明這個表格上沒有彙集索引。換句話說 上面那段sql存在2個表掃描。

【Clustered Index Seek】:直接根據彙集索引獲取記錄,最快!

因此咱們優化的目標是將掃描(scan)變爲查找(seek)

  先來嘗試TicketCharge表 下面咱們所新添而且討論的索引都是非彙集索引

  江湖上流傳着這麼一篇祕訣,建符合索引根據where查詢的順序來,好吧咱們姑且先嚐試一下

1
2
3
4
5
6
7
8
9
10
11
12
/****** Object:  Index [rgyu_test1]    Script Date : 2015-2-2 17:52:50 ******/
CREATE NONCLUSTERED INDEX [chongzi_test] ON [dbo].[TicketCharge]
(
     [CustomerID] ASC ,
     [ChargeType] ASC ,
     [IsSuccessful] ASC ,
     [IsDeleted] ASC ,
     [FeeMonth] ASC
) WITH (PAD_INDEX = OFF , STATISTICS_NORECOMPUTE = OFF , SORT_IN_TEMPDB = OFF , DROP_EXISTING = OFF , ONLINE = OFF , ALLOW_ROW_LOCKS = ON , ALLOW_PAGE_LOCKS = ON ) ON [ PRIMARY ]
GO
ALTER INDEX [chongzi_test] ON [dbo].[TicketCharge] DISABLE
GO

  再看看新的執行計劃

  沒變!江湖祕訣果真仍是得慎重點用,咱們來分析下索引沒命中的緣由。

第一塊是索引基本信息

列名 描述說明

Name

統計信息對象名稱

Update

上一次更新統計信息的日期和時間

Rows

在目標索引、統計信息或列中的總行數。若是篩選索引或統計信息,此行數可能小於表的行數。
Rows Sampled 用於統計信息計算的抽樣總行數。
Steps 統計信息對象第一個鍵列的直方圖中的值範圍數。每一個步驟包括在直方圖結果中定義的 RANGE_ROWS 和 EQ_ROWS。

Density

查詢優化器不使用此值。顯示此值的惟一目的是爲了向後兼容。密度的計算公式爲 1 / distinct rows,其中 distinct rows 是直方圖輸出中全部步驟的 DISTINCT_RANGE_ROWS 之和。若是對行進行抽樣,distinct rows 則基於抽樣行的直方圖值。
Average Key Length 統計信息對象的鍵列中,全部抽樣值中的每一個值的平均字節數
String Index 若是爲「是」,則統計信息中包含字符串摘要索引,以支持爲 LIKE 條件估算結果集大小。僅當第一個鍵列的數據類型爲charvarcharncharnvarcharvarchar(max)nvarchar(max)textntext 時,纔會對此鍵列建立字符串索引。
Filter Expression 包含在統計信息對象中的錶行子集的表達式。NULL = 未篩選的統計信息。有關詳細信息,請參閱篩選統計信息。
Unfiltered Rows 應用篩選器表達式前表中的總行數。若是 Filter Expression 爲 NULL,Unfiltered Rows 等於行標題值。

 第二塊對指定 DENSITY_VECTOR 時結果集中所返回的列進行了說明。

列名 說明
All Density 針對統計信息對象中的列的每一個前綴計算密度(1/ distinct_rows)。
Average Length 每一個列前綴的列值向量的平均長度(按字節計)。例如,若是列前綴爲列 A 和 B,則長度爲列 A 和列 B 的字節之和。
Columns 爲其顯示 All densityAverage length 的前綴中的列的名稱。

第三塊對指定 HISTOGRAM 選項時結果集中所返回的列進行了說明。

列名 說明
RANGE_HI_KEY 直方圖步驟的上限值。

RANGE_ROWS

表中位於直方圖步驟內(不包括上限)的行的估算數目。
EQ_ROWS 表中值與直方圖步驟的上限值相等的行的估算數目。
DISTINCT_RANGE_ROWS 直方圖步驟內(不包括上限)非重複值的估算數目。
AVG_RANGE_ROWS 直方圖步驟內(不包括上限)重複值的頻率或平均數目(若是 DISTINCT_RANGE_ROWS > 0,則爲 RANGE_ROWS / DISTINCT_RANGE_ROWS)。

越小的SQL Server索引密度意味着具備更高的索引選擇性。當密度趨近於1,索引就變得有更少的選擇性,基本上沒有用處了。當索引的選擇性低的時候,優化器可能會 選擇一個表掃描(table scan),或者葉子級的索引掃描(Index scan),而不會進行索引查找(index seek),由於這樣會付出更多的代價。小心你的數據庫中低選擇性的索引。這樣的索引一般是對系統的性能是一個損害。它們一般不只不會用來進行數據的檢 索,並且也會使得數據修改語句變得緩慢,由於須要額外的索引維護。識別這些索引,考慮刪除掉它們。

而上圖咱們的密度已經達到0.33,由於這不是一個好的方案。咱們調整索引順序,將feemonth提到第一列。

再看執行io和執行計劃

 

搞定,咱們再看下統計分析

到此爲止或許你覺得已經搞定了這個索引問題 ticketcharge的讀取從1w3下降到了13 可是友情提請一下 密度會隨着數據分佈的變化而變化 本次的demo數據具備特殊性 具體的問題還須要具體來分析

咱們看一下更詳細的計劃

另外針對本文這種sql寫法 還有另一個點須要關注 那就是索引第一列不能夠是於第一張表關聯的列

調整咱們的索引順序一樣可讓sql命中索引 咱們來看下效果

 

 執行一下

 

雖然命中了索引 可是因爲關聯鍵的問題 致使ticketcharge進行了循環。

因此說 江湖上流傳的按照where查詢條件設計索引順序是徹底錯誤的 第一列的選擇要根據密度選擇性來判斷

另外非彙集索引列分爲鍵值列和包含列(include) 

複合索引的鍵值列不是越多越好,首先索引自己有長度的限制。可是使用非鍵值列就不算在索引長度內。鍵列存儲在索引的全部級別中,而非鍵列僅存儲在葉 級別中。簡單來講原本索引相似於字典的部首查找,你根據部首查找之後還要根據該漢字對應的頁數去查看詳細,可是若是你只是要一個簡單的該漢字的拼音,那麼 包含列至關於把拼音直接附加在部首查找後面,你就不須要再去詳細頁查看其它你不須要的信息了。

換個角度來講,當查詢中的全部列都做爲鍵列或非鍵列包含在索引中時,帶有包含性非鍵列的索引能夠顯著提升查詢性能。這樣能夠實現性能提高,由於查詢優化器能夠在索引中找到全部列值;不訪問表或彙集索引數據,從而減小磁盤 I/O 操做。

至於travelticket的索引設計也相似,不過須要注意的點是不等於運算 以及like運算 都是不能使用索引的。須要結合業務來調整。


除了上述人爲的添加索引 還有一種取巧的辦法

  打開sql server profiler

  

  按照本身的需求新建一個跟蹤腳本

  

  寫個腳本循環跑這個語句而後保存跟蹤腳本。打開推薦sql優化器

  

  導入剛纔的跟蹤腳本,而且選擇索引優化

  

  執行分析

  

  


數據處理優化

  對於db優化在程序員能力已經到瓶頸的前提下,能夠着手從應用程序上的細節出發,例如並行。

  所謂並行也就是多線程針對同任務分區分塊協同處理。多線程的技術你們都很瞭解,這裏突出如下線程同步的問題。例如我有1000我的我分10組任務執行,如何正確的保證當前10組任務正確的完成互相不衝突而且等到全部任務完成後纔開啓下一輪1000.

  這裏介紹一個比較通用的方法,首先申明一個信號量隊列List<ManualResetEvent>();

  取出1000條後對於1000條進行添加順序標識表示而且模餘分組。標識從1開始遞增就能夠,模餘分組方法以下

  testInfos.GroupBy(i => i.index % workTaskCount).Select(g => g.ToList()).ToList();    

  其中index爲剛纔添加的順序標識, workTaskCount爲分組的任務數。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//循環處理批次任務
foreach (var testGroup in testGroups)
{
     var mre = new ManualResetEvent( false );
     manualEvents. Add (mre);
     var testMethodParam = new TestMethodParam
     {
         mrEvent = mre,
         testGroup = TestGroup,
         testParam = testParam
     };
     //線程池處理計劃任務
     ThreadPool.QueueUserWorkItem(DoTestMethod, testMethodParam);
}
if (manualEvents. Count != 0)
{
     //等待全部信號量完成 切記這裏最大值爲64
     WaitHandle.WaitAll(manualEvents.ToArray(), 30 * 60 * 1000);
}

  DoTestMethod就是舊的任務處理邏輯,testMethodParam負責涵蓋你舊邏輯中所須要的參數而且包含一個完成信號量。

  這裏須要牢記的是WaitHandle等待的信號量最大值爲64.

  若是你須要的任務分組數超過64那麼這裏推薦在DoTestMethod方法中不適用信號量,而是使用原子操做的標識,例如 Interlocked.Increment(taskCount)。當taskCount累加到1000(你設計的當前批次值)就結束一輪。不過比起 WaitHandle性能上要慢一些。

  另外若是你使用線程池來管理線程,最好加上最大線程限制。過多的線程是致使cpu資源消耗的緣由之一,最好的線程數是服務器cpu個數的2倍-1或者-2,


死鎖問題

  鎖超時的問題大可能是由於表鎖產生。解決表鎖的問題說難也不難,不過須要犧牲性能。mssql針對主鍵的更新不會產生表鎖而是產生行鎖。針對這個問題那麼死鎖的問題初步解決起來就簡單了。

  這是比較通用的處理方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
DECLARE @step INT
DECLARE @id CHAR (12)
create table #tmpTest  --建立臨時表
(
     rec_index INT ,
     id CHAR (12)
);
INSERT INTO #tmpTest  (rec_index,ID)
SELECT ROW_NUMBER() OVER( ORDER BY ID) AS rec_index,ID FROM TestTable(nolock) WHERE BID = @bID
SET @rowcount=0
SET @step=1
SELECT @rowcount= COUNT (*) FROM #tmpTest AS tt
WHILE(@step<=@rowcount)
BEGIN
     SELECT @id=Id
     FROM #tmpTest  AS tt
     WHERE @step=rec_index
     UPDATE TestTable
     SET
         UpdateUser = @UpdateUser,
         UpdateTime = @UpdateTime,
     WHERE  ID =@id
     SET @step=@step+1
END

cpu飆高小結

對於cpu飆高分2類,應用服務器和db服務器的飆高緣由優先排查點不一樣

對於應用服務器,首先排查while true等死循環,其次看多線程問題。不排除其餘緣由,這裏只介紹主要的狀況

在本機就能夠根據源代碼來調試 

 

如圖所示咱們的cpu問題是因爲線程過得多致使 由於個人demo用的Threadpool因此一個簡單的ThreadPool.SetMaxThreads(5, 5);便可搞定.

若是上述狀況檢查不出你的程序致使cpu飆高的緣由,那麼就須要藉助於其餘工具 例如dotTrace.

 

根據本身的程序類型加載不一樣的探查器

截圖不具備典型性 操做很簡單 沒有什麼過多值得介紹的 官方文檔

http://www.jetbrains.com/profiler/features/index.html

咱們隨便找個程序 


 

下面咱們看看db服務器cpu飆高的一些緣由。

 最大的一個坑是隱式數據類型轉換,之因此爲坑是由於他不是顯示的出現問題,而是當你的數據分佈以及量達到必定條件後纔會產生問題。

什麼是隱式數據類型轉換:

當咱們在語句的where 條件等式的左右提供了不一樣數據類型的列或者變量,SQL Server在處理等式以前,將其中一端的數據轉換成跟另外一端數值的數據類型一致,這個過程叫作隱式數據類型轉換。

好比 char(50)=varchar(50), char(50)=nchar(50), int=float, int=char(20) 這些where 條件的等式都會觸發隱式數據類型轉換。

可是,對於某些數據類型轉換過程當中,能夠轉換的方向只是單向的。例如:

若是你試圖比較INT和FLOAT的列,INT數據類型必須被轉換成FLOAT型 "CONVERT(FLOAT,C_INT) = C_FLOAT".

若是你試圖比較char和nchar的列,char數據類型必須被轉換成unicode型 "CONVERT(nchar,C_char) = C_nchar"

所以,咱們在.net 或者java的程序中,會常常出現因爲隱式數據類型轉換而產生的性能問題。

最簡單的 咱們來作個試驗

1
2
3
4
5
6
7
8
9
CREATE TABLE [Chongzi_Test] (
[TAB_KEY] [ varchar ] (5)  NOT NULL ,
[Data] [ varchar ] (10)  NOT NULL ,
CONSTRAINT [Chongzi_Test_PK] PRIMARY KEY  CLUSTERED
(
[TAB_KEY]
ON [ PRIMARY ]
) ON [ PRIMARY ]
GO

而後插入幾百條數據

咱們執行

1
2
3
4
declare @p1 int
set @p1=0
exec sp_prepexec @p1 output ,N '@P0 varchar(5)' ,N 'select TAB_KEY,Data from Chongzi_Test where TAB_KEY = @P0' ,N '0'
select @p1

而後咱們換一種不匹配類型來看一下

1
2
3
4
declare @p1 int
set @p1=0
exec sp_prepexec @p1 output ,N '@P0 nvarchar(4000)' ,N 'select TAB_KEY,Data from Chongzi_Test where TAB_KEY = @P0' ,N '0'
select @p1

這裏出現了一個操做叫作GetRangeThroughConvert(),在這裏,SQL Server因爲不能直接對varchar(5)的列用nvarchar(4000)的值進行seek,所以,SQL Server必須將nvarchar轉換成varchar。

這個過程當中不一樣的應用場景能夠帶來性能的損耗可能會很大,由於在轉換過程當中可能會存在表掃描。

例如以前項目中有個nvarchar(max)向varchar(50)轉換,因爲包含有特別的字符,如全雙工字符<, 該字符直接轉換成varchar(50)不會那麼順利。須要根據參數的實際長度和表結構中定義的字段長度進行比較,若是小於表結構中定義的字段長 度,Range依舊會比較小,可是若是大於表結構中定義的字段長度,GetRangeMismatchedTypes函數會把Range設得很寬,查詢很 慢。

如圖執行計劃是同樣的,返回數據行不一樣。

 附上c# dbtype 與sql 類型的對應表

AnsiString:VarChar
Binary:VarBinary
Byte:TinyInt
Boolean:Bit
Currency:Money
Date:DateTime
DateTime:DateTime
Decimal:Decimal
Double:Float
Guid:UniqueIdentifier
Int16:SmallInt
Int32:Int
Int64:BigInt
Object:Variant
Single:Real
String:NVarChar
Time:DateTime
AnsiStringFixedLength:Char
StringFixedLength:NChar
Xml:Xml
DateTime2:DateTime2
DateTimeOffset:DateTimeOffset


Crash現象分析

目前仍是用老辦法 抓dump分析堆棧

使用過程能夠參考我以前的博文 http://www.cnblogs.com/dubing/p/3878591.html

面向.Net程序員的後端性能優化實戰

相關文章
相關標籤/搜索