十大性能方面的錯誤

Mau78R 8年前發布 | 14K 次閱讀 程序員 正則表達式 .NET

Martin ThompsonLMAX 的聯合創始人,在 QCon圣保羅2016 上做過關于性能的keynote演講。他最初計劃的演講題目為“ 關于性能的神話與傳說 ”,不過Thompson后來將演講命名為“十大性能錯誤”,因為“我們都會犯錯誤,而且很容易就會出現錯誤”。

下面列出了他在生產環境下所見到的十大性能錯誤,并且還包含了如何避免的建議。

10.不進行升級。很多人抱怨他們的系統不夠快,并通過編寫更好的算法和數據結構來尋求幫助,Thompson認為實際上“他們所需的僅僅就是進行升級”。升級操作系統、JVM、CLR等等。不進行升級的常見借口就是“在新版本中可能會有bug。”

為了避免這種狀況,可以進行定期的持續集成和測試,這應該是開發流程的基礎組成部分。Thompson以一個實時系統進行了例證,開發人員針對新版本的數據庫進行了測試,在所有的測試通過之后,他們就將其發布到了生產環境之中。

9.重復性的工作。Thompson講述了某個系統的故事,這個系統是用來提供Web頁面的,它非常緩慢,開發人員最初認為是數據庫的問題并試圖在這方面進行調優。但是當他在系統上運行profiler時,發現在一個循環中,ORM被調用了7,000次,這才是頁面加載緩慢的罪魁禍首。當這個循環的問題修復之后,系統的響應變得完全正常。這里學到的經驗就是“對系統進行度量。如果系統是一個黑盒的話,你就無法說明時間都耗費在了哪里。”

8. 加載性能依賴于數據。Thompson展現了一個基準測試結果,它會執行一項操作,該操作會對內存(RAM)中1GB數組的所有long型進行求和。這里所耗費的時間取決于內存是如何訪問的,如下面的表格所示:

這個基準測試的結果顯示并非所有的內存操作都是等價的,我們需要關注它是如何進行處理的。Thompson認為非常重要的一點在于了解各種數據結構的性能,他指出對于2GB以上的場景,Java的HashMap要比.NET的Dictionary慢十倍以上。他還補充說,也有一些場景.NET要比Java慢得多。

7. 分配的內存太多。盡管在很多場景中,內存分配幾乎是沒有什么成本的,但是它們的回收卻并非如此,因為在面對大量的數據集時,垃圾收集器需要更多的時間。當分配大量的數據時,緩存會被填滿,較舊的數據會被舍棄,使得在數據操作上的效率變為90ns/op而不是7ns/op,這里變慢了不止一個數量級。

6. 采用并行。盡管對于特定的算法來說,采用并行很有吸引力,但是它也有一些局限性和相關的開銷。Thompson引用了 “可擴展性!但是其COST如何?” 這篇論文,論文的作者通過引入COST(勝過單線程的配置,Configuration that Outperforms a Single Thread)對比了并行系統以及單線程的系統,COST的定義如下:

在特定的平臺中,特定問題的COST指的是優于單線程方案所需的硬件配置。COST將系統的擴展性與系統所引入的開銷進行了權衡,并指明了系統實際所能取得的性能,它們可能并沒有帶來實際的收益,卻增加了并行所引入了開銷。

作者分析了各種數據并行系統的測量結果,并得出如下的結論:“很多的系統要么具有非常高的COST,通常會需要上百個核心,要么針對他們所報告的配置,其性能要比單線程方案更差。”

在這個話題中,Thompson指出,并行任務會有相關的通信和同步開銷,并且有些活動本質上要求是串行的,不能實現并行。按照 Amdahl定律 ,如果系統中有5%的活動需要串行,那么不管使用了多少個處理器,系統的速度提升最多只能達到20倍。

Thompson還提到了Neil J. Gunther在1993年所提出的通用可擴展性定律(Universal Scalability Law, PDF ),該定律指出在并行非共享系統(shared-nothing)中甚至會存在更多的局限性,當所使用的處理器數量達到一定程度后,速度會出現下降,這取決于并發、競爭以及同步的水平。(更多的細節可以參考 如何量化可擴展性 這個頁面。)按照上述兩個規律所總結的速度與處理器數量之間的關系如下圖所示:

Thompson指出通過USL能夠看到性能的下降,這要歸因于并行系統中組件之間進行通信所消耗的成本:“在系統中,所投入資源越多,通信路徑也會隨之增多,這會使算法的效率降低。”

Thompson補充說,在構建并行系統時,主要的建議是避免共享可變(mutable)的狀態,因為“它非常難以進行判斷……最終你會遇到很多的bug”。推薦的方式是要么采用非共享架構,要么針對特定的一塊數據,只使用一個寫入器。

對這個性能問題,他的最終建議:如果你想提升算法的速度的話,在嘗試并行方案之前,先設法提升單線程版本的性能,因為并行方案實在是太難了。

