戰勝Go和Redis! Java ZGC新GC在數TB記憶體中只有毫秒或更短的暫停 - 邁克的部落格

banq發表於2019-06-11

這篇文章是分析了ZGC和Shenandoah的垃圾回收在數TB記憶體中只有毫秒級的暫停時間,並且與Go語言做了比較, Java新傢伙贏得了這場低延遲的比賽。Java在低延遲,快速響應,高效能方面優於Go語言和Redis的延遲。文章很長,只做概要摘錄:

衡量GC的重要設計指標:
  • 程式吞吐量:您的演算法減慢了多少程式?這有時表示為收集與有用工作所花費的CPU時間的百分比。
  • GC吞吐量:在指定固定的CPU時間量的情況下,收集器可以清除多少垃圾?
  • 堆開銷:收集器需要多少額外記憶體超過理論最小值?如果你的演算法在收集時分配臨時結構,是否會使你的程式的記憶體使用非常尖銳?
  • 暫停時間:你的收集器會暫停的時間有多長?
  • 暫停頻率:您的收集器多久停止一次?
  • 暫停分佈:您通常有非常短暫的暫停但有時會有很長的暫停時間嗎?或者你更喜歡停頓有點長但一致?
  • 記憶體分配效能:快速,慢速或不可預測的新記憶體分配?
  • 壓縮:即使有足夠的可用空間來滿足請求,您的收集器是否會報告記憶體不足(OOM)錯誤,因為該空間已經分散在堆中的小塊中?如果沒有,你可能會發現你的程式變慢並最終死亡,即使它實際上有足夠的記憶體來繼續。
  • 併發:您的收集器使用多核機器的程度如何?
  • 縮放:當收集器變大時,收集器的工作情況如何?
  • 微調效能:收集器的配置有多複雜,開箱即用並獲得最佳效能?
  • 預熱時間:您的演算法是否根據測量的行為進行自我調整,如果是,需要多長時間才能達到最佳狀態?
  • 記憶體頁釋放:您的演算法是否會將未使用的記憶體釋放回作業系統?如果是的話,何時?
  • 可移植性:您的GC是否在CPU體系結構上工作,提供比x86更弱的記憶體一致性保證?
  • 相容性:您的收藏家使用哪些語言和編譯器?是否可以使用非GC設計的語言執行,比如C ++?它需要編譯器修改嗎?如果是這樣,更改GC演算法是否需要重新編譯所有程式和依賴項?

使用上面指標比較Shenandoah, ZGC 和其他流行GC!

暫停時間
Java GC工程師是一個相當謙遜的團隊,即使他們花了數年時間開發新的收集器,也不會賣力推銷他們的工作成功。因此,關於ZGC的規範性陳述聲稱“不會超過10毫秒暫停”,但如果我們轉到幻燈片12上的SPECjbb2015基準測試,我們看到實際上,平均暫停時間約為1毫秒。

Shenandoah的暫停時間同樣很短。可以看到GC日誌以大約200到400暫停微秒,它甚至可以在有小堆的慢速硬體上執行 - 在執行Web伺服器的Raspberry Pi上,暫停時間仍然只有3-8毫秒。這對嵌入式裝置來說非常好。

請記住,無論堆大小如何,都會獲得這些暫停時間。也就是說,如果需要,您可以在數TB的大小上獲得毫秒或更短的暫停。能夠自動管理大量堆是一項基本功能,可以改變程式的編寫方式:如果您的資料集適合這樣的堆,您可能根本不需要編寫分散式軟體。

將這些延遲與本機手動記憶體管理所帶來的延遲進行比較會很有趣。不幸的是,很難找到有關malloc延遲分佈的資訊。這篇博文描述了一個Redis負載測試,其中glibc malloc延遲的最壞情況大約為10毫秒。這聽起來是非常糟糕的情況,但不幸的是,大多數mallocs的基準使用非常不切實際的測試場景,比如分配和立即釋放分配,所以很難得到真實的資料。JVM GC使用SPECjbb進行測試,SPECjbb是實施超市物流應用程式的基準。

程式吞吐量
ZGC的目標是開銷不低於15%。這與mallocs報告的最壞情況開銷相當。將吞吐量與Go收集器進行比較很困難,因為Go收集器的效能影響取決於您選擇的記憶體開銷。但是Go的方法對吞吐量的影響非常嚴重這裡有電子表格)。Shenandoah的吞吐量開銷有點差,但仍然在15%-20%左右。

壓實Compact
無論 Shenandoah和ZGC收集器實際上是在程式執行時在記憶體移動位元組,不會暫停執行的程式。這個技巧最初可能看起來不可能,但它實際上是所有現代收集器背後的關鍵理念(Go除外)。壓縮compact有用有三個原因:

  • 它消除了堆碎片。清除碎片分配器通常存在於手動管理的應用程式中,但它們以增加的開銷為代價減少了碎片,並且永遠無法完全消除問題。
  • 透過以更好地利用快取的方式重新組織記憶體中的資料,它可以使程式更快
  • 它能夠以高速度清除大量垃圾,這對於本質上是generational 世代的程式非常有用。


