系統性能優化并不是一上來就是JVM優化,相反JVM優化幾乎是最後的手段了。影響一個系統的性能的因素非常多,如圖:
從服務本身來看,影響服務性能的主要包扣:
- 我們寫代碼時所選擇的資料結構和算法
- 服務開啟的線程時是否合理
- WEB應用,WEB服務
- JVM方面的影響
- 最後是作業系統的影響
從整個服務架構上來看還有:
- 資料持久化
- 服務間的遠端調用
- 消息緩存等中間件的選擇
常用的性能測試名額
響應時間
一個請求從送出到響應所耗費的時間,一般比較關注平均響應時間,和最大響應時間。
常用元件的響應時間:
操作 | 響應時間 |
打開一個站點 | 幾秒 |
資料庫查詢一條記錄(有索引) | 十幾毫秒 |
機械磁盤一次尋址定位 | 4毫秒 |
從機械磁盤順序讀取1M資料 | 2毫秒 |
從SSD磁盤順序讀取1M資料 | 0.3毫秒 |
從遠端分布式緩存Redis讀取一個資料 | 0.5毫秒 |
從記憶體讀取1M資料 | 十幾微妙 |
Java程式本地方法調用 | 幾微妙 |
網絡傳輸2Kb資料 | 1微妙 |
從上述表格我們能看出:
- 資料持久化,使用SSD與使用機械硬碟相比性能可以提高将近10倍;
- 資料查詢,資料如果直接在本地記憶體,那麼它的讀取效率比資料庫快将近1000倍,比redis快30倍左右;如果資料在redis緩存,那麼它的讀取速度比資料庫快30倍左右;這也是為什麼使用緩存是提升系統性能的“銀彈”的原因。
為監控而生的多級緩存架構 layering-cache這是我開源的一個多級緩存架構的實作,如果有興趣可以看一下。
并發數
并發數是指同一時刻,對伺服器有實際互動的請求數。一般并發數是在線上使用者數的5%-15%之間,如線上使用者數是1000,那麼可以預估并發數在50-150之間。
吞吐量
吞吐量是機關時間内完成的工作量(請求)的數量。如:每分鐘的資料庫事務,每秒傳送的檔案千位元組數,每分鐘的 Web 伺服器命中數。
關系
通常,平均響應時間越短,系統吞吐量越大;平均響應時間越長,系統吞吐量越小。但是,系統吞吐量越大, 未必平均響應時間越短。
性能優化原則
避免過早優化
不應該把大量的時間耗費在小的性能改進上,過早考慮優化是所有噩夢的根源。在開發初期我們的首要目标是完成功能,編寫清晰,直接,易懂的代碼。
進行系統性能測試
所有調優都應該建立在新能測試的基礎上,不要滿目靠猜測進行優化。
尋找系統瓶頸,分而治之,逐漸優化
性能測試後,對整個請求經曆的各個環節進行分析,排查出現性能瓶頸的地方,定位問題,分析影響性能的的主要因素是什麼?記憶體、磁盤IO、網絡、CPU,還是代碼問題?架構設計不足?或者确實是系統資源不足?
常用的性能優化手段
高并發優化
提高系統并發能力的方式,方法論上主要有兩種:垂直擴充(Scale Up)與水準擴充(Scale Out)。
垂直擴充
提升單機處理能力。垂直擴充的方式又有兩種:
- 增強單機硬體性能,例如:增加CPU核數如32核,更新更好的網卡如萬兆,更新更好的硬碟如SSD,擴充硬碟容量如2T,擴充系統記憶體如128G;
- 提升單機架構性能,例如:使用Cache來減少IO次數,使用異步來增加單服務吞吐量,使用無鎖資料結構來減少響應時間;
水準擴充
隻要增加伺服器數量,就能線性擴充系統性能。水準擴充對系統架構設計是有要求的,如何在架構各層進行可水準擴充的設計,以及網際網路公司架構各層常見的水準擴充實踐,是本文重點讨論的内容。
網際網路分層架構中,各層次水準擴充的實踐又有所不同:
- 反向代理層可以通過“DNS輪詢”的方式來進行水準擴充;
- 站點層可以通過nginx來進行水準擴充;
- 服務層可以通過服務連接配接池來進行水準擴充;
- 資料庫可以按照資料範圍,或者資料哈希的方式來進行水準擴充;
高可用優化
高可用的核心思想是:資源保護和備援。
資源保護常用手段是服務限流和熔斷降級;
備援是是指資源備份,如資料庫主從設計,redis的哨兵機制。
前端優化常用手段
浏覽器/App
-
減少請求數
合并CSS,Js,圖檔;
-
使用用戶端緩存;
靜态資源檔案緩存在浏覽器中,如果檔案發生了變化,則通過改變檔案名來更新緩存。
-
啟用壓縮
它可以減少網絡傳輸量,但會給浏覽器和伺服器帶來性能的壓力,需要權衡使用。
-
資源檔案加載順序
css放在頁面最上面,js放在最下面。
-
減少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
- 多線程
- 消息隊列
代碼級别
- 選擇合适的資料結構,比如ArrayList和LinkedList的适用場景;
- 選擇更優的算法,比如最大子序列和問題,選擇窮舉算法那麼時間複雜度是O(n^3);如果選擇動态規劃算法,那麼時間複雜度就是O(n)了;
- 編寫更精簡的代碼,同樣正确的程式,小程式比大程式要快;
- 并發程式設計,充分利用多核CPU資源;
- 同步情況下減少鎖的競争;
- 資源的複用,比如單例模式,池化技術;
- 序列化優化,比如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次;
調優的原則
- 大多數的java應用不需要GC調優
- 大部分需要GC調優的的,不是參數問題,是代碼問題
- 在實際使用中,分析GC情況優化代碼比優化GC參數要多得多;
- GC調優是最後的手段
GC調優的最重要的三個選項
- 選擇合适的GC回收器
- 選擇合适的堆大小
- 選擇年輕代在堆中的比重
GC調優的步驟
-
監控GC的狀态
使用各種JVM工具,檢視目前日志,分析目前JVM參數設定,并且分析目前堆記憶體快照和gc日志,根據實際的各區域記憶體劃分和GC執行時間,覺得是否進行優化;
-
分析結果,判斷是否需要優化
如果各項參數設定合理,系統沒有逾時日志出現,GC頻率不高,GC耗時不高,那麼沒有必要進行GC優化;如果GC時間超過1-3秒,或者頻繁GC,則必須優化;
-
調整GC類型和記憶體配置設定
如果記憶體配置設定過大或過小,或者采用的GC收集器比較慢,則應該優先調整這些參數,并且先找1台或幾台機器進行beta,然後比較優化過的機器和沒有優化的機器的性能對比,并有針對性的做出最後選擇;
-
不斷的分析和調整
通過不斷的試驗和試錯,分析并找到最合适的參數
-
全面應用參數
如果找到了最合适的參數,則将這些參數應用到所有伺服器,并進行後續跟蹤。
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
:列印簡單的GC日志-XX:+PrintGC
-
和-XX:+PrintGCDetails
:列印詳細的GC日志+XX:+PrintGCTimeStamps
-
:指定GC日志路徑,如-Xlogger:[logpath]
Xlogger:log/gc.log
-
:列印推資訊,擷取Heap在每次垃圾回收前後的使用狀況-XX:+PrintHeapAtGC
-
: 在系統控制台資訊中看到class加載的過程和具體的class資訊,可用以分析類的加載順序以及是否可進行精簡操作。-XX:+TraceClassLoading
-
:禁止在運作期顯式地調用System.gc()-XX:+DisableExplicitGC
-
:預設關閉,建議開啟,在java.lang.OutOfMemoryError 異常出現時,輸出一個dump.core檔案,記錄當時的堆記憶體快照。-XX:-HeapDumpOnOutOfMemoryError
-
:預設是java程序啟動位置,用來設定堆記憶體快照的存儲檔案路徑。-XX:HeapDumpPath=./java_pid<pid>.hprof
存儲性能優化
- 盡量使用SSD;
- 定時清理資料或者按資料的性質分開存放;
- 結果集處理,如:用setFetchSize控制jdbc每次從資料庫中傳回多少資料;