從技巧、案例和工具入手,詳解性能優化怎么做
作者介紹
顏圣杰, .NET平臺軟件工程師,對DDD領域驅動設計感興趣,目前在研究ABP框架,熱愛寫作與分享。
最近一段時間系統新版本要發布,在beta客戶測試期間,暴露了很多問題,除了一些業務和異常問題外,其它都集中在性能上。有幸接觸到這些性能調優的機會,這里跟大家歸納交流一下。
性能優化是一個老生常談的問題了,典型的性能問題如頁面響應慢、接口超時,服務器負載高、并發數低,數據庫頻繁死鎖等。而造成性能問題又有很多種,比如磁盤I/O、內存、網絡、算法、大數據量等。我們可以大致把性能問題分為四個層次: 代碼層次、數據庫層次、算法層次、架構層次。
下面我會結合實際性能優化案例,和大家分享下性能調優的工具、方法和技巧。
先說心態
說到性能問題,你可能首先就想到的是麻煩或者頭大,因為一般性能問題都比較緊急,輕則影響客戶體驗,重則宕機導致財務損失,而且性能問題比較隱蔽,不易發現。因此一時間無從下手,而這時我們就很容易從心底開始去排斥它,不愿接這燙手的山芋。
恰巧,性能調優也是體現程序員水平的一個重要指標。
因為處理BUG、崩潰、調優、入侵等突發事件比編程本身更能體現平庸程序員與理想程序員的差距。當面對一個未知的問題時,如何定位復雜條件下的核心問題、如何抽絲剝繭地分析問題的潛在原因、如何排除干擾還原一個最小的可驗證場景、如何抓住關鍵數據驗證自己的猜測與實驗,都是體現程序員思考力的最好場景。是的,在衡量理想程序員的標準上,思考力比經驗更加重要。
所以,若你不甘平庸,請擁抱性能調優的每一個機會。當你擁有一個正確的心態,你所面對的性能問題就已經解決了一半。
再說技巧
拿到一個性能問題,不要忙著先上工具, 先了解問題出現的背景,問題的嚴重程度 。然后大致根據自己的經驗積累作出預估。比如客戶來了個性能問題說系統宕機了,已經造成資金損失了。這種涉及到錢的問題,大家都比較敏感,根據自己的Level,決定是否要接這個鍋。這不是逃避,而是自知之明。
了解問題背景后, 下一步就來嘗試問題重現 。如果在測試環境能夠重現,那這種問題會很好跟蹤分析。如果問題不能穩定重現或僅能在生產環境重現,那就相對比較棘手,這時要立刻收集現場證據,包括但不限于抓dump、收集應用程序以及系統日志、關注CPU內存情況、數據庫備份等,之后不妨再嘗試重現,比如恢復客戶數據庫到測試環境重現。
不管問題能否重現, 再 下一步,我們要大致對問題進行分類 ,是代碼層次的業務邏輯問題還是數據庫層次的操作耗時問題,又或是系統架構的吞吐量問題。那如何確定呢?而我傾向于先從數據庫動手。我的習慣做法是,使用數據庫監控工具,先跟蹤下SQL耗時情況。如果監控到耗時較長的SQL語句,那基本上就是數據庫層次的問題,否則就是代碼層次。若為代碼層次,再研究完代碼后,再細化為算法或架構層次問題。
確定問題種類后,是時候上工具來精準定位問題點了:
-
SQL耗時問題,推薦使用免費的Plan Explorer分析執行計劃。
-
代碼問題定位,優先推薦使用VS自帶的Performance Analysis,其次是RedGate的性能分析套件.NET Developer Bundle;然后還有Jet Brains的dotTrace -- .NET performance profiler,dotMemory-- .NET memory profiler;再然后就是反人類的Windbg等等。
精準定位問題點后,就是著手優化了。相信到這一步,就是優化策略的選擇了,這里就不展開了。
優化后,最后當然要進行測試了,畢竟優化了多少,我們也要做到心里有譜才行。
啰啰嗦嗦有點多,下面直接上案例。
案例分享
這里分享下我針對代碼層面、數據庫層面和算法層面的優化案例。
1. SQL 優化案例
案例1: 客戶反饋某結算報表統計十天內的數據耗時10mins左右。
由于前幾天剛學會用RedGate的分析工具,拿到這個問題,本地嘗試重現后,就直接想使用工具分析。然而,這工具在使用webdev模式起站點時,總是報錯,而當時時一根筋,老是想解決這個工具的報錯問題。結果,白白搞了半天也沒搞定。最后不得已放棄工具,轉而選擇使用SQL Server Profiler去監控SQL語句耗時。一跟蹤不要緊,問題就直接暴露了,整個全屏的重復SQL語句,如下圖:
SQL Profiler監控結果
這下問題就很明顯了,八成是代碼在循環拼接SQL執行語句。根據抓取到SQL關鍵字往代碼中去搜索,果然如此。
看到這段代碼,咱先不評判這段代碼的優劣,因為畢竟代碼注釋清晰,省了我們理清業務的功夫。這段SQL主要是想做去重處理,很顯然選用了錯誤的方案。改后代碼如下:
改后測試相同數據量,耗時由10mins降到10s左右。
2. 代碼優化案例
案例2: 客戶反饋銷售訂單100條分錄行,保存進行可發量校驗時,耗時7mins左右。
拿到這個問題后,本地重現后,監控SQL耗時沒有異常,那就著重分析代碼了。因為可發量校驗的業務邏輯極其復雜,加上又直接再一個類文件實現該功能,3500+行的代碼,加上零星注釋,真是讓人避之不及。逃避不是辦法,還是上工具分析一把。
這次我選用的時VS自帶的Performance Profiler,開發環境下極其強大的性能調優工具。針對我們當前案例,我們僅需要跟蹤指定服務對應的DLL即可,使用步驟如下:
-
Analyze-->Profiler-->New Performance Session
-
打開Performance Explorer
-
找到新添加的Performance Session,右鍵Targets,然后選擇Add Target Binary,添加要跟蹤的dll文件即可
-
將應用跑起來
-
選中Performance Session,右鍵Attach對應進程即可跟蹤分析性能了
-
在跟蹤過程中,可隨時暫停跟蹤和停止跟蹤
圖示步驟
跟蹤結束后本案例跟蹤到的采樣結果如下圖:
VS Performance Profiler分析報告
同時Performance Profiler也給出了問題的建議,如下圖:
VS Performance Profiler分析提示
其中第1、4條大致說明程序I/O消耗大,第一代的GC上存在未及時釋放的垃圾占比過高。而根據上圖的采樣結果,我們可以直接看出是由于再代碼中頻繁操作DataTable引起的性能瓶頸。走讀代碼發現的確如此,所有的數量統計都是在代碼中循環遍歷DataTable進行處理的。而最終的優化策略,就相當于一次大的重構,將所有代碼中通過遍歷DataTable的計算邏輯全部挪到SQL中去做。由于代碼過多,就不再放出。
案例3: 客戶反饋批量引入1000張訂單,耗時40mins左右,且容易中斷。
同樣,我們還是先嘗試本地重現。經測試批量引入101張單據,就耗時5mins左右。下一步打開SQL監控工具也未發現耗時語句。但考慮到是批量導入操作,雖然單個耗時不多,但乘以100這個基數,就明顯了。下面我們就使用RedGate的Ants Performance Profiler跟蹤一下。
該工具比較直觀,可以同時監控代碼和SQL執行情況。第一步,New Profiler Session,第二步進行設置,如下圖。根據自己的應用程序類別,選擇相應的跟蹤方式。
跟蹤設置
針對這個問題,我們跟蹤到的調用堆棧和SQL耗時結果如下圖:
調用堆棧監控結果
SQL監控結果
首先從調用堆棧中的Hit Count,我們可以首先看出它是一個批量過程,因為入口函數僅調用一次;第二個我們可以代碼中是循環處理每一個單據,因為Hit Count與我們批量引入的單據數量相符;第三個,突然來了個10201,如果有一定的數字敏感性的話,這次性能問題的原因就被你找到了。這里就不賣關子了,101 x 101 = 10201。
是不是明白了什么,存在循環嵌套循環的情況。我們走讀代碼確定一下:
好吧,外層套了一個空循環卻什么也沒做。修改就很簡單了,刪除無效外層循環即可。
3. 算法優化案例
案例4: 某全流程跟蹤報表超時。
這個報表是用來跟蹤所有單據從下單到出庫的業務流程數據流轉情況。而所有的流程數據都是按照樹形結果存儲在數據庫表中的,類似這樣:
圖中的流程為: 銷售合同-->銷售訂單-->發貨通知單-->銷售出庫單
為了構造流程圖,之前的處理方法是把流程數據取回來,通過代碼構造流程圖。這也就是性能差的原因。
而針對這種情況,就是考驗我們平時經驗積累了。對于樹形結構的表,我們也是可以通過SQL來進行直接查詢的,這就要用到了SQL Server的CTE語法來進行遞歸查詢。
仔細觀察上面的表結構,會發現其樹形結構的特點:
-
FFIRSTNODE:標記是否為根節點
-
FSTABLENAME:標記來源單據名稱
-
FSID:標記來源單據分錄ID
-
FTTABLENAME:標記目標單據名稱
-
FTID:標記目標單據分錄ID
首先想到的辦法就是把流程數據取回來,然后代碼構造流程圖。
第一個思路: 根據根節點循環往下找,吭呲半天,發現沒那么簡單。因為任何一個源頭單據都可以多次下推目標單據。
第二個思路: 先找到終極節點,在從終極節點往上找只至根節點為0。
這個思路實現起來也沒有那么復雜,邏輯理清,循環遍歷,最終也能實現結果。(但在大數據量情況下,易導致性能瓶頸。)
這一次我們換一個思路,讓SQL來替我們做這一復雜的遞歸查詢。
SQL Server 遞歸查詢
基本概念
公用表表達式(CTE) 可以認為是在單個 SELECT、INSERT、UPDATE、DELETE 或CREATE VIEW 語句的執行范圍內定義的臨時結果集。公用表表達式可以包括對自身的引用,這種表達式稱為遞歸公用表表達式。
-
創建遞歸查詢。
-
在不需要常規使用視圖時替換視圖,也就是說,不必將定義存儲在元數據中。
-
啟用按從標量嵌套 select 語句派生的列進行分組,或者按不確定性函數或有外部訪問的函數進行分組。
-
在同一語句中多次引用生成的表。
MSDN上對CTE的介紹
-
https://docs.microsoft.com/zh-cn/sql/t-sql/queries/with-common-table-expression-transact-sql
T-SQL查詢進階--詳解公用表表達式(CTE)
-
http://www.cnblogs.com/CareySon/archive/2011/12/12/2284740.htm l
CTE 的基本語法結構如下:
即三個部分:
-
公用表表達式的名字(在WITH關鍵字之后)
-
查詢的列名(可選)
-
緊跟AS之后的SELECT語句(如果AS之后有多個對公用表的查詢,則只有第一個查詢有效)
動手實踐
根據官網示例我們很簡單就可以寫出CTE語句應用于我們的應用場景:
在查詢中我們指定條件參數WHERETBIE.FTTABLENAME = 'T_SAL_ORDERENTRY' AND TBIE.FTID = 121625,即可查詢到指定節點的完整流程數據。
其中在與公用表TEST_CTE進行關聯時,我指定了兩個條件CTBIE.FSID=CTE.FTIDAND CTBIE.FSTABLENAME = CTE.FTTABLENAME,因為不同類型的單據各有一套自增的ID,直接用ID進行關聯迭代不可行。
需要注意的是OPTION(MAXRECURSION10)是用來限制遞歸次數,以避免無限遞歸導致數據庫性能消耗嚴重。
擴展:構造遞歸路徑
基于上一個查詢,增加一列手動拼接遞歸路徑。注意SQL中將PATH設置的類型為navarchar(4000),在union中,兩邊的表結構類型必須保持一致,否則會報錯定位點類型和遞歸部分的類型不匹配。 可參考此篇博文
《解決CTE定位點類型和遞歸部分的類型不匹配》。 (http://www.cnblogs.com/ccding13/p/3515393.html)
遞歸路徑查詢結果
Oracle 遞歸查詢
基本概念
Oracle中的遞歸查詢語句為start with…connect by prior,為中序遍歷算法。
可參考《Oracle 樹操作、遞歸查詢(select…start with…connect by…prior)》了解更多。 (鏈接http://www.cnblogs.com/yingsong/p/5035907.html)
其基本語法是:
selectcolname from tablename
start with條件1
connect by條件2
where 條件3
-
條件1: 是根結點的限定語句,當然可以放寬限定條件,以遍歷多個根結點,實際就是多棵樹。
-
條件2:是連接條件,其中用PRIOR表示上一條記錄。
比如CONNECT BY PRIOR Id = Parent_Id就是說上一條記錄的Id 是本條記錄的Parent_Id。 -
條件3:過濾返回的結果集。
PRIOR 關鍵字
運算符PRIOR被放置于等號前后的位置,決定著查詢時的檢索順序。
-
PRIOR被置于CONNECT BY子句中等號的前面時,則強制從根節點到葉節點的順序檢索,為自頂向下查找。
如:CONNECT BY PRIOR Id=Parent_Id -
PIROR運算符被置于CONNECT BY 子句中等號的后面時,則強制從葉節點到根節點的順序檢索,為自底向上的查找。
如:CONNECT BY Id=PRIOR Parent_Id
PS:當CONNECT BY后指定多個連接條件時,每個條件都應指定PRIOR關鍵字。
動手實踐
理清了用法,我們用Oracle來對查詢一下業務流程。
查詢結果
該流程為: 銷售訂單-->發貨通知單-->銷售出庫單-->退貨通知單-->銷售退貨單
其中在指定連接條件時,我指定了兩個條件FSID= PRIOR FTID AND FSTABLENAME=PRIOR FTTABLENAME,因為不同類型的單據各有一套自增的ID,直接用ID進行關聯迭代不可行。
擴展:構造遞歸路徑
Oracle中提供了SYS_CONNECT_BY_PATH函數用來進行連接路徑。
基于上個查詢,增加了一列SUBSTR(SYS_CONNECT_BY_PATH(FTID,'->'),3)NAME_PATH用來拼接遞歸路徑。
遞歸路徑查詢結果
顯示當前節點的根節點
這個時候我們要用到connect_by_root函數,用來記錄當前節點的根節點信息。
當前節點的根節點的查詢結果
Oracle 中的with...as語句
Oracle也有with..as 查詢語法,一般用來進行子查詢,提高查詢效率。
語法:
with tempTableName as ( select * from table1 )
select *from tempTableName
拿我們的案例舉例就是:
為啥要講這個呢,我們可以在Oracle遞歸查詢后進行篩選啊。
總結
性能調優是一個循序漸進的過程,不可能一蹴而就,重在平時的點滴積累。關于工具的選擇和使用,本文并未展開,也希望讀者也不要糾結與此。當你真正想解決一個問題的時候,相信工具的使用是難不住你的。
最后就大致總結下我的調優思路:
-
調整心態,積極應對
-
了解性能背景, 收集證據, 嘗試重現
-
問題分類,先監控SQL耗時,大致確定是SQL或是代碼層次原因
-
使用性能分析工具,確定問題點
-
調優測試
來自:http://mp.weixin.qq.com/s/jTamavW9VjUDOH6CJIfBFg