大型跨境電商JVM調優經歷

scholers發表於2018-05-26



前提:
某大型跨境電商業務發展非常快,線上機器擴容也很頻繁,但是對於線上機器的執行情況,特別是jvm記憶體的情況,一直沒有一個統一的標準來給到各個應用服務的owner。經過618大促之後,和運維的同學討論了下,希望將線上伺服器的jvm引數標準化,可以以一個統一的方式給到各個應用,提升線上伺服器的穩定性,同時減少大家都去調整jvm引數的時間。
參考了之前在淘寶天貓工作的公司的經歷:經過大家討論,根據jdk的版本以及線上機器配置,確定了一個推薦的預設jvm模版:

最終推薦的jvm模版:
jdk版本 機器配置 建議jvm引數 備註
jdk1.7 6V8G -server -Xms4g -Xmx4g -Xmn2g -Xss768k -XX:PermSize=512m -XX:MaxPermSize=512m -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSClassUnloadingEnabled -XX:+DisableExplicitGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=68 -verbose:gc -XX:+PrintGCDetails -Xloggc:{CATALINA_BASE}/logs/gc.log -XX:+PrintGCDateStamps -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath={CATALINA_BASE}/logs 前臺
jdk1.7 8V8G -server -Xms4g -Xmx4g -Xmn2g -Xss768k -XX:PermSize=512m -XX:MaxPermSize=512m -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSClassUnloadingEnabled -XX:+DisableExplicitGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=68 -verbose:gc -XX:+PrintGCDetails -Xloggc:{CATALINA_BASE}/logs/gc.log -XX:+PrintGCDateStamps -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath={CATALINA_BASE}/logs 前臺
jdk1.7 4V8G -server -Xms4g -Xmx4g -Xmn2g -Xss768k -XX:PermSize=512m -XX:MaxPermSize=512m -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSClassUnloadingEnabled -XX:+DisableExplicitGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=68 -verbose:gc -XX:+PrintGCDetails -Xloggc:{CATALINA_BASE}/logs/gc.log -XX:+PrintGCDateStamps -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath={CATALINA_BASE}/logs 前臺
jdk1.7 6V8G -server -Xms4g -Xmx4g -XX:MaxPermSize=512m \
-verbose:gc -XX:+PrintGCDetails -Xloggc{CATALINA_BASE}/logs/gc.log -XX:+PrintGCTimeStamps \ 後臺


某網際網路(bat)公司的推薦配置:




配置說明:
1. 堆設定
o -Xms:初始堆大小
o -Xmx:最大堆大小
o -XX:NewSize=n:設定年輕代大小
o -XX:NewRatio=n:設定年輕代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代佔整個年輕代年老代和的1/4
o -XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區佔整個年輕代的1/5
o -XX:MaxPermSize=n:設定持久代大小
2. 收集器設定
o -XX:+UseSerialGC:設定序列收集器
o -XX:+UseParallelGC:設定並行收集器
o -XX:+UseParalledlOldGC:設定並行年老代收集器
o -XX:+UseConcMarkSweepGC:設定併發收集器
3. 垃圾回收統計資訊
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
"
4. 並行收集器設定
-XX:ParallelGCThreads=n:設定並行收集器收集時使用的CPU數。並行收集執行緒數。
-XX:MaxGCPauseMillis=n:設定並行收集最大暫停時間
-XX:GCTimeRatio=n:設定垃圾回收時間佔程式執行時間的百分比。公式為1/(1+n)
5. 併發收集器設定
-XX:+CMSIncrementalMode:設定為增量模式。適用於單CPU情況。
-XX:ParallelGCThreads=n:設定併發收集器年輕代收集方式為並行收集時,使用的CPU數。並行收集執行緒數。
(4)

引數解釋:

-Xms3072m -Xmx3072m
針對JVM堆的設定,通過-Xms -Xmx限定其最小、最大值
-Xmn1024m設定年輕代大小為1024m
整個JVM記憶體大小=年輕代大小 + 年老代大小 + 持久代大小(perm)。

-Xss768k 設定每個執行緒的堆疊大小。JDK5.0以後每個執行緒堆疊大小為1M,以前每個執行緒堆疊大小為256K。更具應用的執行緒所需記憶體大小進行調整。在相同實體記憶體下,減小這個值能生成更多的執行緒。但是作業系統對一個程式內的執行緒數還是有限制的,不能無限生成,經驗值在3000~5000左右。

-XX:PermSize=512m -XX:MaxPermSize=512m
持久代一般固定大小為64m,所以增大年輕代後,將會減小年老代大小。此值對系統效能影響較大,Sun官方推薦配置為整個堆的3/8。
設定非堆記憶體初始值,預設是實體記憶體的1/64;由XX:MaxPermSize設定最大非堆記憶體的大小,預設是實體記憶體的1/4

