性能優化知多少

1. 引言

最近一段時間,系統新版本要發佈,在beta客戶測試期間,暴露了不少問題,除了一些業務和異常問題外,其餘都集中在性能上。有幸接觸到這些性能調優的機會,固然要學習總結了。程序員

性能優化是一個老生常談的問題了,典型的性能問題如頁面響應慢、接口超時,服務器負載高、併發數低,數據庫頻繁死鎖等。而形成性能問題又有不少種,好比磁盤I/O、內存、網絡、算法、大數據量等等。咱們能夠大體把性能問題分爲四個層次:代碼層次、數據庫層次、算法層次、架構層次。
因此下面我會結合實際性能優化案例,和你們分享下性能調優的工具、方法和技巧。web

2. 先說心態

說到性能問題,你可能首先就想到的是麻煩或者頭大,由於通常性能問題都比較緊急,輕則影響客戶體驗,重則宕機致使財務損失,並且性能問題比較隱蔽,不易發現。所以一時間無從下手,而這時咱們就很容易從心底開始去排斥它,不肯接這燙手的山芋。算法

而恰巧,性能調優是體現程序員水平的一個重要指標。sql

由於處理bug、崩潰、調優、入侵等突發事件比編程自己更能體現平庸程序員與理想程序員的差距。當面對一個未知的問題時,如何定位複雜條件下的核心問題、如何抽絲剝繭地分析問題的潛在緣由、如何排除干擾還原一個最小的可驗證場景、如何抓住關鍵數據驗證本身的猜想與實驗,都是體現程序員思考力的最好場景。是的,在衡量理想程序員的標準上,思考力比經驗更加劇要。數據庫

因此,若你不甘平庸,請擁抱性能調優的每個機會。當你擁有一個正確的心態,你所面對的性能問題就已經解決了一半。編程

3. 再說技巧

拿到一個性能問題,不要忙着先上工具,先了解問題出現的背景,問題的嚴重程度。而後大體根據本身的經驗積累做出預估。好比客戶來了個性能問題說系統宕機了,已經形成資金損失了。這種涉及到錢的問題,你們都比較敏感,根據本身的level,決定是否要接這個鍋。這不是逃避,而是自知之明。性能優化

瞭解問題背景以後,下一步就來嘗試問題重現。若是在測試環境可以重現,那這種問題就很好跟蹤分析。若是問題不能穩定重現或僅能在生產環境重現,那問題就相對比較棘手,這時要馬上收集現場證據,包括但不限於抓dump、收集應用程序以及系統日誌、關注CPU內存狀況、數據庫備份等等,以後不妨再嘗試重現,好比恢復客戶數據庫到測試環境重現。服務器

無論問題可否重現,下一步,咱們就要大體對問題進行分類,是代碼層次的業務邏輯問題仍是數據庫層次的操做耗時問題,又或是系統架構的吞吐量問題。那如何肯定呢?而我傾向於先從數據庫動手。個人習慣作法是,使用數據庫監控工具,先跟蹤下Sql耗時狀況。若是監控到耗時較長的SQL語句,那基本上就是數據庫層次的問題,不然就是代碼層次。若爲代碼層次,再研究完代碼後,再細化爲算法或架構層次問題。網絡

肯定問題種類後,是時候上工具來精準定位問題點了:架構

精準定位問題點後,就是着手優化了。相信到這一步,就是優化策略的選擇了,這裏就不展開了。

優化後,最後固然要進行測試了,畢竟優化了多少,咱們也要作到內心有譜才行。

以上囉囉嗦嗦有點多,下面咱們直接上案例。

4. 案例分享

下面就分享下我針對代碼層面、數據庫層面和算法層面的優化案例。

4.1. SQL優化案例

案例1:客戶反饋某結算報表統計十天內的數據耗時10mins左右。

因爲前幾天剛學會用RedGate的分析工具,拿到這個問題,本地嘗試重現後,就直接想使用工具分析。然而,這工具在使用webdev模式起站點時,老是報錯,而當時時一根筋,總是想解決這個工具的報錯問題。結果,白白搞了半天也沒搞定。最後不得已放棄工具,轉而選擇使用sql server profiler去監控sql語句耗時。一跟蹤沒關係,問題就直接暴露了,整個全屏的重複sql語句,以下圖。

Sql Profiler監控結果

這下問題就很明顯了,八成是代碼在循環拼接sql執行語句。根據抓取到sql關鍵字往代碼中去搜索,果真如此。

