億級(jí)流量電商系統(tǒng)JVM模型參數(shù)預(yù)估方案,在原來(lái)的基礎(chǔ)上采用ParNew+CMS垃圾收集器
一、億級(jí)流量分析及jvm參數(shù)設(shè)置
1. 需求分析
大促在即,擁有億級(jí)流量的電商平臺(tái)開發(fā)了一個(gè)訂單系統(tǒng),我們應(yīng)該如何來(lái)預(yù)估其并發(fā)量?如何根據(jù)并發(fā)量來(lái)合理配置JVM參數(shù)呢?
假設(shè),現(xiàn)在有一個(gè)場(chǎng)景,一個(gè)電商平臺(tái),比如京東,需要承擔(dān)每天上億的流量。現(xiàn)在開發(fā)了一個(gè)訂單系統(tǒng),那么這個(gè)訂單系統(tǒng)每秒的并發(fā)量是多少呢?我們應(yīng)該如何分配其內(nèi)存空間呢?先來(lái)分析一下

每日億級(jí)流量,平均一個(gè)用戶點(diǎn)擊量在20-30左右,通過(guò)這個(gè)計(jì)算出日活用戶數(shù)約1億/20=500萬(wàn), 看的人多,買的人少,通常下單率不超過(guò)10%,我們按照留存率10%來(lái)計(jì)算,日均訂單約50萬(wàn)單。這是分兩種情況:
- 一種是普通流量,非特殊節(jié)假日,通常早上、中午、晚上非工作時(shí)間有1個(gè)小時(shí)的時(shí)間集中購(gòu)買。我們按照早上1小時(shí),中午1小時(shí),晚上1小時(shí)來(lái)計(jì)算,也就是3小時(shí)。這樣平均到每秒就是50萬(wàn)/3/3600=46, 也就是及時(shí)并發(fā),通常我們的服務(wù)都是一個(gè)集群,有好幾臺(tái)服務(wù)器承受著幾十并發(fā),應(yīng)該不成問(wèn)題。
- 另一種是大促流量,比如雙十一,基本流量都集中在雙十一當(dāng)天的投幾分鐘。這時(shí)每秒的并發(fā)量大概在50萬(wàn)/10/60=866,平均每秒并發(fā)量不到1000。這時(shí)服務(wù)集群有3臺(tái)服務(wù)器,沒太服務(wù)器承受的壓力是400單/s。
2. 常規(guī)方案及問(wèn)題暴露
對(duì)于這每秒400但會(huì)產(chǎn)生多大的對(duì)象呢?

我們假設(shè)訂單對(duì)象的大小是1kb,實(shí)際上訂單對(duì)象的大小和訂單對(duì)象中的字段有關(guān)系,我們假設(shè)是1kb。每秒400單,也就是會(huì)產(chǎn)生400kb的訂單對(duì)象。下單還涉及到其他對(duì)象,比如庫(kù)存,優(yōu)惠券,積分等等,我們將對(duì)象擴(kuò)大20倍, 大約是(400kb*20)/秒. 可能同時(shí)還有其他操作,比如查詢訂單的操作,我們?cè)僦v其擴(kuò)大10倍,大約是80M,也就是每秒產(chǎn)生約80M的對(duì)象,這些對(duì)象在1s后都會(huì)變?yōu)槔?/p>
對(duì)于一臺(tái)4核8G的服務(wù)器來(lái)說(shuō),通常我們不設(shè)置JVM參數(shù),也可能會(huì)根據(jù)物理機(jī)的8G內(nèi)存來(lái)設(shè)置JVM參數(shù)。如果根據(jù)JVM參數(shù)來(lái)設(shè)置參數(shù)如何設(shè)置呢?
之前說(shuō)過(guò)開啟逃逸分析會(huì)將對(duì)象分配到棧上,我們這里計(jì)算分析的時(shí)候暫且忽略逃逸分析分配到棧上的對(duì)象,因?yàn)檫@部分對(duì)象相對(duì)來(lái)說(shuō)比較少。下面我們來(lái)驗(yàn)證上面的預(yù)估算法是否準(zhǔn)確,會(huì)有什么樣的問(wèn)題呢?
物理機(jī)有8G,分給os操作系統(tǒng)3G,分給JVM5G,然后JVM中給堆分配3G,元數(shù)據(jù)空間分配512M,線程棧分配1M等等。這是估算,不夠精細(xì),到底分配這么多空間夠不夠呢,會(huì)不會(huì)浪費(fèi)呢?會(huì)產(chǎn)生什么樣的問(wèn)題呢?
設(shè)置jvm參數(shù)大致如下:
-Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M
這樣設(shè)置到底行不行呢?有沒有問(wèn)題呢?我們來(lái)看看運(yùn)行時(shí)數(shù)據(jù)區(qū):