-XX:+UseConcMarkSweepGC
CMS收集器也被稱為短暫停頓併發收集器。它是對年老代進行垃圾收集的。CMS收集器通過多執行緒併發進行垃圾回收,儘量減少垃圾收集造成的停頓。CMS收集器對年輕代進行垃圾回收使用的演算法和Parallel收集器一樣。這個垃圾收集器適用於不能忍受長時間停頓要求快速響應的應用。

-XX:+UseParNewGC對年輕代採用多執行緒並行回收,這樣收得快;

-XX:+CMSClassUnloadingEnabled
如果你啟用了CMSClassUnloadingEnabled ,垃圾回收會清理持久代,移除不再使用的classes。這個引數只有在 UseConcMarkSweepGC 也啟用的情況下才有用。

-XX:+DisableExplicitGC禁止System.gc(),免得程式設計師誤呼叫gc方法影響效能;

-XX:+UseCMSInitiatingOccupancyOnly
標誌來命令JVM不基於執行時收集的資料來啟動CMS垃圾收集週期。而是,當該標誌被開啟時,JVM通過CMSInitiatingOccupancyFraction的值進行每一次CMS收集,而不僅僅是第一次。然而,請記住大多數情況下,JVM比我們自己能作出更好的垃圾收集決策。因此,只有當我們充足的理由(比如測試)並且對應用程式產生的物件的生命週期有深刻的認知時,才應該使用該標誌。

-XX:CMSInitiatingOccupancyFraction=68
預設CMS是在tenured generation(年老代)佔滿68%的時候開始進行CMS收集,如果你的年老代增長不是那麼快,並且希望降低CMS次數的話,可以適當調高此值;

-XX:+UseParNewGC:對年輕代採用多執行緒並行回收,這樣收得快;


-XX:HeapDumpPath
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:/usr/aaa/dump/heap_trace.txt
上面的的引數打Heap Dump資訊

" -XX:+HeapDumpOnOutOfMemoryError
此引數可以控制OutOfMemoryError時列印堆的資訊


大家可能注意到了,這裡推薦採用cms方式進行垃圾回收;
CMS是一種以獲取最短回收停頓時間為目標的收集器,可以有效減少伺服器停頓的時間;
CMS的GC執行緒對CPU的佔用率會比較高,但在多核的伺服器上還是展現了優越的特性,目前也被部署在國內的各大電商網站上。所以這裡強烈推薦!

cms的概念:
CMS收集器也被稱為短暫停頓併發收集器。它是對年老代進行垃圾收集的。CMS收集器通過多執行緒併發進行垃圾回收,儘量減少垃圾收集造成的停頓。CMS收集器對年輕代進行垃圾回收使用的演算法和Parallel收集器一樣。這個垃圾收集器適用於不能忍受長時間停頓要求快速響應的應用。CMS採用了多種方式儘可能降低GC的暫停時間,減少使用者程式停頓。停頓時間降低的同時犧牲了CPU吞吐量 。這是在停頓時間和效能間做出的取捨,可以簡單理解為"空間(效能)"換時間。

調整的節奏:
由於怕影響線上應用,所以調整的步驟分三步:
第一步:部分影響少量機器試點,對比未調整的機器,觀察調整後的結果;
第二步:調整部分應用的引數,進行壓測,觀察高併發壓測之後的效果;
第三步:調整部分核心應用的jvm引數,通過818大促來實際檢驗效果;
目前818大促已經結果。正好做一個個總結。

一:長期表現,
第一個變化:fgc的次數減少,減少了大概一倍以上;
mobile工程,調整前基本上一天1-2輛次,調整後基本上就是2-3天一次:



online(另外一個工程):可以明顯看到fgc的統計頻率少了很多;





第二個變化:fgc的時間減少








原來一次fgc要將近500ms,現在只要100ms不到了。
也證明了cms最大的好處就是減少fgc的停頓時間。

二:壓測及大促表現
fgc的時間基本上是大大縮短,yanggc的時間變長,次數變化不大;
資料來源:測試團隊的壓測總結

xxxx-online4.server.org
CMS

xxxx-online1.server.org
CMS

xxxx-online34.server.org
預設垃圾收集器

說明

fullgc次數

1

1

1

fullgc總時間

343

250

1219

預設垃圾收集器/CMS fullgc 時間

3.55

4.88

CMS fullgc時間比預設垃圾收集器時間明顯要少

fullgc時間點

2:48:36

3:14:36

5:30:36

fullgc時使用率CPU%

40%

10%

16%

fullgc時的load Average

1.19

0.49

1.21

younggc總次數

1094

1098

1078

younggc總時間

44093

44632

30387

younggc平均時間

40.30

40.65

28.19

younggc最大時間

1332

1268

928

CMS/預設垃圾收集器(younggc總時間)

1.45

1.47

