天天看點

深入了解JVM - 系統性能優化

系統性能優化并不是一上來就是JVM優化,相反JVM優化幾乎是最後的手段了。影響一個系統的性能的因素非常多,如圖:

深入了解JVM - 系統性能優化

從服務本身來看,影響服務性能的主要包扣:

  • 我們寫代碼時所選擇的資料結構和算法
  • 服務開啟的線程時是否合理
  • WEB應用,WEB服務
  • JVM方面的影響
  • 最後是作業系統的影響

從整個服務架構上來看還有:

  • 資料持久化
  • 服務間的遠端調用
  • 消息緩存等中間件的選擇

常用的性能測試名額

響應時間

一個請求從送出到響應所耗費的時間,一般比較關注平均響應時間,和最大響應時間。

常用元件的響應時間:

操作 響應時間
打開一個站點 幾秒
資料庫查詢一條記錄(有索引) 十幾毫秒
機械磁盤一次尋址定位 4毫秒
從機械磁盤順序讀取1M資料 2毫秒
從SSD磁盤順序讀取1M資料 0.3毫秒
從遠端分布式緩存Redis讀取一個資料 0.5毫秒
從記憶體讀取1M資料 十幾微妙
Java程式本地方法調用 幾微妙
網絡傳輸2Kb資料 1微妙

從上述表格我們能看出:

  1. 資料持久化,使用SSD與使用機械硬碟相比性能可以提高将近10倍;
  2. 資料查詢,資料如果直接在本地記憶體,那麼它的讀取效率比資料庫快将近1000倍,比redis快30倍左右;如果資料在redis緩存,那麼它的讀取速度比資料庫快30倍左右;這也是為什麼使用緩存是提升系統性能的“銀彈”的原因。
​​為監控而生的多級緩存架構 layering-cache​​這是我開源的一個多級緩存架構的實作,如果有興趣可以看一下。

并發數

并發數是指同一時刻,對伺服器有實際互動的請求數。一般并發數是在線上使用者數的5%-15%之間,如線上使用者數是1000,那麼可以預估并發數在50-150之間。

吞吐量

吞吐量是機關時間内完成的工作量(請求)的數量。如:每分鐘的資料庫事務,每秒傳送的檔案千位元組數,每分鐘的 Web 伺服器命中數。

關系

通常,平均響應時間越短,系統吞吐量越大;平均響應時間越長,系統吞吐量越小。但是,系統吞吐量越大, 未必平均響應時間越短。

性能優化原則

避免過早優化

不應該把大量的時間耗費在小的性能改進上,過早考慮優化是所有噩夢的根源。在開發初期我們的首要目标是完成功能,編寫清晰,直接,易懂的代碼。

進行系統性能測試

所有調優都應該建立在新能測試的基礎上,不要滿目靠猜測進行優化。

尋找系統瓶頸,分而治之,逐漸優化

性能測試後,對整個請求經曆的各個環節進行分析,排查出現性能瓶頸的地方,定位問題,分析影響性能的的主要因素是什麼?記憶體、磁盤IO、網絡、CPU,還是代碼問題?架構設計不足?或者确實是系統資源不足?

常用的性能優化手段

高并發優化

提高系統并發能力的方式,方法論上主要有兩種:垂直擴充(Scale Up)與水準擴充(Scale Out)。

垂直擴充

提升單機處理能力。垂直擴充的方式又有兩種:

  1. 增強單機硬體性能,例如:增加CPU核數如32核,更新更好的網卡如萬兆,更新更好的硬碟如SSD,擴充硬碟容量如2T,擴充系統記憶體如128G;
  2. 提升單機架構性能,例如:使用Cache來減少IO次數,使用異步來增加單服務吞吐量,使用無鎖資料結構來減少響應時間;

水準擴充

隻要增加伺服器數量,就能線性擴充系統性能。水準擴充對系統架構設計是有要求的,如何在架構各層進行可水準擴充的設計,以及網際網路公司架構各層常見的水準擴充實踐,是本文重點讨論的内容。

網際網路分層架構中,各層次水準擴充的實踐又有所不同:

  1. 反向代理層可以通過“DNS輪詢”的方式來進行水準擴充;
  2. 站點層可以通過nginx來進行水準擴充;
  3. 服務層可以通過服務連接配接池來進行水準擴充;
  4. 資料庫可以按照資料範圍,或者資料哈希的方式來進行水準擴充;

高可用優化