#region更新三張表數據結合的中間臨時表數據,有上游單據的直接調撥單分屢次下推時,只計算一次的調撥數量和價稅合計
string sSql = string.Format(@
"SELECT FENTRYID FROM {0} GROUP BY FENTRYID HAVING COUNT(FENTRYID) > 1", sJoinDataTempTable);
using(IDataReader reader = DBUtils.ExecuteReader(this.Context, sSql)) {
    while (reader.Read()) {
        sbSql.AppendFormat(@"
UPDATE {0} SET FDIRECTQTY = 0,FALLAMOUNT = 0 
WHERE FSEQ NOT IN (
SELECT TOP 1 FSEQ FROM {0} WHERE FENTRYID = {1}) AND FENTRYID = ({1});"
, sJoinDataTempTable, Convert.ToInt32(reader["FENTRYID"]));
        listSqlObj.Add(new SqlObject(sbSql.ToString(), new List < SqlParam > ()));
        sbSql.Clear();
    }
}
#endregion

看到這段代碼,咱先不評判這段代碼的優劣,由於畢竟代碼註釋清晰,省了咱們理清業務的功夫。這段sql主要是想作去重處理,很顯然選用了錯誤的方案。改後代碼以下:

string sqlMerge = string.Format(@"
merge into {0} t1
using(
select min(Fseq) fseq,Fentryid from {0} t2 group by fentryid
) t3 on (t1.fentryid = t3.fentryid and t1.fseq <> t3.fseq)
when matched then
update set t1.FDIRECTQTY = 0, t1.FALLAMOUNT = 0
", sJoinDataTempTable);

listSqlObj.Add(new SqlObject(sqlMerge, new List < SqlParam > ()));
sbSql.Clear();

改後測試相同數據量,耗時由10mins降到10s左右。

4.2. 代碼優化案例

案例2:客戶反饋銷售訂單100條分錄行,保存進行可髮量校驗時,耗時7mins左右。

拿到這個問題後,本地重現後,監控sql耗時沒有異常,那就着重分析代碼了。由於可髮量校驗的業務邏輯極其複雜,又加上又直接再一個類文件實現該功能,3500+行的代碼,加上零星註釋,真是讓人避之不及。逃避不是辦法,仍是上工具分析一把。
此次我選用的時VS自帶的Performance Profiler,開發環境下極其強大的性能調優工具。針對咱們當前案例,咱們僅須要跟蹤指定服務對應的dll便可,使用步驟以下:

  1. Analyze-->Profiler-->New Performance Session
  2. 打開Performance Explorer
  3. 找到新添加的Performance Session,右鍵Targets,而後選擇Add Target Binary,添加要跟蹤的dll文件便可
  4. 將應用跑起來
  5. 選中Performance Session,右鍵Attach對應進程便可跟蹤分析性能了
  6. 在跟蹤過程當中,可隨時暫停跟蹤和中止跟蹤

圖示步驟

跟蹤結束後本案例跟蹤到的採樣結果以下圖:

VS Performance Profiler分析報告

同時Performance Profiler也給出了問題的建議,以下圖:
VS Performance Profiler分析提示

其中第一、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。
是否是明白了什麼,存在循環嵌套循環的狀況。咱們走讀代碼肯定一下:

//Save.cs
public override void EndOperationTransaction(EndOperationTransactionArgs e) {
    //省略其餘代碼
    foreach(DynamicObject dyItem in e.DataEntitys) {
        //反寫收款單
        WriteBackReceiveBill wb = new WriteBackReceiveBill();
        wb.WriteBackForSave(e, this.Context);
    }
}

//WriteBackReceiveBill .cs
public void WriteBackForSave(EndOperationTransactionArgs e, Context contx) {
    //省略其餘代碼:
    foreach(DynamicObject item in e.DataEntitys) {
        //do something 
    }
}

好嘛,外層套了一個空循環卻什麼也沒作。修改就很簡單了,刪除無效外層循環便可。

4.3. 算法優化案例

案例4:某全流程跟蹤報表超時。

這個報表是用來跟蹤全部單據從下單到出庫的業務流程數據流轉狀況。而全部的流程數據都是按照樹形結果存儲在數據庫表中的,相似這樣:

流程樹表

圖中的流程爲:
銷售合同-->銷售訂單-->發貨通知單-->銷售出庫單

爲了構造流程圖,以前的處理方法是把流程數據取回來,經過代碼構造流程圖。這也就是性能差的緣由。

而針對這種狀況,就是考驗咱們平時經驗積累了。對於樹形結構的表,咱們也是能夠經過SQL來進行直接查詢的,這就要用到了SQL Server的CTE語法來進行遞歸查詢。關於遞歸查詢,可參考我這篇文章:SQL遞歸查詢知多少。這裏就不展開了。

5.總結

性能調優是一個按部就班的過程,不可能一蹴而就,重在平時的點滴積累。關於工具的選擇和使用,本文並未展開,也但願讀者也不要糾結與此。當你真正想解決一個問題的時候,相信工具的使用是難不住你的。

最後就大體總結下個人調優思路:

  1. 調整心態,積極應對
  2. 瞭解性能背景, 收集證據, 嘗試重現
  3. 問題分類,先監控SQL耗時,大體肯定是SQL或是代碼層次緣由
  4. 使用性能分析工具,肯定問題點
  5. 調優測試
相關文章
相關標籤/搜索