CMS younggc時間比預設垃圾收集器耗時

CMS/預設垃圾收集器(younggc平均時間)

1.43

1.44

CMS younggc時間比預設垃圾收集器耗時

CMS/預設垃圾收集器(younggc最大時間)

1.44

1.37

CMS younggc時間比預設垃圾收集器最差情況要差

<!--EndFragment-->

三:關於哨兵上統計full gc的次數的解釋,哨兵上
我們可以安全的說:
1. Full GC == Major GC指的是對老年代/永久代的stop the world的GC
2. Full GC的次數 = 老年代GC時 stop the world的次數
3. Full GC的時間 = 老年代GC時 stop the world的總時間
4. CMS 不等於Full GC,我們可以看到CMS分為多個階段,只有stop the world的階段被計算到了Full GC的次數和時間,而和業務執行緒併發的GC的次數和時間則不被認為是Full GC

Full GC的次數說的是stop the world的次數,所以一次CMS至少會讓Full GC的次數+2,因為CMS Initial mark和remark都會stop the world,記做2次。而CMS可能失敗再引發一次Full GC
如果CMS併發GC過程中出現了concurrent mode failure的話那麼接下來就會做一次mark-sweep-compact的full GC,這個是完全stop-the-world的。

正是這個特徵,使得CMS的每個併發GC週期總共會更新full GC計數器兩次,initial mark與final re-mark各一次;如果出現concurrent mode failure,則接下來的full GC自己算一次。

四:遇到的幾個問題:
問題一:堆疊溢位;
-Xss256k這個引數調整了,遠濤反饋可能會影響trace的呼叫。 報如下錯誤:
Java.lang.StackOverflowError
at net.sf.jsqlparser.util.deparser.ExpressionDeParser.visitBinaryExpression(ExpressionDeParser.java:278)
at net.sf.jsqlparser.util.deparser.ExpressionDeParser.visit(ExpressionDeParser.java:246)
at net.sf.jsqlparser.expression.operators.conditional.OrExpression.accept(OrExpression.java:37)
at net.sf.jsqlparser.util.deparser.ExpressionDeParser.visitBinaryExpression(ExpressionDeParser.java:278)
at net.sf.jsqlparser.util.deparser.ExpressionDeParser.visit(ExpressionDeParser.java:246)
因為這個引數是設定每個執行緒的堆疊大小。JDK5.0以後每個執行緒堆疊大小為1M,以前每個執行緒堆疊大小為256K。在相同實體記憶體下,減小這個值能生成更多的執行緒。
所以今天去掉某臺inventory機器的-Xss256k引數,看一下是不是這個導致的

問題二:初始化標記階段耗時過長:
一般的建議是cms階段兩次STW的時間不超過200ms,如果是CMS Initial mark階段導致的時間過長:
在初始化標記階段(CMS Initial mark),為了最大限度地減少STW的時間開銷,我們可以使用:
-XX:+CMSParallelInitialMarkEnabled
開啟初始標記過程中的並行化,進一步提升初始化標記效率;
問題三:remark階段stw的時間過長
如下圖:




可以採用的方式是:
在CMS GC前啟動一次ygc,目的在於減少old gen對ygc gen的引用,降低remark時的開銷-----一般CMS的GC耗時 80%都在remark階段
-XX:+CMSScavengeBeforeRemark
jmap分析:




問題四:nio框架佔用DirectMemory導致的OutOfMemoryError
處理方式:使用XX:+DisableExplicitGC
增加DirectMemory的大小;
1、DirectMemory不屬於java堆記憶體、分配記憶體其實是呼叫作業系統的Os:malloc()函式。
2、容量可通過-XX:MaxDirectMemorySize指定,如果不指定,則預設與Java堆的最大值(-Xmx指定)一樣。注意 ibm jvm預設Direct Memory與-Xmx無直接關係。
3、Direct Memory 記憶體的使用避免Java堆和Native堆中來回複製資料。從某些場景中提高效能。
4、直接ByteBuffer物件會自動清理本機緩衝區,但這個過程只能作為Java堆GC的一部分來執行,因此它們不會自動響應施加在本機堆上的壓力。
5、GC僅在Java堆被填滿,以至於無法為堆分配請求提供服務時發生,或者在Java應用程式中顯示呼叫System.gc()函式來釋放記憶體(一些NIO框架就是用這個方法釋放佔用的DirectMemory)。
6、該區域使用不合理,也是會引起OutOfMemoryError。
7、在需要頻繁建立Buffer的場合,由於建立和銷燬DirectBuffer的代價比較高昂,是不宜使用DirectBuffer的,但是如果能將DirectBuffer進行復用,那麼 ,在讀寫頻繁的情況下,它完全可以大幅改善效能。(對DirectBuffer的讀寫比普通Buffer快,但是對他的建立和銷燬比普通Buffer慢)。








相關文章