深入淺出 Java 虛擬機(六)大流量高併發下的調優

本文章爲《深入淺出 Java 虛擬機》系列課程學習筆記,侵刪。學習地址爲 深入淺出 Java 虛擬機

1 引言

垃圾回收器一般使用默認參數,就可以比較好的運行。但如果用錯了某些參數,那麼後果可能會比較嚴重。

如果你的應用程序目前已經滿足了需求,那建議不要再隨便動這些參數了。另外,優化代碼獲得的性能提升,遠遠大於參數調整所獲得的性能提升,不要純粹爲了調參數而走了彎路。

那麼,GC 優化的目標是什麼呢?其實可以總結爲以下三點:

  1. 系統容量
  2. 延遲
  3. 吞吐量

2 系統容量

假如你的內存是無限大的,那麼無論是存活對象,還是垃圾對象,都不需要額外的計算和回收,你只需要往裏放就可以了。這樣,就沒有什麼吞吐量和延遲的概念了。

但其實,越是資源限制比較嚴格的系統,對它的優化就會越明顯。通常在一個資源相對寬鬆的環境下優化的參數,平移到另外一個限制資源的環境下,並不是最優解。

3 吞吐量、延遲

吞吐量大不代表響應能力高,吞吐量一般這麼描述:在一個時間段內完成了多少個事務操作;在一個小時之內完成了多少批量操作。

響應能力是以最大的延遲時間來判斷的,比如:一個桌面按鈕對一個觸發事件響應有多快;需要多長時間返回一個網頁;查詢一行 SQL 需要多長時間,等等。

這兩個目標,在有限的資源下,通常不能夠同時達到,我們需要做一些權衡。

4 如何選擇垃圾回收器

  1. 如果你的堆大小不是很大(比如 100MB),選擇串行收集器一般是效率最高的。參數:-XX:+UseSerialGC
  2. 如果你的應用運行在單核的機器上,或者你的虛擬機核數只有 1C,選擇串行收集器依然是合適的,這時候啓用一些並行收集器沒有任何收益。參數:-XX:+UseSerialGC
  3. 如果你的應用是“吞吐量”優先的,並且對較長時間的停頓沒有什麼特別的要求。選擇並行收集器是比較好的。參數:-XX:+UseParallelGC
  4. 如果你的應用對響應時間要求較高,想要較少的停頓。甚至1秒的停頓都會引起大量的請求失敗,那麼選擇 G1、ZGC、CMS 都是合理的。雖然這些收集器的 GC 停頓通常都比較短,但它需要一些額外的資源去處理這些工作,通常吞吐量會低一些。參數:-XX:+UseConcMarkSweepGC、-XX:+UseG1GC、-XX:+UseZGC 等

從上面這些出發點來看,我們平常的 Web 服務器,都是對響應性要求非常高的。選擇性其實就集中在 CMS、G1、ZGC 上。

而對於某些定時任務,使用並行收集器,是一個比較好的選擇。

5 大流量應用

這是一類對延遲非常敏感的系統。吞吐量一般可以通過堆機器解決。

假如某個接口一天有 10 億次請求,每秒的峯值大概也就 5~6 w/秒,雖然不算是很大,但也不算小。

一般達到這種量級的系統,承接請求的都不是一臺服務器,接口都會要求快速響應,一般不會超過 100ms。

這種系統,一般都是社交、電商、遊戲、支付場景等,要求的是短、平、快。長時間停頓會堆積海量的請求,所以在停頓發生的時候,表現會特別明顯。我們要考量這些系統,有很多指標。

  1. 每秒處理的事務數量(TPS)
  2. 平均響應時間(AVG)
  3. TP 值,比如 TP90 代表有 90% 的請求響應時間小於 x 毫秒

其中我們重點了解一下 TP 值,它最能代表系統中到底有多少長尾請求,這部分請求才是影響系統穩定性的元兇。大多數情況下,GC 增加,長尾請求的數量也會增加。

我們的目標,就是減少這些停頓。本文章假定使用的是 CMS 垃圾回收器。

6 調優

假設每次請求的大小有 20KB,這個接口每天有 10 億次請求,那麼一天的流量就有 18TB 之巨。假如高峯請求 6w/s,我們部署了 10 臺機器,那麼每個 JVM 的流量就可以達到 120MB/s,這個速度算是比較快的了。