高可用的核心思想是:資源保護和備援。

資源保護常用手段是服務限流和熔斷降級;

備援是是指資源備份,如資料庫主從設計,redis的哨兵機制。

前端優化常用手段

浏覽器/App

  1. 減少請求數

    合并CSS,Js,圖檔;

  2. 使用用戶端緩存;

    靜态資源檔案緩存在浏覽器中,如果檔案發生了變化,則通過改變檔案名來更新緩存。

  3. 啟用壓縮

    它可以減少網絡傳輸量,但會給浏覽器和伺服器帶來性能的壓力,需要權衡使用。

  4. 資源檔案加載順序

    css放在頁面最上面,js放在最下面。

  5. 減少Cookie傳輸

    cookie會包含在每次的請求和響應中,是以哪些資料寫入cookie需要慎重考慮。

CDN加速

CDN,又稱内容分發網絡,本質仍然是一個緩存,而且是将資料緩存在使用者最近的地方。免費的CDN加速器​​七牛雲​​。

反向代理緩存

将靜态資源檔案緩存在反向代理伺服器上,一般是Nginx。

WEB元件分離

浏覽器在同一個域名下下載下傳資源存在并發限制,是以,将js,css和圖檔檔案放在不同的域名下,可以提高浏覽器在下載下傳web元件的并發數。

應用服務性能優化

緩存

緩存的本質是将資料存放在通路速度較高的媒體中,可以減少資料通路的時間,同時避免重複計算。

從上面響應時間表格我們可以看出,使用緩存将資料緩存在本地或者redis伺服器,将會對查詢效率有較大的提升。網站性能優化的第一定律也是使用緩存,​​為監控而生的多級緩存架構 layering-cache​​這是我開源的一個多級緩存架構的實作,如果有興趣可以看一下。

叢集

負載均衡伺服器(nginx,f5等)使用負責均衡算法,将請求分發到多個節點上進行處理。

異步

同步和異步關注的是結果消息的通信機制:

  • 同步:同步的意思就是調用方需要主動等待結果的傳回;
  • 異步:異步的意思就是不需要主動等待結果的傳回,而是通過其他手段比如,狀态通知,回調函數等;

阻塞和非阻塞主要關注的是等待結果傳回調用方的狀态:

  • 阻塞:是指結果傳回之前,目前線程被挂起,不做任何事;
  • 非阻塞:是指結果在傳回之前,線程可以做一些其他事,不會被挂起;

BIO、NIO和AIO

  • 同步阻塞:去商店買衣服,你去了之後發現衣服賣完了,商家說要去庫房拿,那你就在店裡面一直等,期間不做任何事(包括看手機),等着商家拿貨,這就是同步阻塞,效率低;
  • 同步非阻塞:去商店買衣服,你去了之後發現衣服賣完了,商家說要去庫房拿,這時你可以去繼續逛街,但是時不時需要回去問商家貨到了沒,這就是同步非阻塞;
  • 異步阻塞:去商店買衣服,你去了之後發現衣服賣完了,這個時候時候你給商家留下電話,等貨到了電話通知你,然後你就啥事都不敢,守着電話等通知,這個模式有點傻,用的很少;
  • 異步非阻塞:去商店買衣服,你去了之後發現衣服賣完了,這時候你給商家留下電話,然後就可以去逛街了,等貨物到了商家會回電話通知你。
jdk裡的BIO就屬于同步阻塞;jdk裡的NIO就屬于同步非阻塞;jdk裡的AIO就屬于異步。

常見的異步元件

  • Servlet3
  • 多線程
  • 消息隊列

代碼級别

  1. 選擇合适的資料結構,比如ArrayList和LinkedList的适用場景;
  2. 選擇更優的算法,比如最大子序列和問題,選擇窮舉算法那麼時間複雜度是O(n^3);如果選擇動态規劃算法,那麼時間複雜度就是O(n)了;
  3. 編寫更精簡的代碼,同樣正确的程式,小程式比大程式要快;
  4. 并發程式設計,充分利用多核CPU資源;
  5. 同步情況下減少鎖的競争;
  6. 資源的複用,比如單例模式,池化技術;
  7. 序列化優化,比如redis的使用預設的JDK序列化和FastJson序列化,最後JDK序列化所暫用的空間是FastJson的3倍左右;

GC優化

GC優化的終極目的

  • GC的時間夠小
  • GC的次數夠少,發生Full GC的周期足夠的長,時間合理,最好是不發生。