5. 不理解TCP。針對這個話題,Thompson認為很多在考慮微服務架構的人對TCP并沒有充分的理解。在特定的場景中,有可能會遇到延遲的ACK,它會限制鏈路上所發送的數據包,每秒鐘只會有2-5個數據包。這是因為TCP兩個算法所引起的死鎖: Nagle 以及 TCP Delayed Acknowledgement 。在200-500ms的超時之后,會打破這個死鎖,但是微服務之間的通信卻會分別受到影響。推薦的方案是使用TCP_NODELAY,它會禁用Nagle的算法,多個更小的包可以依次發送。按照Thompson的說法,其中的差別在5到500 req/sec。

4. 同步通信。客戶端和服務器之間的同步通信會帶來時間的損耗,對于需要快速通信的系統來說,這會成為一個問題。Thompson說,它的解決方案并不是購買更加昂貴和快速的硬件,而是使用異步通信。在這種場景下,客戶端可以發送多個請求到服務器端,而不必等待它們之間的響應。采用這種方式需要改變客戶端發送請求的方式,但這是值得的。

3. 文本編碼。開發人員很多時候會選擇使用文本編碼格式實現鏈路上的數據傳輸,比如JSON、XML或Base64,因為“這對人類是可讀的”。但是Thompson指出在兩個系統之間進行對話的時候,是沒有人讀這些數據的。借助這種方式,使用簡單的文本編輯器就能很容易地進行調試,但是在將二進制數據與文本之間進行互相轉換的時候,這會帶來很高的CPU損耗。該問題的解決方案是使用能夠理解二進制的更好的工具,Thompson提到了 Wireshark

2. API設計。按照Thompson的說法,有一些與性能相關的最負面影響是由API引起的。它使用如下的代碼來闡述較差的代碼簽名:

public void startElement( String uri, String localName, String qName, Attributes atts) throws SAXException

描述:

在處理XML的時候,通常我們并不會使用這些值[三個String以及屬性的集合]。我們分配了很多的內容,但是卻將其浪費并拋棄掉了。這會損耗電池的壽命,白白地浪費資源。我們需要使其更加簡單一些。

他建議采用如下的簽名,實現更加簡單的方法:

public void characters( char[] ch, int start, int length) throws SAXException

有些人可能會抱怨后面的這個方法要比前一個更難用,Thompson建議采用組合的方式,將其中一個用另一個封裝起來,這樣的話,能夠給用戶多一個選擇。如果性能不是什么問題的話,可以采用第一種(使用String),否則的話,第二個方案會更好一些。

Thompson提到的第二個樣例是字符串拆分:

public String[] split(String regex)

這個方法簽名相關的性能問題包括:

  • 每次方法調用的時候,正則表達式都需要進行編譯;
  • 需要實例化一個動態的結構,用來存儲字符串中所包含的初始數量未知的token;
  • 返回的結構是一個固定大小的數組,這就必須要將token收集到一個臨時的結構中,然后再拷貝到數組里面;
  • 如果調用者想要對這些token進行一些操作的話,比如排序,需要將它們拷貝到另外一個結構之中。

更好的方案是使用Iterable,它能夠避免在內存中創建中間狀態的token副本:

public Iterable split(String regex)

另外一種方案是允許調用者提供存儲token的集合。如果調用者想要對token列表去重的話,應該傳遞一個Set進來,如果想得到有序列表的話,就需要傳遞一個TreeMap進來:

public void split( String regex, Collection dst)

1.日志。Thompson所列的排名第一的性能問題是寫日志所耗費的時間。他通過一個圖表展現了當線程數增加的時候,日志操作所耗費的平均時間:

這個圖顯示了一個100%的順序操作,不管使用多少線程來記錄日志,所需的時間均呈線性增長。Thompson說大多數已有的日志系統都可以得出這樣一幅圖表,“Logger是系統中最大的瓶頸之一”。這個問題的解決方案是使用異步的Logger。

另外,Logger所記錄的數據應該是結構化的數據,便于后續的工具進行讀取和處理,而不應該是一堆String。如果是記錄重復的錯誤,他建議在錯誤第一次出現的時候進行記錄,后續出現時只需對一個計數器進行遞增,告知對應的錯誤出現了多少次即可。對于實時系統的調試,Thompson建議使用代碼編織(code weaver)的技術,如 Byte Buddy ,因為它能夠避免編寫和運行不必要的日志代碼。

這個分享的講義可以在線獲取( PDF )。

關于作者

Abel Avram從2008年以來參與了很多InfoQ的編輯活動,熱衷于編寫移動領域、HTML、.NET、云計算、EA以及其他話題的新聞報道。他是 《Domain-Driven Design Quickly》 一書的合著者。

過去,他曾經擔任多年的Java和.NET軟件工程師、遺留系統的項目/團隊領導等職務。目前,他的職業是羅馬尼亞蒂米什瓦拉技術大學計算機與自動化系的助教。

如果你有意提交新聞或教育方面的文章,可以通過abel [at] infoq.com聯系到他。

查看英文原文: Top 10 Performance Mistakes

 

閱讀原文

 

 本文由用戶 Mau78R 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!