假設我們的機器是 4C 8GB 的,分配給了 JVM 10248GB/32= 5460MB 的空間。那麼年輕代大小就有 5460MB/3=1820MB。進而可以推斷出,Eden 區的大小約 1456MB,那麼大約只需要 12 秒,就會發生一次 Minor GC。不僅如此,每隔半個小時,會發生一次 Major GC。

不管是年輕代還是老年代,這個 GC 頻率都有點頻繁了。

可以算一下我們的 Survivor 區大小,大約是 182MB 左右,如果稍微有點流量偏移,或者流量突增,再或者和其他接口共用了 JVM,那麼這個 Survivor 區就已經裝不下 Minor GC 後的內容了。總有一部分超出的容量,需要老年代來補齊。這些垃圾信息就要保存更長時間,直到老年代空間不足。
在這裏插入圖片描述
我們發現,用戶請求完這些信息之後,很快它們就會變成垃圾。所以每次 MinorGC 之後,剩下的對象都很少。

也就是說,我們的流量雖然很多,但大多數都在年輕代就銷燬了。如果我們加大年輕代的大小,由於 GC 的時間受到活躍對象數的影響,回收時間並不會增加太多。

如果我們把一半空間給年輕代。也就是下面的配置:

-XX:+UseConcMarkSweepGC -Xmx5460M -Xms5460M -Xmn2730M

重新估算一下,發現 Minor GC 的間隔,由 12 秒提高到了 18 秒。

Minor GC 有所改善,但是並沒有顯著的提升。相比較而言,Major GC 的間隔卻增加到了 3 小時,是一個非常大的性能優化。這就是在容量限制下的初步調優方案。

但是這有一個問題,由於每秒的請求都非常大,如果應用重啓或者更新,流量瞬間打過來,JVM 還沒預熱完畢,這時候就會有大量的用戶請求超時、失敗。

爲了解決這種問題,通常會逐步的把新發布的機器進行放量預熱。比如第一秒 100 請求,第二秒 200 請求,第三秒 5000 請求。大型的應用都會有這個預熱過程。

在這裏插入圖片描述
如圖所示,負載均衡器負責服務的放量,server4 將在 6 秒之後流量正常流通。但是奇怪的是,每次重啓大約 20 多秒以後,就會發生一次詭異的 Full GC。

注意是 Full GC,而不是老年代的 Major GC,也不是年輕代的 Minor GC。事實上,經過觀察,此時年輕代和老年代的空間還有很大一部分,那 Full GC 是怎麼產生的呢?

一般,Full GC 都是在老年代空間不足的時候執行。但不要忘了,我們還有一個區域叫作 Metaspace,它的容量是沒有上限的,但是每當它擴容時,就會發生 Full GC。

事實上,MetaspaceSize 的大小大約是 20MB。這個初始值太小了。現在很多類庫,包括 Spring,都會大量生成一些動態類,20MB 很容易就超了,我們可以試着調大這個數值。按照經驗,一般調整成 256MB 就足夠了。同時,爲了避免無限制使用造成操作系統內存溢出,我們同時設置它的上限。

經觀察,啓動後停頓消失。

這種方式通常是行之有效的,但也可以通過擴容機器內存或者擴容機器數量的辦法,顯著地降低 GC 頻率。這些都是在估算容量後的優化手段。

7 總結

其實,如果沒有明顯的內存泄漏問題和嚴重的性能問題,專門調優一些 JVM 參數是非常沒有必要的,優化空間也比較小。

所以,我們一般優化的思路有一個重要的順序:

  1. 程序優化,效果通常非常大
  2. 擴容,能用錢解決的問題不算問題
  3. 參數調優,在成本、吞吐量、延遲之間找一個平衡點

我們的業務場景是高併發的。對象誕生的快,死亡的也快,對年輕代的利用直接影響了整個堆的垃圾收集。

  1. 足夠大的年輕代,會增加系統的吞吐,但不會增加 GC 的負擔
  2. 容量足夠的 Survivor 區,能夠讓對象儘可能的留在年輕代,減少對象的晉升,進而減少 Major GC

我們還看到了一個元空間引起的 Full GC 的過程,這在高併發的場景下影響會格外突出,尤其是對於使用了大量動態類的應用來說。通過調大它的初始值,可以解決這個問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章