根據(jù)計(jì)算
- 整個(gè)堆空間3G
- Eden區(qū)800M
- s1/s2各100M
- 方法區(qū)512M
- 一個(gè)線程1M
按照這個(gè)模型來(lái)分析,得到如下結(jié)果:

- 大促期間1s產(chǎn)生80M的對(duì)象數(shù)據(jù)。我們知道對(duì)象數(shù)據(jù)都是放在Eden園區(qū),Eden園區(qū)一共800M,那么大約10s就放滿了,放滿了就會(huì)觸發(fā)Minor GC
- 觸發(fā)Minor GC的期間,會(huì)Stop The World暫停業(yè)務(wù)線程。在第10s觸發(fā)MinorGC的時(shí)候,前9s的720M數(shù)據(jù)都已經(jīng)變成垃圾了,會(huì)被回收掉,最后1s的80M數(shù)據(jù)由于還有對(duì)象引用,只是暫停了業(yè)務(wù)線程,因此不是垃圾,不能被回收。會(huì)被放入S1區(qū)。
- 在Survivor區(qū)有一個(gè)對(duì)象動(dòng)態(tài)年齡判斷機(jī)制。什么是對(duì)象動(dòng)態(tài)年齡判斷機(jī)制呢?
當(dāng)前放對(duì)象的Survivor區(qū)域里(其中一塊區(qū)域,放對(duì)象的那塊s區(qū)),一批對(duì)象的總大小大于這塊Survivor區(qū)域內(nèi)存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此時(shí)大于等于這批對(duì)象年齡最大值的對(duì)象,就可以直接進(jìn)入老年代了,
例如:Survivor區(qū)域里現(xiàn)在有一批對(duì)象,年齡1+年齡2+年齡n的多個(gè)年齡對(duì)象總和超過(guò)了Survivor區(qū)域的50%,此時(shí)就會(huì)把年齡n(含)以上的對(duì)象都放入老年代。這個(gè)規(guī)則其實(shí)是希望那些可能是長(zhǎng)期存活的對(duì)象,盡早進(jìn)入老年代。
對(duì)象動(dòng)態(tài)年齡判斷機(jī)制一般是在minor gc之后觸發(fā)的。
也就是說(shuō)當(dāng)在Survivor區(qū)經(jīng)過(guò)幾代的回收以后,如果對(duì)象總和大于Survivor區(qū)域的一半,則會(huì)直接放入到老年代。Survivor是100M,第10s的對(duì)象是80M,大于100M,會(huì)直接將這個(gè)對(duì)象放入到老年代。

- 老年代一共有2G空間,2G空間執(zhí)行多少次會(huì)滿呢?2G/80M=25次,也就是發(fā)生25次(25秒)Minor GC就會(huì)觸發(fā)一次Full GC。這個(gè)頻率就太高了,通常應(yīng)該要很少觸發(fā)Full GC,起碼也得1個(gè)小時(shí)觸發(fā)一次。而觸發(fā)的原因是因?yàn)槔鴮?duì)象(這些對(duì)象1s后都變成垃圾了),這樣肯定是不行的。我們需要優(yōu)化JVM參數(shù)。
3. JVM優(yōu)化
有問(wèn)題有就解決問(wèn)題。問(wèn)題的根本原因是老年代發(fā)生了Full GC,為什么會(huì)發(fā)生Full GC呢?
之所以80M對(duì)象會(huì)放到了老年代是因?yàn)槊棵氘a(chǎn)生的數(shù)據(jù) 大于 Survivor區(qū)空間的一半。所以,我們可以調(diào)整Survivor區(qū)大小。通常我們不會(huì)修改默認(rèn)的Eden:S1:S2的比例,所以,我們可以考慮從整體擴(kuò)大新生代的內(nèi)存空間。假設(shè)我們擴(kuò)大到2G,讓老年代是1G。

這時(shí)會(huì)怎么樣呢?
- Young區(qū)占2G,Eden區(qū)有1.6G, S1、S2各有200M。
這時(shí)在分析:

