從CMS到G1:LinkedIn個人主頁調優實戰

歡迎關注筆者的公眾號:【阿飛的博客】,首發都在這里!!!

LinkedIn中的個人主頁是訪問量最多的頁面之一,它允許其他人訪問你的個人主頁,從而了解你的專業技能,經驗和興趣等:

LinkedIn個人主頁

所以,確保用戶訪問主頁時以最快的速度返回是非常重要的。這篇文章,將談論LinkedIn如何調優,從而確保個人主頁達到毫秒級別的響應速度。

背景

在單個數據中心中,個人主頁的QPS能輕松的到達幾十萬以上,然而,當流量發生切換的時候(流量從一個數據中心切換到另一個數據中心),這些額外的負載就會被加到目標數據中心,導致QPS上揚,延遲增大。最終可能導致請求超時。個人主頁變慢,就會拖慢其他依賴主頁的接口,整個WEB服務性能出現級聯反應。

整個切換過程各種問題非常多,這篇文章我們主要介紹在流量高峰期的時候,數據路由層碰到的垃圾收集性能問題,以及我們從CMS切換到G1的動機,我們還將對數據中心切換做進一步的優化。

配置CMS的路由器問題

LinkedIn的個人主頁數據保存在Espresso中(LinkedIn使用的分布式、面向文檔、水平擴容以及高可用的KV存儲,你可以把它當作LinkedIn的MongoDB),當用戶觸發一個到Espresso的讀寫請求時, 路由器首先把請求定向到包含請求數據的存儲節點。許多客戶端選擇從Master存儲節點讀取數據,僅是為了保證讀取的一致性。然而,客戶端也可以選擇通過將讀請求發送到從節點從而擴展讀取能力,當然代價就是可能讀取到過時的數據。

想要了解更多關于Espresso,請戳:https://engineering.linkedin.com/espresso/introducing-espresso-linkedins-hot-new-distributed-document-store

Identity服務,即提供主頁數據的系統,過去一年該服務的QPS翻了一倍,由于這個服務海量的請求,導致它會對存儲節點產生幾十萬QPS。此外,隨著服務存活的時間越長,JVM中CMS的堆碎片化問題就越嚴重

image.png

前段時間,我們大概切換了網站某個數據中心75%的流量到另一個數據中心,這個動作導致路由器返回給上游調用大量的503響應碼告警。并且JVM出現了大量的晉升失敗(promotion failures),導致路由器上很長、而且很多的停頓。很長的停頓就會讓許多客戶端嘗試多次重試,從而使問題越來越惡化。路由器服務中,兩個特別突出的JVM參數:-XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly引起了我們的注意。

這兩個參數意味著,老年代(3G)占用75%,即2.25G的時候,就會滿足觸發CMS的條件。另外,考慮到老年代中巨大的垃圾碎片,老年代可能在占用1.9G的時候,promotion failed就會被觸發,導致2.8秒的長時間停頓:

promotion failed

我們最初的想法是將老年代的大小翻倍提升到6G,這樣可能幫助我們減少碎片化問題。但是,我們很快意識到,增加老年代容量只能延遲整體的清理時間,但是并不是杜絕碎片化問題和promotion failed,并且這次停頓達到了驚人的10.76秒,如下圖所示:

image.png

CMS的碎片化

許多垃圾回收器都有一個標記階段:發現并標記存活的對象,然后清理階段回收死對象占用的內存空間。CMS不是采用復制算法的垃圾收集器,這就意味著在掃描和清理階段,只能在它的free-list中更新對象,但是不會壓縮存活的對象,這就會在內存中產生一個一個的“洞”,如下圖所示,展示了這些“洞”是如何導致堆的碎片化的:

heap-fragmentation

如上圖所示,是CMS回收前后的對比圖。由上圖可知,在老年代使用65%的時候滿足了觸發CMS GC的條件。下圖中空白處就是CMS GC回收掉的垃圾釋放的空間,由于這些小空間,慢慢就就出現了碎片化(圖中空白處所示),而且隨著越來越多次觸發CMS,碎片化問題會越來越嚴重。

