神秘的偶發服務超時,原因可能是那些壞鄰居
1. 惡鄰A君
唯品會在服務化體系改造的初期,一個對延時敏感的應用,偶然會發生一些超時,事發當時zabbix分鐘級監控,dstat秒級監控的服務器指標都正常,應用,數據庫,緩存,網絡也正常,那這是為什么呢?
某天腦洞大開,把懷疑的目光投向了在后臺運行日志收集程序Flume,發現它的GC運行得比較狂野,于是對它的GC線程數做了限制:
- 修改前:15分鐘內, 大于30ms的業務調用173次, 大于50ms的23次
- 修改后: 246分鐘內,大于30ms的業務調用41次, 大于50ms的4次
2. 惡鄰B君
又過了若干個月,又有某些應用,又開始抽風。這次相對好查一些,因為我們新升級了服務器的監控系統,只要在兩臺機器上做一下對比測試就好了。 只花了一個晚上,基本就能驗明兇手了。
那這個新升級的監控系統,又是怎么影響到主應用的呢?找出它與應用有交互的部分,原來對于JVM的各種線程數信息,堆內存各代的信息,每拿一個數據都會啟動一次JMX Client,所以每分鐘都有一秒要連拿7個數據,啟動7個JMX Client。
改進方法很簡單,我們自己定制了一下JMX Client,將7個數據合并在一個命令里獲得,另外定制了一下JMX Client的JVM參數,將它啟動的動靜盡量減少。
3. 逆優化
可見,JVM是個運行服務端應用的好VM,但體量有點大。如果你只是想頻繁地運行一段Java寫的腳本,或者在跑一些輔助性的程序比如監控和日志收集,往常推薦的JVM參數也就不再合適里,需要進行逆優化才能做個安靜的好鄰居:
- 啟動快速,動靜小。
- 低成本,節約CPU、內存和線程。
- 低擾動,不干擾主應用的運行。
4. 從失敗的取經開始
***時間,覺得和JDK自帶的jmap,jstack們用一樣的參數就好了,多簡單。
在它們運行時,跑jps -v ,結果發現通通只有一個-Xms8m 。
還不死心,又去翻源碼,JDK7在 Makefile.launcher,JDK8在CompileLaunchers.gmk,結果發現全部8M,通通8M,再沒別的參數了。
有同學又從久遠的記憶中想起一個-client,感覺也是比較弱氣的選項,但在這個多核的64位Linux服務器上是根本無效的,一定是-server,必須是-server。
5. 逆優化的思路
JVM與上述訴求相沖的幾個地方:
- 各種吃內存
- 各種后臺線程
- JIT時CPU表現狂野
- GC時CPU表現狂野
- 那我們就從這幾個方面著手。
在開始折騰前,先準備好測試手段:
首先,給工具腳本配上GC 日志參數,在GC日志里就能看到實際啟動參數,GC紀錄,以及運行結束時內存各代的占用。
-Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime
其次,長期跑一個 pidstat -l 1| grep xxx ,緊密監控進程的CPU消耗。
***,jstack看線程。
6. 類的加載和編譯優化
6.1 -Xverify:none
來自優化Eclipse啟動速度的經驗,說關閉Java類加載驗證可以加快10% -15%的啟動速度,嗯,好,加。
6.2 設定編譯級別
JIT編譯之后的代碼比解釋執行字節碼更快,更省CPU。比如vjtools里的vjtop,沒編譯時每回運行要50%單核CPU,75ms執行完一個探測循環,而編譯后10%單核,15ms就能完工。
但編譯本身就需要CPU,也需要額外的編譯線程。
如果腳本只簡單的跑一次,比如vjtools里的vjmxcli,建議就不要進行JIT編譯了,編譯完了也用不上,直接解釋執行就好。禁止它:-Djava.compiler=NONE
如果腳本用于密集計算,比如vjtools里的vjmap,則建議打開多層編譯,一開始就對運行到的方法進行靜態編譯,不用等方法被調用1萬次。多層編譯在JDK8默認打開,顯式打開:-XX:+TieredCompilation。
但打開多層編譯也會導致程序運行初期有較多的編譯任務,吃比較多的CPU,可以顯式關掉多層編譯 -XX:-TieredCompilation來對比一下,綜合其帶來的性能提升,腳本的運行時間的長短,以及額外的CPU支出來綜合評價。
6.3 編譯線程的設定
在24核服務器上,默認有4條C1編譯線程,8條C2編譯線程(多層編譯下),可以把它設到最小的-XX:CICompilerCount=2。
6.4 未來黑科技-AOT
JIT真的不適合腳本,還是預先把代碼編譯(Ahead-of-Time,AOT) 更好。 JDK9里有一個Hotspot編譯器組搞的試驗性的jaotc,另一個選擇是GraalVM全家桶里帶的SubstrateVM,支持JDK8。
看各位大大炫,但我還沒玩過。
7. GC 優化
腳本們一般不介意GC延時,建議使用吞度量最的串行收集算法 -XX:+UseSerialGC,避免了其他GC算法所需的大量GC線程,更絕對保證了自己GC時不會影響到主應用。
如果依然想使用并行算法,就一定要設置GC線程數,在24核機器上YGC和CMS GC的線程數默認分別是18和5,為了避免成為惡鄰A君。可設為:
-XX:ParallelGCThreads=4 -XX:ConcGCThreads=2
8. 內存優化
- 首先,JVM的堆內存
- 默認的JVM初始內存大小,在大內存的服務器上會比較大,必須設置。
- -Xms 與 -Xmx 不等時, 自動擴張并沒有想象中那么智能和合理。
- 新生代默認只有1/3堆大小,而在腳本看來新生代才是大頭。
建議根據GC日志的結果,完整設置-Xms 和 -Xmx,并用-Xmn(新生代占大頭) 或-XX:NewRatio=1(一半半) 來設置新生代大小。
其次,每條線程的內存,從默認1M回到256k: -xss256k
其他***代,CodeCache的初始值還算合理,沒看到特別浪費的情況不用管。