- Eden區(qū)有1.6G,每秒產(chǎn)生80M的對(duì)象放到Eden區(qū),大約1.6G/80=20s放滿。
- 放滿以后觸發(fā)Minor GC, 此時(shí)前19s的對(duì)象都已經(jīng)成為垃圾被回收,第20s的對(duì)象被轉(zhuǎn)移到S1區(qū)。
- 此時(shí),S1區(qū)有200M,80<S1區(qū)空間的一半,所以不會(huì)轉(zhuǎn)移到老年代。這樣第一次GC結(jié)束
- 又過(guò)了20s,進(jìn)行第二次Minor GC,這次Eden區(qū)又產(chǎn)生了1.52G的垃圾被回收,之前在S1區(qū)的80M對(duì)象也已經(jīng)變成垃圾被回收。新的80M對(duì)象被放入到S2區(qū)。沒有進(jìn)入到老年代。
- 以此類推,第三次,第四次,垃圾對(duì)象不會(huì)再進(jìn)入老年代,因此也不會(huì)在發(fā)生Full GC.
由此分析,大大降低了Full GC發(fā)生的頻率。
最終參數(shù)設(shè)置:
-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M
為了更清晰的看到效果,可以打印GC詳細(xì)日志
-XX:+PrintGCDetails
4. 總結(jié)
通過(guò)上面的數(shù)據(jù)分析,我們要養(yǎng)成一個(gè)習(xí)慣,做任何事情都是要有理有據(jù),不能是拍腦袋就說(shuō)出來(lái)的。一定要能夠經(jīng)得起驗(yàn)證的。
二、億級(jí)流量jvm參數(shù)優(yōu)化--使用parNew和CMS垃圾收集器
1. 需求分析
上面的參數(shù)設(shè)置,幫我們解決了多次觸發(fā)Full GC的問(wèn)題,通過(guò)調(diào)整參數(shù)以后,我們看出在預(yù)期正常情況下,基本不會(huì)觸發(fā)Full GC。但如果有意外情況呢?比如,我們的一臺(tái)服務(wù)器能夠承受的最大并發(fā)量是400/s,但如果在秒殺的時(shí)候,并發(fā)量超過(guò)了這種情況是在不發(fā)生意外的情況下。假如并發(fā)流量達(dá)到1000,內(nèi)存模型是怎么樣的呢?

根據(jù)這個(gè)估算模型,正常情況下訂單系統(tǒng)可以承接的訂單并發(fā)量是400單/s,但遇到某一個(gè)大促活動(dòng),很可能并發(fā)量沖到700單/s, 1000單/s,這是一秒產(chǎn)生的垃圾就不是60M了,可能是120M,甚至更多。根據(jù)之前的分析,這時(shí)又會(huì)頻繁的觸發(fā)Full GC了。當(dāng)然了,我們有很多辦法來(lái)控制并發(fā)量,比如限流、擴(kuò)容。但這里我們從JVM的角度來(lái)分析,如何處理這個(gè)問(wèn)題。
正常情況我們的jvm參數(shù)是如下設(shè)置:
‐Xms3072M ‐Xmx3072M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8
經(jīng)過(guò)上面的分析,這樣設(shè)置可能會(huì)由于動(dòng)態(tài)對(duì)象年齡判斷原則導(dǎo)致頻繁full gc。于是我們?cè)O(shè)置如下JVM參數(shù),盡量避免觸發(fā)full GC
‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8
2. JVM優(yōu)化
這個(gè)原理在上面已經(jīng)說(shuō)過(guò)了,但是如果并發(fā)量從峰值400單/s,一下沖到700~1000單/s。這時(shí)候,很顯然,又會(huì)觸發(fā)Full GC了,因?yàn)閮?nèi)存對(duì)象從原來(lái)的80M,變成了160M甚至更多,Survior區(qū)200M空間,他的一半小于160M, 所以會(huì)直接放入到老年代。針對(duì)這個(gè)問(wèn)題,我們來(lái)做參數(shù)優(yōu)化。
優(yōu)化一:分代年齡從15變成5
系統(tǒng)默認(rèn)的分代年齡是15,也就是一個(gè)對(duì)象在Survivor兩個(gè)區(qū)輪回15次才會(huì)進(jìn)入到老年代。15次大概是多長(zhǎng)時(shí)間呢?我們來(lái)計(jì)算一下,按照參數(shù)來(lái)分析一下內(nèi)存模型,如下圖:
‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8