GC運作名額

如果滿足則一般不需要調優:

  • Minor GC執行時間不到50ms;

    - Minor GC執行不頻繁,約10秒一次;

  • Full GC執行時間不到1s;
  • Full GC執行頻率不算頻繁,不低于10分鐘1次;

調優的原則

  1. 大多數的java應用不需要GC調優
  2. 大部分需要GC調優的的,不是參數問題,是代碼問題
  3. 在實際使用中,分析GC情況優化代碼比優化GC參數要多得多;
  4. GC調優是最後的手段

GC調優的最重要的三個選項

  1. 選擇合适的GC回收器
  2. 選擇合适的堆大小
  3. 選擇年輕代在堆中的比重

GC調優的步驟

  1. 監控GC的狀态

    使用各種JVM工具,檢視目前日志,分析目前JVM參數設定,并且分析目前堆記憶體快照和gc日志,根據實際的各區域記憶體劃分和GC執行時間,覺得是否進行優化;

  2. 分析結果,判斷是否需要優化

    如果各項參數設定合理,系統沒有逾時日志出現,GC頻率不高,GC耗時不高,那麼沒有必要進行GC優化;如果GC時間超過1-3秒,或者頻繁GC,則必須優化;

  3. 調整GC類型和記憶體配置設定

    如果記憶體配置設定過大或過小,或者采用的GC收集器比較慢,則應該優先調整這些參數,并且先找1台或幾台機器進行beta,然後比較優化過的機器和沒有優化的機器的性能對比,并有針對性的做出最後選擇;

  4. 不斷的分析和調整

    通過不斷的試驗和試錯,分析并找到最合适的參數

  5. 全面應用參數

    如果找到了最合适的參數,則将這些參數應用到所有伺服器,并進行後續跟蹤。

GC日志

以參數-Xms5m -Xmx5m -XX:+PrintGCDetails -XX:+UseSerialGC為例:

[DefNew: 1855K->1855K(1856K), 0.0000148 secs][Tenured: 2815K->4095K(4096K), 0.0134819 secs] 4671K      
  • DefNew:指明了收集器類型,而且說明了收集發生在新生代。
  • 1855K->1855K(1856K):表示,回收前 新生代占用1855K,回收後占用1855K,新生代大小1856K。
  • 0.0000148 secs: 表明新生代回收耗時。
  • Tenured:表明收集發生在老年代
  • 2815K->4095K(4096K):回收前後的值
  • 0.0134819 secs:老年代回收耗時
  • 最後的4671K指明堆的大小。

收集器參數變為-XX:+UseParNewGC,日志變為:

[ParNew: 1856K->1856K(1856K), 0.0000107 secs][Tenured: 2890K->4095K(4096K), 0.0121148 secs]      
[PSYoungGen: 1024K->1022K(1536K)] [ParOldGen: 3783K->3782K(4096K)] 4807K->4804K(5632K)      

GC相關的參數

  • ​-verbose:gc​

    ​​和​

    ​-XX:+PrintGC​

    ​:列印簡單的GC日志
  • ​-XX:+PrintGCDetails​

    ​​和​

    ​+XX:+PrintGCTimeStamps​

    ​:列印詳細的GC日志
  • ​-Xlogger:[logpath]​

    ​​:指定GC日志路徑,如​

    ​Xlogger:log/gc.log​

  • ​-XX:+PrintHeapAtGC​

    ​:列印推資訊,擷取Heap在每次垃圾回收前後的使用狀況
  • ​-XX:+TraceClassLoading​

    ​: 在系統控制台資訊中看到class加載的過程和具體的class資訊,可用以分析類的加載順序以及是否可進行精簡操作。
  • ​-XX:+DisableExplicitGC​

    ​:禁止在運作期顯式地調用System.gc()
  • ​-XX:-HeapDumpOnOutOfMemoryError​

    ​:預設關閉,建議開啟,在java.lang.OutOfMemoryError 異常出現時,輸出一個dump.core檔案,記錄當時的堆記憶體快照。
  • ​-XX:HeapDumpPath=./java_pid<pid>.hprof​

    ​:預設是java程序啟動位置,用來設定堆記憶體快照的存儲檔案路徑。

存儲性能優化

  • 盡量使用SSD;
  • 定時清理資料或者按資料的性質分開存放;
  • 結果集處理,如:用setFetchSize控制jdbc每次從資料庫中傳回多少資料;