當我們談高性能時,我們談些什麼?

網站越快,用戶的黏性就越高;

網站越快,用戶忠誠度更高;

網站越快,用戶轉化率越高。

簡言之,速度是關鍵。

——《Web 性能權威指南》

顯然,高性能意味着“快”。但對快的定義,在不同的系統中,標準是不一樣的。爲了獲得快的體驗,通常我們需要平衡成本和收益等方面制定優化方法。

如果說“快”的標準不好把握的話,但我們都對類似這樣的典型論述有一致的結論,比如:

內存是快的

U 盤是慢的

之所以對這些問題容易有統一的認知,是因爲我們已經對需要度量的範圍與標準有了較清晰的概念,我們不自覺地預設一些應該受到控制的變量,並將它們與其他某些事物做了對比。比如,在談 U 盤慢時,往往指的是在同一臺計算機上與硬盤、內存等其他存儲方法作比較。

類似地,當我們談論應用的性能時,也需要把應用所在的環境、處理的數據量等情況一同納入考慮範圍內。這些因素常常讓速度的定義變得複雜。因此,在談論高性能之前,我們首先需要對性能進行建模,確定合適的用於反映性能狀況的指標,並據此制定明確在各項因素在各種情況下的性能目標。比如,在談論 Web 應用程序的速度時,我們經常談到 QPS、TPS 和響應時間等指標,而確定性能目標時,則往往要考慮數據量的多少、併發用戶數目和網絡帶寬等情況。

在性能建模時,越細緻、越全面的數據就越真實地反映出系統的狀況,從而能爲做出正確的決策提供更準確的依據。一般來說,網絡、磁盤等 IO 設備、操作系統,以及應用程序運行時(如 CLR、JVM)往往都提供了豐富的性能數據。對於 Web 和雲原生系統而言,設計良好的服務端框架(如 Kubernetes),以及對開發人員友好的瀏覽器(如 Chrome)等也都會提供相關的性能接口。

舉例來說,如果考慮一個桌面應用程序,我們需要收集的指標有:

第一個界面出現的時間

第一個界面加載完畢的時間

某些典型事務處理時間

關閉窗口時的等待時間

從開始加載文件到能夠編輯文件的時間

在制定性能目標時,我們要選用一些典型的用戶機型進行模擬。在收集測試數據時,擺在面前的第一個問題是如何準確獲得這麼多種類時間?靠人肉掐秒錶也是一種思路,不過還是顯得太原始了一些。這時,我們需要在應用中添加一些性能埋點,並利用統一的工具收集這些數據(如操作系統提供的性能計數器)。

下面是利用 Windows 性能工具來查看 IO 操作在物理磁盤上執行的情況:

這些性能數據不但能夠在制定科學的性能目標時提供幫助,更能夠在系統的性能未達預期時,在第一時間展示出系統瓶頸所在。一段時間以來,當網站慢時,我總是能夠通過 Chrome 開發人員工具提供的網絡時間線和性能分析工具快速地發現問題所在,並且運用《高性能網站建設指南》等經典書籍中給出的切實的建議針對性地做出優化。

下面是一個 Chrome 瀏覽器中加載資源時的資源瀑布圖:

有人說,所發現了問題時,就是解決了一半,對於一些容易解決的問題尤其如此。不過,一般來說,性能問題往往沒那麼容易解決——除非是程序的實現有明顯的瑕疵(比如陷入了死循環)。相比於邏輯錯誤問題,解決性能問題一般需要具有一定經驗的工程師才能勝任。

在優化應用程序的性能時,一般會由根據距離應用的遠近,從外部到內部將優化分爲這三個層次:

鏈路上的優化

結構性優化

應用內優化

鏈路上的優化指的是在用戶操作發出之後、真正到達應用程序邏輯執行之前的過程期間執行的優化;結構性優化指的是對應用程序所處的環境、依賴的資源,以及應用本身的子系統間的關係等方面的優化;而應用內優化則指的是對應用內的實現方式(比如算法和數據結構)進行優化。

舉例來說,下圖是一個簡單的 Web 系統的結構,用戶的操作指令發出之後,需要經過互聯網到達負載均衡服務器,最後到達兩臺應用服務器中執行運算邏輯,而應用依賴了讀寫分離的兩個數據庫。

在上述三種層次上,對系統的性能進行優化時,可以考慮以下措施:

(鏈路上)使用 DNS prefetch 技術減少域名查找時間

(鏈路上)增加負載均衡服務器的出口帶寬

(結構性)增加一個新的應用服務器

(結構性)在應用服務器增加 CPU 核心數和內存

(應用內)用多線程技術把操作數據庫和訪問磁盤的操作同步進行

(應用內)消除應用加載數據時的 n+1 問題

