系統(tǒng)性能優(yōu)化并不是一上來(lái)就是JVM優(yōu)化,相反JVM優(yōu)化幾乎是最后的手段了。影響一個(gè)系統(tǒng)的性能的因素非常多,如圖:

從服務(wù)本身來(lái)看,影響服務(wù)性能的主要包扣:
- 我們寫(xiě)代碼時(shí)所選擇的數(shù)據(jù)結(jié)構(gòu)和算法
- 服務(wù)開(kāi)啟的線程時(shí)是否合理
- WEB應(yīng)用,WEB服務(wù)
- JVM方面的影響
- 最后是操作系統(tǒng)的影響
從整個(gè)服務(wù)架構(gòu)上來(lái)看還有:
- 數(shù)據(jù)持久化
- 服務(wù)間的遠(yuǎn)程調(diào)用
- 消息緩存等中間件的選擇
常用的性能測(cè)試指標(biāo)
響應(yīng)時(shí)間
一個(gè)請(qǐng)求從提交到響應(yīng)所耗費(fèi)的時(shí)間,一般比較關(guān)注平均響應(yīng)時(shí)間,和最大響應(yīng)時(shí)間。
常用組件的響應(yīng)時(shí)間:
| 操作 | 響應(yīng)時(shí)間 |
|---|---|
| 打開(kāi)一個(gè)站點(diǎn) | 幾秒 |
| 數(shù)據(jù)庫(kù)查詢一條記錄(有索引) | 十幾毫秒 |
| 機(jī)械磁盤(pán)一次尋址定位 | 4毫秒 |
| 從機(jī)械磁盤(pán)順序讀取1M數(shù)據(jù) | 2毫秒 |
| 從SSD磁盤(pán)順序讀取1M數(shù)據(jù) | 0.3毫秒 |
| 從遠(yuǎn)程分布式緩存Redis讀取一個(gè)數(shù)據(jù) | 0.5毫秒 |
| 從內(nèi)存讀取1M數(shù)據(jù) | 十幾微妙 |
| Java程序本地方法調(diào)用 | 幾微妙 |
從上述表格我們能看出:
- 數(shù)據(jù)持久化,使用SSD與使用機(jī)械硬盤(pán)相比性能可以提高將近10倍;
- 數(shù)據(jù)查詢,數(shù)據(jù)如果直接在本地內(nèi)存,那么它的讀取效率比數(shù)據(jù)庫(kù)快將近1000倍,比redis快30倍左右;如果數(shù)據(jù)在redis緩存,那么它的讀取速度比數(shù)據(jù)庫(kù)快30倍左右;這也是為什么使用緩存是提升系統(tǒng)性能的“銀彈”的原因。
為監(jiān)控而生的多級(jí)緩存框架 layering-cache這是我開(kāi)源的一個(gè)多級(jí)緩存框架的實(shí)現(xiàn),如果有興趣可以看一下。
并發(fā)數(shù)
并發(fā)數(shù)是指同一時(shí)刻,對(duì)服務(wù)器有實(shí)際交互的請(qǐng)求數(shù)。一般并發(fā)數(shù)是在在線用戶數(shù)的5%-15%之間,如在線用戶數(shù)是1000,那么可以預(yù)估并發(fā)數(shù)在50-150之間。
吞吐量
吞吐量是單位時(shí)間內(nèi)完成的工作量(請(qǐng)求)的數(shù)量。如:每分鐘的數(shù)據(jù)庫(kù)事務(wù),每秒傳送的文件千字節(jié)數(shù),每分鐘的 Web 服務(wù)器命中數(shù)。
關(guān)系
通常,平均響應(yīng)時(shí)間越短,系統(tǒng)吞吐量越大;平均響應(yīng)時(shí)間越長(zhǎng),系統(tǒng)吞吐量越小。但是,系統(tǒng)吞吐量越大, 未必平均響應(yīng)時(shí)間越短。
性能優(yōu)化原則
避免過(guò)早優(yōu)化
不應(yīng)該把大量的時(shí)間耗費(fèi)在小的性能改進(jìn)上,過(guò)早考慮優(yōu)化是所有噩夢(mèng)的根源。在開(kāi)發(fā)初期我們的首要目標(biāo)是完成功能,編寫(xiě)清晰,直接,易懂的代碼。
進(jìn)行系統(tǒng)性能測(cè)試
所有調(diào)優(yōu)都應(yīng)該建立在新能測(cè)試的基礎(chǔ)上,不要滿目靠猜測(cè)進(jìn)行優(yōu)化。
尋找系統(tǒng)瓶頸,分而治之,逐步優(yōu)化
性能測(cè)試后,對(duì)整個(gè)請(qǐng)求經(jīng)歷的各個(gè)環(huán)節(jié)進(jìn)行分析,排查出現(xiàn)性能瓶頸的地方,定位問(wèn)題,分析影響性能的的主要因素是什么??jī)?nèi)存、磁盤(pán)IO、網(wǎng)絡(luò)、CPU,還是代碼問(wèn)題?架構(gòu)設(shè)計(jì)不足?或者確實(shí)是系統(tǒng)資源不足?
常用的性能優(yōu)化手段
高并發(fā)優(yōu)化
提高系統(tǒng)并發(fā)能力的方式,方法論上主要有兩種:垂直擴(kuò)展(Scale Up)與水平擴(kuò)展(Scale Out)。
垂直擴(kuò)展
提升單機(jī)處理能力。垂直擴(kuò)展的方式又有兩種:
- 增強(qiáng)單機(jī)硬件性能,例如:增加CPU核數(shù)如32核,升級(jí)更好的網(wǎng)卡如萬(wàn)兆,升級(jí)更好的硬盤(pán)如SSD,擴(kuò)充硬盤(pán)容量如2T,擴(kuò)充系統(tǒng)內(nèi)存如128G;
- 提升單機(jī)架構(gòu)性能,例如:使用Cache來(lái)減少I(mǎi)O次數(shù),使用異步來(lái)增加單服務(wù)吞吐量,使用無(wú)鎖數(shù)據(jù)結(jié)構(gòu)來(lái)減少響應(yīng)時(shí)間;
水平擴(kuò)展
只要增加服務(wù)器數(shù)量,就能線性擴(kuò)充系統(tǒng)性能。水平擴(kuò)展對(duì)系統(tǒng)架構(gòu)設(shè)計(jì)是有要求的,如何在架構(gòu)各層進(jìn)行可水平擴(kuò)展的設(shè)計(jì),以及互聯(lián)網(wǎng)公司架構(gòu)各層常見(jiàn)的水平擴(kuò)展實(shí)踐,是本文重點(diǎn)討論的內(nèi)容。
互聯(lián)網(wǎng)分層架構(gòu)中,各層次水平擴(kuò)展的實(shí)踐又有所不同:
- 反向代理層可以通過(guò)“DNS輪詢”的方式來(lái)進(jìn)行水平擴(kuò)展;
- 站點(diǎn)層可以通過(guò)nginx來(lái)進(jìn)行水平擴(kuò)展;
- 服務(wù)層可以通過(guò)服務(wù)連接池來(lái)進(jìn)行水平擴(kuò)展;
- 數(shù)據(jù)庫(kù)可以按照數(shù)據(jù)范圍,或者數(shù)據(jù)哈希的方式來(lái)進(jìn)行水平擴(kuò)展;
高可用優(yōu)化
高可用的核心思想是:資源保護(hù)和冗余。
資源保護(hù)常用手段是服務(wù)限流和熔斷降級(jí);
冗余是是指資源備份,如數(shù)據(jù)庫(kù)主從設(shè)計(jì),redis的哨兵機(jī)制。
前端優(yōu)化常用手段
瀏覽器/App
- 減少請(qǐng)求數(shù)
合并CSS,Js,圖片; - 使用客戶端緩存;
靜態(tài)資源文件緩存在瀏覽器中,如果文件發(fā)生了變化,則通過(guò)改變文件名來(lái)更新緩存。 - 啟用壓縮
它可以減少網(wǎng)絡(luò)傳輸量,但會(huì)給瀏覽器和服務(wù)器帶來(lái)性能的壓力,需要權(quán)衡使用。 - 資源文件加載順序
css放在頁(yè)面最上面,js放在最下面。 - 減少Cookie傳輸
cookie會(huì)包含在每次的請(qǐng)求和響應(yīng)中,因此哪些數(shù)據(jù)寫(xiě)入cookie需要慎重考慮。
CDN加速
CDN,又稱內(nèi)容分發(fā)網(wǎng)絡(luò),本質(zhì)仍然是一個(gè)緩存,而且是將數(shù)據(jù)緩存在用戶最近的地方。免費(fèi)的CDN加速器七牛云。
反向代理緩存
將靜態(tài)資源文件緩存在反向代理服務(wù)器上,一般是Nginx。
WEB組件分離
瀏覽器在同一個(gè)域名下下載資源存在并發(fā)限制,所以,將js,css和圖片文件放在不同的域名下,可以提高瀏覽器在下載web組件的并發(fā)數(shù)。
應(yīng)用服務(wù)性能優(yōu)化
緩存
緩存的本質(zhì)是將數(shù)據(jù)存放在訪問(wèn)速度較高的介質(zhì)中,可以減少數(shù)據(jù)訪問(wèn)的時(shí)間,同時(shí)避免重復(fù)計(jì)算。
從上面響應(yīng)時(shí)間表格我們可以看出,使用緩存將數(shù)據(jù)緩存在本地或者redis服務(wù)器,將會(huì)對(duì)查詢效率有較大的提升。網(wǎng)站性能優(yōu)化的第一定律也是使用緩存,為監(jiān)控而生的多級(jí)緩存框架 layering-cache這是我開(kāi)源的一個(gè)多級(jí)緩存框架的實(shí)現(xiàn),如果有興趣可以看一下。
集群
負(fù)載均衡服務(wù)器(nginx,f5等)使用負(fù)責(zé)均衡算法,將請(qǐng)求分發(fā)到多個(gè)節(jié)點(diǎn)上進(jìn)行處理。
異步
同步和異步關(guān)注的是結(jié)果消息的通信機(jī)制:
- 同步:同步的意思就是調(diào)用方需要主動(dòng)等待結(jié)果的返回;
- 異步:異步的意思就是不需要主動(dòng)等待結(jié)果的返回,而是通過(guò)其他手段比如,狀態(tài)通知,回調(diào)函數(shù)等;
阻塞和非阻塞主要關(guān)注的是等待結(jié)果返回調(diào)用方的狀態(tài):
- 阻塞:是指結(jié)果返回之前,當(dāng)前線程被掛起,不做任何事;
- 非阻塞:是指結(jié)果在返回之前,線程可以做一些其他事,不會(huì)被掛起;
BIO、NIO和AIO
- 同步阻塞:去商店買(mǎi)衣服,你去了之后發(fā)現(xiàn)衣服賣完了,商家說(shuō)要去庫(kù)房拿,那你就在店里面一直等,期間不做任何事(包括看手機(jī)),等著商家拿貨,這就是同步阻塞,效率低;
- 同步非阻塞:去商店買(mǎi)衣服,你去了之后發(fā)現(xiàn)衣服賣完了,商家說(shuō)要去庫(kù)房拿,這時(shí)你可以去繼續(xù)逛街,但是時(shí)不時(shí)需要回去問(wèn)商家貨到了沒(méi),這就是同步非阻塞;
- 異步阻塞:去商店買(mǎi)衣服,你去了之后發(fā)現(xiàn)衣服賣完了,這個(gè)時(shí)候時(shí)候你給商家留下電話,等貨到了電話通知你,然后你就啥事都不敢,守著電話等通知,這個(gè)模式有點(diǎn)傻,用的很少;
- 異步非阻塞:去商店買(mǎi)衣服,你去了之后發(fā)現(xiàn)衣服賣完了,這時(shí)候你給商家留下電話,然后就可以去逛街了,等貨物到了商家會(huì)回電話通知你。
jdk里的BIO就屬于同步阻塞;jdk里的NIO就屬于同步非阻塞;jdk里的AIO就屬于異步。
常見(jiàn)的異步組件
- Servlet3
- 多線程
- 消息隊(duì)列
代碼級(jí)別
- 選擇合適的數(shù)據(jù)結(jié)構(gòu),比如ArrayList和LinkedList的適用場(chǎng)景;
- 選擇更優(yōu)的算法,比如最大子序列和問(wèn)題,選擇窮舉算法那么時(shí)間復(fù)雜度是O(n^3);如果選擇動(dòng)態(tài)規(guī)劃算法,那么時(shí)間復(fù)雜度就是O(n)了;
- 編寫(xiě)更精簡(jiǎn)的代碼,同樣正確的程序,小程序比大程序要快;
- 并發(fā)編程,充分利用多核CPU資源;
- 同步情況下減少鎖的競(jìng)爭(zhēng);
- 資源的復(fù)用,比如單例模式,池化技術(shù);
- 序列化優(yōu)化,比如redis的使用默認(rèn)的JDK序列化和FastJson序列化,最后JDK序列化所暫用的空間是FastJson的3倍左右;
GC優(yōu)化
GC優(yōu)化的終極目的
- GC的時(shí)間夠小
- GC的次數(shù)夠少,發(fā)生Full GC的周期足夠的長(zhǎng),時(shí)間合理,最好是不發(fā)生。
GC運(yùn)行指標(biāo)
如果滿足則一般不需要調(diào)優(yōu):
- Minor GC執(zhí)行時(shí)間不到50ms;
- Minor GC執(zhí)行不頻繁,約10秒一次;
- Full GC執(zhí)行時(shí)間不到1s;
- Full GC執(zhí)行頻率不算頻繁,不低于10分鐘1次;
調(diào)優(yōu)的原則
- 大多數(shù)的java應(yīng)用不需要GC調(diào)優(yōu)
- 大部分需要GC調(diào)優(yōu)的的,不是參數(shù)問(wèn)題,是代碼問(wèn)題
- 在實(shí)際使用中,分析GC情況優(yōu)化代碼比優(yōu)化GC參數(shù)要多得多;
- GC調(diào)優(yōu)是最后的手段
GC調(diào)優(yōu)的最重要的三個(gè)選項(xiàng)
- 選擇合適的GC回收器
- 選擇合適的堆大小
- 選擇年輕代在堆中的比重
GC調(diào)優(yōu)的步驟
監(jiān)控GC的狀態(tài)
使用各種JVM工具,查看當(dāng)前日志,分析當(dāng)前JVM參數(shù)設(shè)置,并且分析當(dāng)前堆內(nèi)存快照和gc日志,根據(jù)實(shí)際的各區(qū)域內(nèi)存劃分和GC執(zhí)行時(shí)間,覺(jué)得是否進(jìn)行優(yōu)化;分析結(jié)果,判斷是否需要優(yōu)化
如果各項(xiàng)參數(shù)設(shè)置合理,系統(tǒng)沒(méi)有超時(shí)日志出現(xiàn),GC頻率不高,GC耗時(shí)不高,那么沒(méi)有必要進(jìn)行GC優(yōu)化;如果GC時(shí)間超過(guò)1-3秒,或者頻繁GC,則必須優(yōu)化;調(diào)整GC類型和內(nèi)存分配
如果內(nèi)存分配過(guò)大或過(guò)小,或者采用的GC收集器比較慢,則應(yīng)該優(yōu)先調(diào)整這些參數(shù),并且先找1臺(tái)或幾臺(tái)機(jī)器進(jìn)行beta,然后比較優(yōu)化過(guò)的機(jī)器和沒(méi)有優(yōu)化的機(jī)器的性能對(duì)比,并有針對(duì)性的做出最后選擇;不斷的分析和調(diào)整
通過(guò)不斷的試驗(yàn)和試錯(cuò),分析并找到最合適的參數(shù)全面應(yīng)用參數(shù)
如果找到了最合適的參數(shù),則將這些參數(shù)應(yīng)用到所有服務(wù)器,并進(jìn)行后續(xù)跟蹤。
GC日志
以參數(shù)-Xms5m -Xmx5m -XX:+PrintGCDetails -XX:+UseSerialGC為例:
[DefNew: 1855K->1855K(1856K), 0.0000148 secs][Tenured: 2815K->4095K(4096K), 0.0134819 secs] 4671K
- DefNew:指明了收集器類型,而且說(shuō)明了收集發(fā)生在新生代。
- 1855K->1855K(1856K):表示,回收前 新生代占用1855K,回收后占用1855K,新生代大小1856K。
- 0.0000148 secs: 表明新生代回收耗時(shí)。
- Tenured:表明收集發(fā)生在老年代
- 2815K->4095K(4096K):回收前后的值
- 0.0134819 secs:老年代回收耗時(shí)
- 最后的4671K指明堆的大小。
收集器參數(shù)變?yōu)?XX:+UseParNewGC,日志變?yōu)椋?/p>
[ParNew: 1856K->1856K(1856K), 0.0000107 secs][Tenured: 2890K->4095K(4096K), 0.0121148 secs]
收集器參數(shù)變?yōu)?XX:+ UseParallelGC或UseParallelOldGC,日志變?yōu)椋?/p>
[PSYoungGen: 1024K->1022K(1536K)] [ParOldGen: 3783K->3782K(4096K)] 4807K->4804K(5632K)
CMS收集器和G1收集器會(huì)有明顯的相關(guān)字樣
GC相關(guān)的參數(shù)
-
-verbose:gc和-XX:+PrintGC:打印簡(jiǎn)單的GC日志 -
-XX:+PrintGCDetails和+XX:+PrintGCTimeStamps:打印詳細(xì)的GC日志 -
-Xlogger:[logpath]:指定GC日志路徑,如Xlogger:log/gc.log -
-XX:+PrintHeapAtGC:打印推信息,獲取Heap在每次垃圾回收前后的使用狀況 -
-XX:+TraceClassLoading: 在系統(tǒng)控制臺(tái)信息中看到class加載的過(guò)程和具體的class信息,可用以分析類的加載順序以及是否可進(jìn)行精簡(jiǎn)操作。 -
-XX:+DisableExplicitGC:禁止在運(yùn)行期顯式地調(diào)用System.gc() -
-XX:-HeapDumpOnOutOfMemoryError:默認(rèn)關(guān)閉,建議開(kāi)啟,在java.lang.OutOfMemoryError 異常出現(xiàn)時(shí),輸出一個(gè)dump.core文件,記錄當(dāng)時(shí)的堆內(nèi)存快照。 -
-XX:HeapDumpPath=./java_pid<pid>.hprof:默認(rèn)是java進(jìn)程啟動(dòng)位置,用來(lái)設(shè)置堆內(nèi)存快照的存儲(chǔ)文件路徑。
存儲(chǔ)性能優(yōu)化
- 盡量使用SSD;
- 定時(shí)清理數(shù)據(jù)或者按數(shù)據(jù)的性質(zhì)分開(kāi)存放;
- 結(jié)果集處理,如:用setFetchSize控制jdbc每次從數(shù)據(jù)庫(kù)中返回多少數(shù)據(jù);