記憶體分配
使用壓縮的一個重要原因是它清除了大的連續記憶體區域。這反過來意味著分配可以快速完成 - 一旦執行緒抓住了清空堆區域的一大塊,它就可以透過簡單地增加單個指標並進行比較來分配記憶體,即只需要兩三條指令,內聯到分配站點。

釋放記憶體頁
Java在記憶體使用方面聲名狼借的原因之一是,它的大多數垃圾收集器都不會經常將記憶體釋放回作業系統。在伺服器端沒有問題,但是在桌上型電腦,尤其是許多程式競爭RAM的開發人員桌面上,這種行為是反社會的。Shenandoah有一個很好的功能 -  即使應用程式處於空閒狀態它也會主動進行垃圾收集,並穩定地將記憶體釋放回作業系統。這使其成為與IDE等桌面應用程式一起使用的潛在不錯選擇。JVM使用的預設G1收集器也學習瞭如何在Java 12中執行此操作

相容性
JVM執行許多語言,新的GC不僅可以讓Java受益,還可以授益與JavaScript,Python,Ruby,Haskell(透過Eta),Clojure,Scala,Kotlin,R,COBOL。


與Go比較
在我之前關於垃圾收集的2016年的文章中,我觀察到Go正在宣傳自己,因為它有一個“一刀切”的收集器,它比“企業”替代品更好,並且足以在可預見的未來做好一切。但事實並非如此,所以我批評他們的營銷方式。幾年後,Go團隊發表了這個名為“Getting to Go”的優秀演講,我將其描述為幾乎完全相反 - 這非常誠實。

它告訴我們Go GC的設計是以不尋常的方式設計的,主要是由於谷歌之前未提及的內部工程限制,特別是由於對短期成功的驚人需求。

最初的計劃是做一個無障礙的併發複製GC。那是長期計劃。讀取障礙的開銷存在很大的不確定性,因此Go想要避免它們。
但是短期內2014年我們不得不一起行動......我們也需要快速的東西並專注於延遲,但效能影響必須小於編譯器提供的加速。所以我們受到限制。
我們還關注編譯器速度,即編譯器生成的程式碼......在2015年也迫切需要短期成功。


Go的GC設計有另一個限制 - 在他們的同事已經沉迷於暫停延遲的長尾的環境中。

無論原因如何,Go的不尋常的設計選擇顯然是由於不同尋常的限制:他們在一家公司經歷著對尾部延遲的痴迷,他們的程式設計時間有限,因為他們同時在Go中重寫標準庫,而且大多數他們所需要的只是非常快地推動暫停延遲以獲得“短期成功”。

Go確實降低了暫停延遲,儘管其他領域的成本很高,他們在工程預算有限的情況下很快就做到了,他們獲得了他們所需的短期成功。令人遺憾的是,他們採用了一些奇怪的要求,例如不希望向使用者公開微調開關,這導致他們被不同程式的各種各樣的需求嚴重壓制。

Go編譯器是一個經典的批處理作業。暫停時間對於編譯器來說根本不重要,只有總執行時間才有效。但是減少暫停時間的技術都會增加總執行時間,因此這是一個問題。用於批處理作業的良好GC演算法類似於JVM的並行GC。

他們嘗試了一個“面向請求的收集器”,它可以更好地擴充套件到某些重要的應用程式:

正如你所看到的那樣,如果你有ROC而不是很多共享,事情實際上可以很好地擴充套件。如果你沒有ROC,那就差不多了。

但是......它減慢了他們的編譯器:

那時我們對編譯器有很多擔心,我們無法放慢編譯器的速度。不幸的是,編譯器正是ROC沒有做好的程式。我們看到30%,40%,50%和更多的減速,這是不可接受的。Go對其編譯器的速度感到自豪。

GC設計很難!Go的人可能已經重走了JVM路線,讓使用者根據他們是否更關心延遲或吞吐量兩種中一個而選擇GC演算法。

所以他們放棄並嘗試了一種新的方法 - 一個普通的世代垃圾收集器。

因此,他們在2018年時的建議是:等待記憶體價格下降,並要求Go使用者給Go應用程式大量的堆記憶體,以減少當前演算法需要完成的GC工作量。

如果我們將這個故事與JVM世界中的等同物進行比較,我們可以看到不同的東西。在SPECjbb 2015基準測試(模擬倉庫管理資料庫應用程式)中,ZGC強制實際上不會減慢應用程式的速度,但會顯著改善延遲:

暫停時間幾乎總是毫秒或更短,但吞吐量不會受到嚴重損害。

同時有三個標準調整標誌選項 - 一個用於選擇ZGC而不是另一個演算法,一個用於設定最大堆大小,一個通常可以單獨保留,因為它會自動調整,但會設定收集器獲得的CPU時間。透過利用Linux核心的大頁面功能,可以使用一些更加模糊的內容來進一步提高效能。

總的來說,在我看來,Java傢伙正在贏得低延遲遊戲的勝利。




 

相關文章