再看下面這幅圖,請求需要4KB的空間,即使堆中總有效內存是足夠的(所有空白處相加),但是這個請求還是無法成功分配到內存,因為沒有連續至少4KB的空間,最終導致promotion failure

嚴重的碎片化

調優

現在的情況,個人主頁請求的響應報文大小在2MB以內。一開始,晉升到Old區的對象都是分配的連續的內存塊。 隨著時間的推移,許多大大小小不同尺寸的對象被分配,碎片化開始形成并越來越嚴重。接下來的某個時間點,如果晉升一個2MB甚至更大的對象,老年代可能就沒有這么多連續的內存空間。那么一個FullGC就會被觸發,FullGC意味著完全的STW,應用線程徹底停止,直到完成對堆的清理。而且FullGC的時間一般都是持續超過1秒,甚至幾秒,上10秒。堆越大FullGC停頓的時間就越長。所以,增大內存(當前的Xmx為4GB,我們嘗試將其調大到5GB,甚至10GB)并不能解決辦法,它只是拖延了長時間停頓的問題,并且我們看到了更長的GC停頓。

切換到G1的動機

總之,CMS垃圾回收器不能滿足我們對性能的需求,而G1相比CMS有更清晰的優勢:

  1. CMS沒有采用復制算法,所以它不能壓縮,最終導致內存碎片化問題。而G1采用了復制算法,它通過把對象從若干個Region拷貝到新的Region過程中,執行了壓縮處理。
  2. 在G1中,堆是由Region組成的,因此碎片化問題比CMS肯定要少的多。而且,當碎片化出現的時候,它只影響特定的Region,而不是影響整個堆中的老年代。
  3. 而且CMS必須掃描整個堆來確認存活對象,所以,長時間停頓是非常常見的。而G1的停頓時間取決于收集的Region集合數量,而不是整個堆的大小,所以相比起CMS,長時間停頓要少很多,可控很多。

G1調優

G1被配置在一部分服務器上,且堆大小為5G,測試其與CMS的對比效果。并且路由器節點運行在JDK8上:Intel Xeon E5-2640芯片,24核心,2.5GHz,64G內存。

  1. Reference處理問題

G1偶然性出現奇怪的soft/weak引用清理,從下面的日志可以看出,在YGC階段清理時,Ref Proc居然耗時達到101.3毫秒,從而導致Eden區減少,這就會導致部分對象提早晉升到Old區,從而導致并發GC周期變短:

2015-06-10T12:00:01.854+0000: 711685.290: [GC pause (G1 Evacuation Pause) (young) [Ref Proc: 4.0 ms] [Eden: 2904.0M(2904.0M)->0.0B(2872.0M)
2015-06-10T12:00:05.899+0000: 711689.335: [GC pause (G1 Evacuation Pause) (young) [Ref Proc: 101.3 ms] [Eden: 2872.0M(2872.0M)->0.0B(216.0M) Survivors: 32.0M->40.0M Heap: 4570.1M(5120.0M)->1706.8M(5120.0M)]

我們通過設置-XX:G1NewSizePercent=40,即設置年輕代占用堆最小百分比為40%來解決這個問題。因為有了這個設置,即使引用處理耗時變長,Eden區大小也不可能比這個閾值更低,從而避免對象提早晉升。同時,我們還添加了參數-XX:+ParallelRefProcEnabled,從而在Remark階段多線程并發處理引用對象。

  1. G1的Region大小

G1使用默認的Region大小,但是運行沒多久后,我們就看到了巨大(humongous)對象!對于G1來說,只有那些超過Region大小50%的對象,才會被當作巨大對象。巨大對象分配時需要連續的Region集合,并且直接被分配在H區(特殊的Old區)。在JDK8U40之前,巨大對象只能在并發收集階段或者FullGC時才能回收清理,JDK8U40之后,有一定的優化。因此,我們將JDK升級到8U40以后的版本,清理就可以在更早的GC階段進行了。

CMS vs. G1

接下來,從堆的使用情況、CPU使用時間和GC暫停時間等幾個方面對比CMS和G1。

先看CMS的一些統計信息--說明,CMS+ParNew的組合,GC日志中出現Allocation Failure就表示發生了YGC:

CMS Stat 出現的次數 平均 總計
Allocation Failure 21778 0.0258 562.41
應用線程暫停時間(秒) 39541 0.0132 22.85

再看G1的一些統計信息--說明,G1的話,GC日志中出現Evacuation Pause就表示發生了YGC:

G1 Stat 出現的次數 平均 總計
Evacuation Pause 7212 0.0299 215.6
應用線程暫停時間(秒) 25026 0.0139 350.12
  • 堆使用情況

如下兩張圖所示,第一張圖是CMS堆的使用情況,我們可以發現,有3次明顯的波動,即發生了3次CMS GC:


CMS Heap Usage

第二張圖是G1堆的使用情況。我們可以看到,只有1次明顯的波動,所以G1相比CMS優勢明顯:


G1 Heap Usage
  • CPU使用時間

我們再看CPU使用時間,如第一張圖所示,是CMS的CPU使用時間,有3次的毛刺,這3次毛刺的時間點剛好是發生CMS GC的時候,事實上就是CMS的Remark非常耗時,且峰值達到了驚人的115ms。


CMS CPU Time

第二張圖是G1的CPU使用時間,我們可以看到,只有1次明顯的波動,且峰值只有75ms左右:


G1 CPU Time
  • YGC停頓時間

CMS+ParNew前提下,YGC的平均停頓時間是20~25ms。而G1前提下,YGC的停頓時間是27~30ms,盡管G1相比CMS的YGC平均停頓時間高20%左右,但是G1大概是3秒1次YGC,而CMS的化,大概1秒1次YGC。所以,以3秒為單位的話,CMS的停頓時間要放到3倍,即60~75ms,遠遠大于G1的27~30ms。因此,G1相比CMS的吞吐能力更強。

如下面兩張圖所示,前面一張圖是CMS的YGC停頓時間,后面一張圖是G1的YGC停頓時間:

YGC Of CMS
YGC Of G1
  • 并發GC頻率

并發收集就是當老年代達到一定比例的時候,比如CMS通過參數-XX:CMSInitiatingOccupancyFraction=75配置老年代占用75%的時候滿足并發GC的條件。

對于Identity服務而言,平均6~8小時發生一次CMS GC。而對于G1,周期差不多,大概在6~7小時。如下面兩張圖所示,前面一張圖是CMS并發收集周期,后面一張圖是G1并發收集周期:

CMS GC周期
G1 GC周期
  • Remark停頓時間

并發標記階段就是GC從Root集合遍歷并標記存活對象,這個是一個并發階段,即應用線程和GC線程一起運行。然而,Remark階段是STW的,意味著應用線程必須停頓,直到GC從并發標記開始到找出所有更新的引用。

因為并發收集應用停頓時間主要來自于Remark階段(相比起Remark階段,初始化標記階段時間短很多),因此,調優讓Remark停頓時間盡可能的低,就變得非常非常重要。對于Identity服務,CMS的Remark階段平均停頓時間是150ms,而G1只有50ms。可見,相比CMS,G1并發標記階段的停頓時間控制好很多。

調優效果

通過從CMS切換到G1,我們將平均停頓時間從150ms降低到50ms,并且大大減少了JVM碎片化問題(G1也不能完全避免),并且幾乎沒有觀察到延遲毛刺。而且,將一個數據中心的流量全部切換到另一個數據中心的流量,也完全可以Hold住了。

這次調優已經證明,G1很大的緩解了性能問題,最大化吞吐量的同時,也最小化了延遲,現在服務的平均RT只有30ms。除了Identity服務之外,我們還在測量其他集群的性能指標,以了解它們將從G1中受益多少。

備注:本篇文章翻譯自Tuning Espresso’s JVM Performance:https://engineering.linkedin.com/blog/2016/01/tuning_espresso_jvm

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容