? 每秒鐘產(chǎn)生80M垃圾,放入到Eden區(qū),Eden區(qū)一共1.6G,預(yù)計(jì)20s放滿,觸發(fā)Minor GC, 然后大部分對(duì)象被回收,只有一小部分對(duì)象進(jìn)入到Survivor區(qū)。第二次回收的時(shí)候,上次進(jìn)入Survivor區(qū)的大部分對(duì)象被垃圾回收,另一部分進(jìn)入到另一個(gè)Survivor區(qū)。這些進(jìn)入到另一個(gè)Survivor的對(duì)象要經(jīng)歷15次Minor GC,也就是年齡是15的時(shí)候,被轉(zhuǎn)移到老年代,花費(fèi)大約20s*15約5分鐘的時(shí)間才能進(jìn)入到老年代。其實(shí)這些長(zhǎng)期存活的對(duì)象都是JAVA運(yùn)行或者spring運(yùn)行是的一些java.lang.String, java.util.Math, 和一些bean對(duì)象。既然這些對(duì)象本身是長(zhǎng)期存活的,那么我們就沒必要讓他經(jīng)歷那么多代才進(jìn)入到老年代。
? 我們完全可以將默認(rèn)的15歲改小一點(diǎn),比如改為5,那么意味著對(duì)象要經(jīng)過(guò)5次minor gc才會(huì)進(jìn)入老年代,如果經(jīng)歷5次Minor GC還沒有被回收,我們完全可以認(rèn)為她就是要長(zhǎng)期存活的對(duì)象了,將其移動(dòng)到老年代,而不是繼續(xù)一直占用survivor區(qū)空間。整個(gè)過(guò)程時(shí)間不到兩分鐘。
設(shè)置參數(shù)如下:
‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8
‐XX:MaxTenuringThreshold=5
優(yōu)化二:大對(duì)象直接進(jìn)入老年代
對(duì)于多大的對(duì)象直接進(jìn)入老年代合適呢?這個(gè)一般可以結(jié)合你自己系統(tǒng)看下有沒有什么大對(duì)象生成,預(yù)估下大對(duì)象的大小,一般來(lái)說(shuō)設(shè)置為1M就差不多了,很少有超過(guò)1M的大對(duì)象,這些對(duì)象一般就是你系統(tǒng)初始化分配的緩存對(duì)象,比如大的緩存List,Map之類的對(duì)象。 設(shè)置大對(duì)象直接進(jìn)入老年代使用的參數(shù):-XX:PretenureSizeThreshold
參數(shù)設(shè)置如下:
‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8
‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M
優(yōu)化三:替換垃圾收集器為ParNew + CMS
JDK8默認(rèn)使用的垃圾回收器是-XX:+UseParallelGC(年輕代)和-XX:+UseParallelOldGC(老年代),通常使用Parallel會(huì)有什么問(wèn)題呢?經(jīng)驗(yàn)告訴我們,當(dāng)系統(tǒng)內(nèi)存較大的時(shí)候(超過(guò)4G,經(jīng)驗(yàn)值),系統(tǒng)對(duì)停頓時(shí)間是比較敏感的。 通常大于4G內(nèi)存,我們可以采用ParNew + CMS垃圾收集器。可不可以使用G1收集器呢?G1收集器通常是內(nèi)存大于8G時(shí)使用的。 內(nèi)存小于8G時(shí),在jdk8中G1收集器的算法耗費(fèi)的內(nèi)存要比CMS多。所以這里我們替換垃圾收集器為ParNew + CMS。設(shè)置使用ParNew + CMS的參數(shù)是:-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
經(jīng)驗(yàn): 很多使用jdk8的公司都是用時(shí)ParNew + CMS垃圾回收
參數(shù)設(shè)置如下:
‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8
‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
替換成ParNew + CMS垃圾收集器能解決上面并發(fā)流量達(dá)到700~1000單/s的問(wèn)題么?我們來(lái)分析一下:
1) 當(dāng)并發(fā)流量導(dǎo)到700單/s的時(shí)候, 原來(lái)每秒產(chǎn)生80M垃圾,現(xiàn)在可能達(dá)到160M,那么年輕代Survivor放不下,會(huì)直接放入到老年代。
2)當(dāng)兵發(fā)流量大了的時(shí)候,本來(lái)系統(tǒng)能承受的是400單/s, 但是突增到500單/s的時(shí)候,原來(lái)每秒可以處理一個(gè)訂單,現(xiàn)在可能1秒處理不完了,要2秒甚至更多。那么就有可能在垃圾回收的時(shí)候,2s內(nèi)的對(duì)象的引用關(guān)系都還在,不能被回收,剛好又大于新生代一半的空間,也會(huì)被直接放入老年代。
3)經(jīng)過(guò)上面的優(yōu)化,發(fā)生一次Minor GC,大約要20s, 老年代有1G空間,1G/160M*20/60=2分鐘。2分鐘觸發(fā)一次GC,通常高峰流量也就半個(gè)小時(shí)左右。2分鐘觸發(fā)一次GC,這也不太合適。
優(yōu)化四:設(shè)置CMS收集器的參數(shù)
1) 避免并發(fā)失敗參數(shù)設(shè)置
在CMS收集器那塊我們說(shuō)過(guò),CMS正在收集垃圾但還沒有完成的時(shí)候,又產(chǎn)生了新的垃圾,導(dǎo)致再次觸發(fā)垃圾回收,這就發(fā)生死循環(huán)了,這就是concurrentmode failure并發(fā)失敗。為了避免并發(fā)失敗,這時(shí)會(huì)停止CMS垃圾回收的全部線程,進(jìn)入到Serial Old串行垃圾收集。串行速度是很慢的,嚴(yán)重影響用戶體驗(yàn)。我們盡量不要讓這種情況發(fā)生。因此,我們?cè)O(shè)置垃圾回收參數(shù):‐XX:CMSInitiatingOccupancyFraction,我們?cè)O(shè)置老年代達(dá)到一定比例比如80%就出發(fā)Full GC,留出足夠大的空間給大對(duì)象,這樣就不會(huì)觸發(fā)Serial Old了。
這個(gè)值默認(rèn)是92,也可以設(shè)置成80,但設(shè)置成80就表示,剩下20%的內(nèi)存空間正常情況下處于閑置了。
參數(shù)設(shè)置如下:
‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8
‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
‐XX:CMSInitiatingOccupancyFraction=92
2)壓縮整理參數(shù)設(shè)置
我們可以設(shè)置在發(fā)生Full GC之后進(jìn)行內(nèi)存空間的壓縮整理。這里涉及到兩個(gè)參數(shù),一個(gè)是開啟壓縮整理,另一個(gè)是觸發(fā)幾次Full GC整理一次內(nèi)存空間。
-XX:+UseCMSCompactAtFullCollection:FullGC之后做壓縮整理(減少碎片)
-XX:CMSFullGCsBeforeCompaction:多少次FullGC之后壓縮一次,默認(rèn)是0,代表每次FullGC后都會(huì)壓縮一次
這個(gè)參數(shù)是說(shuō)執(zhí)行多少次Full GC以后進(jìn)行一次壓縮。如果其值是3,則表示執(zhí)行3次Full GC,進(jìn)行一次壓縮整理。
在觸發(fā)了CMS垃圾回收之后,進(jìn)行內(nèi)存整理,也會(huì)對(duì)性能有一定的影響的。 因?yàn)樗矔?huì)STW。這個(gè)過(guò)程不會(huì)特別慢,這和剩余的對(duì)象有關(guān),剩余的對(duì)象少,效率就高。剩余的對(duì)象多,效率就低。因?yàn)樵谡淼倪^(guò)程中,對(duì)象的地址會(huì)發(fā)生變化。
對(duì)于我們上面的案例,我們可以設(shè)置每次垃圾回收后都進(jìn)行整理,為什么可以這么設(shè)置呢?因?yàn)槲覀僨ull GC發(fā)生的頻率很低。偶爾搞一次大促呢?也沒關(guān)系,大促的前面二三十分鐘流量最高,二三十分鐘觸發(fā)一次Full GC沒關(guān)系的,因?yàn)榇蟠倩窘Y(jié)束了。
如果系統(tǒng)壓力比較大,觸發(fā)Full GC很頻繁,這個(gè)參數(shù)就不要這么設(shè)置了。可以設(shè)置-XX:CMSFullGCsBeforeCompaction為3次,5次。
不做碎片整理可不可以呢?
最好不要,因?yàn)槿绻蛔鏊槠恚夏甏乃槠蜁?huì)越來(lái)越多,正常的大對(duì)象都放不下了。
參數(shù)設(shè)置如下:
‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8
‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
‐XX:CMSInitiatingOccupancyFraction=92 ‐XX:+UseCMSCompactAtFullCollection ‐XX:CMSFullGCsBeforeCompaction=0