一般來說,鏈路上和結構性方面的優化,對於使用各類編程平臺實現的應用程序都一樣適用。對於一個 Web 系統來說,HTTP 請求到達 Web 應用之前的所有優化,都屬於這兩類。時下,關於大流量、大併發系統的性能優化方法,人們已經討論的很多了。不過,縱觀那些優化方法,實際上也都可以從應用內的優化方法上看到類似的方法,可以認爲這些優化思想大體是相通的。

從應用內部優化時,業務專家可以通過一些業務流程上的簡化等方法來有效地改善應用程序的性能,例如,在某些情形下省略一些不必要的運算等。這類優化主要取決於業務規則的自身特點,難以統一地歸納出具體的經驗。而在業務之外的優化方法,各種應用程序之間卻是相通的。

通常,在應用內,要在業務流程之外進行性能優化,我們考慮以下方法:

併發與並行 也就是讓程序同時做多件事,以及將一件事分爲多個小塊同時完成。 將按順序執行(串行)的多個任務改爲並行之後,處理時間從多個任務之和變成了多個任務中最慢的任務的時間。計算機系統中某個層次(如應用程序)的並行能力是由它的底層(如操作系統)的併發能力提供支持的。既然多核計算機早已普及,那麼就沒有理由不好好利用並行處理能力了。

異步 在計算機系統中,運算往往要比輸入輸出(IO)操作快得多,不少看起來很慢的系統經常是在空置地等待完成 IO 操作。異步化 IO 操作能讓計算機不再等待 IO 完成,從而能最大化地利用系統的運算能力。

緩存 不論是運算,還是 IO 操作都是要耗費時間的。“利用空間換時間”經常是有效的優化方法:典型地,在運算斐波那契數列時,如果不是有通項公式,在不用緩存的情況下幾乎不可能寫出符合時間要求的算法。

精減與壓縮 還是 IO 的問題。如果 IO 慢,就更不能讓它執行不必要的操作了——比如多執行的 SQL、多加載的 JavaScript 文件等。經常,當數據量少了之後,由於需要處理的內容少了,還同時能夠節省運算的時間。

技巧性優化 上面所述的幾種方式,是在代碼級別的“通用”方法。而面對具體的問題,經常會有一些技巧性優化方法。比如,採用更具針對性的數據結構,或已經證實更高效的算法,更利於運行時進行垃圾回收的代碼風格等。

在上述這些方面的優化,在不同類型的應用程序裏,有具體的表現形式;而在不同編程平臺上,又可能有不同的實現。舉例來說,在 Web 前端應用裏,異步可表現爲異步加載 JavaScript/CSS 文件和異步執行 Ajax;而在一個 Web 後端應用裏,異步則表現爲對文件、網絡等操作的異步執行。如果具體到編程語言,Node.js 應用可能使用一個 nextTick 操作,而一個 ASP.NET 應用則使用一個 async 方法。需要參考其對應編程平臺的優化經驗進行實施。

最後,當我們掌握了優化性能的方法,也不意味着我們一定要用盡各種方法去優化我們編寫的每一個應用程序。在編寫代碼過程中,優先選用更高性能的寫法通常是有益的(例如,使用 HashSet 替換 List 來存儲需要快速查找的集合)。另一方面,過早優化是所有開發人員都容易忽略的陷阱(例如,集合的元素並不多,又大量用於循環時用 HashSet 替換了 List)。幾乎所有新的開發人員都聽說過“字符串是不可變量,應該儘量使用 StringBuilder”、“反射的性能很糟糕,應該儘量避免使用”之類的經驗之談。單從字面來理解確實都是金玉良言,但作爲一種優化手段,性能優化往往會要求應用程序遵守一些約束,這些約束有時會破壞代碼的可讀性,有時會改變編寫代碼的習慣,而這些往往意味着在更廣的維度上給團隊帶來成本。

《.NET性能優化》

[美]薩沙·戈德斯汀(Sasha Goldshtein) 著

作者和其他三位 ThoughtWorks 同事一同翻譯的《.NET 性能優化》一書已於近日出版,各大在線書店有售。本書詳細解釋了影響應用程序性能的 Windows、CLR 的內部結構,併爲讀者提供了衡量代碼如何獨立於外部因素執行優化的知識和工具。書中提供了大量的 C# 代碼示例和技巧,將幫您最大限度地提高算法和應用的性能。

儘管成書於數年之前,書中所述的大多數經驗仍適用於最新的 .NET 框架和運行時。它不光講述適用於 .NET 平臺的性能優化方法,還詳細地講解了性能度量的指標,以及普適性的性能優化的思路和原理。既授人以魚,又授人以漁,是不可多得的上佳之作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章