SparkアプリケーションのためのJavaガベージコレクションのチューニングについて

この記事は、インテルの SSG STOビッグデータテクノロジーグループのメンバーからDataBricksに寄稿されたブログを翻訳したものです。誤訳がありましたら、@teppei_tosaに御連絡ください。

f:id:teppei-studio:20150601233612p:plain

Sparkは、その優れた性能、シンプルなインターフェイス、および分析や計算のための豊富なライブラリによって、幅広い業界で採用されてきています。ビッグデータエコシステムにおける多くのプロジェクトと同様に、Sparkは、Java仮想マシンJVM)上で実行されます。Sparkはメモリに大量のデータを格納することにおいて、Javaのメモリ管理とガベージコレクションGC)に大きく頼っています。また、プロジェクトTungstenなどの新たな取り組みは、将来のバージョンで、メモリ管理のさらなる簡素化と最適化を目指しています。しかし、今日時点でも、JavaGCオプションとパラメータを理解しているユーザであれば、自分たちのSpark アプリケーションの最高のパフォーマンスを引き出すようにチューニングすることが可能です。この記事では、SparkのJVMのガベージコレクタの設定について説明し、Sparkのパフォーマンスを向上させるためにGCをどのようにチューニングするかを、実際の使用例交えて説明します。コレクションスループットやレイテンシのような、GCのチューニングを行う際の、重要な検討事項を見ていきます。

Spark とガベージコレクションの紹介

Sparkは様々な産業で幅広く利用されており、Sparkのアプリケーションの安定性とパフォーマンスチューニングの問題は、ますます関心のあるトピックとなっています。Sparkがメモリ中心のアプローチをとるために、従来のJavaアプリケーションではほとんど見られませんが、ヒープ·スペースとして100GB以上のメモリを使用するのが一般的です。大企業がSparkを利用する場合は、Sparkのアプリケーションの実行中にGCを取り巻く様々な課題についての懸念に多く晒されます。例えば、GC による長い遅延や、ひどい場合にやプログラムのクラッシュを引き起こして、さらに長時間浪費することもあります。この記事では、これらの問題を軽減することができるSparkアプリケーションのためのGCのチューニング方法を議論するために、特定の問題と合わせ、実際の例を使用します。

Javaアプリケーションは、通常、2つのガベージコレクション戦略のいずれかを使用します。並行マークスイープ(CMSガベージコレクションとParallelOldガベージコレクションです。前者の狙いは低レイテンシにあり、後者は高スループットにあります。どちらの戦略もパフォーマンスのボトルネックを持っています。CMS GCは圧縮[1]を実行しませんし、パラレルGCはかなりの休止時間をもたらす全ヒープコンパクションを実行します。インテルでは、与えられたアプリケーション要件に応じた戦略を選択することを勧めています。リアルタイム応答が必要なアプリケーションであれば、我々は一般的に、CMS GCをお勧めします。オフライン解析プログラムであれば、パラレルGCになります。

ストリーミングコンピューティングと従来のバッチ処理の両方をサポートするSparkのようなコンピューティングフレームワークで、最適なコレクタを見つけることができるでしょうか。Hotspot JVMのバージョン1.6は、ガベージコレクションの第三の選択肢を導入しました。ガベージ·ファーストGC(G1 GC)です。G1のコレクタは、OracleCMS GCのための長期的な代替品として計画したものです。最も重要なのは、G1のコレクタは、高スループットと低レイテンシの両方を目標としていることです。Spark におけるG1 コレクタの利用についての詳細に入る前に、JavaGCの基礎について見ていきましょう

Javaガベージコレクションはどのように動くのか

従来のJVMのメモリ管理では、ヒープスペースをYoungとOldに分けています。図1にあるように、Youngは2つの小さなSurvivorスペースと、Edenと呼ばれるエリアで構成されています。新しく作れたオブジェクトは、最初にEdenに割り当てられます。マイナーGCが発生する度に、JVMはEden内の利用中のオブジェクトをSuvivorスペースの空き領域にコピーし、他のSuvivorスペースにある利用中オブジェクトもそのSuvivorスペースの空き領域にコピーします。このアプローチによって、オブジェクトを保持するSurvivorスペースと他の空の領域を、次のコレクションのためにそのままにします。何回かのマイナー·コレクションを生き残ったオブジェクトはOld領域にコピーされます。Old領域がいっぱいになると、メジャーGCはすべてのスレッドを中断し、フルGC、すなわちOld領域内におけるオブジェクトの最適化と削除を実行します。

すべてのスレッドが中断されているこの一時停止は、Stop The World(STW)と呼ばれ、ほとんどのGCアルゴリズムでパフォーマンスを犠牲にします。[2]

図1 世代的Hotspotヒープ構造 [2] ※※
f:id:teppei-studio:20150601233640p:plain

Javaの新しいG1 GCは完全に従来のアプローチを変えています。ヒープは、仮想メモリ内のそれぞれ連続した領域として、等しいサイズのヒープ領域のセットに分割されます。(図2参照)ひとつの領域セットは古いコレクタと同じ役割(Eden、Survivor、Old)が割り当てられていますが、それらのための固定サイズはありません。これは、メモリ利用におけるより大きな柔軟性を提供します。オブジェクトが作成されると、最初に利用可能な領域に割り当てられます。領域がいっぱいになると、JVMはオブジェクトを格納するための新しい領域を作成します。マイナーGCが発生するとG1は、ひとつもしくは複数のヒープ領域から利用中のオブジェクトをヒープ上の単一の領域にコピーし、Eden領域として新しいフリーの領域をいくつか選択します。全ての領域が利用中のオブジェクトを保持しており、全て空の領域が見つからない場合にのみ、フルGCが発生します。
G1は利用中のオブジェクトをマーキングする際に、 Remembered Set(RSets)のコンセプトを利用します。RSetsは、外部領域にあって、オブジェクトがどの領域にあるかをトレースします。ひとつのヒープ領域ごとにRSetがあります。RSetは、全ヒープスキャンを回避し、領域毎の並列かつ独立したコレクションを可能にします。これによって、フルGCが引き起こされたときのヒープ占有率を大幅に改善させるだけでなく、マイナーGCによる停止時間をより制御可能なものにし、巨大なメモリ環境を扱いやすいものにしてくれます。これらの破壊的な改善は、GCの性能をどのように変えるのでしょうか。ここでは、古いGC設定からG1のGCの設定に移行させることで、性能の変化を観察する最も簡単な方法を見ていきます。 [3]

表2 G1 ヒープ構造 [3]※※
f:id:teppei-studio:20150601233805p:plain

G1は固定化されたYoung/Old オブジェクトのためのヒープパーティションを使うアプローチを断念しているので、私たちはG1コレクターを使用してアプリケーションの円滑な実行を保護するためにGCの構成オプションを調整する必要があります。古いガベージコレクタとは異なり、G1のコレクタとの良好な出発点は、任意のチューニングを実行することではないことと我々は考えました。だから我々は、-XX:+UseG1GC オプションによって単純にG1を利用可能にし、デフォルト設定で開始することをお勧めします。アプリケーションが複数スレッドを使う時、私達が時々便利に行っているひとつの微調整は、PLAB()リサイズを閉じ、多数のスレッド通信によって引き起こされるパフォーマンス低下を避けるために、 -XX: -ResizePLAB オプションを利用することです。

Hotsport JVMでサポートされているGCパラメータの完全なリストは、-XX: +PrintFlagsFinalパラメータを使ってリストを出力するか、パラメターの一部を説明するオラクスの公式ドキュメントを参照することで見ることができます。

Sparkのメモリ管理について

RDD( Resilient Distributed Dataset : 弾力的分散データセット )は、Sparkにおける中心的な抽象概念です。RDDの生成とキャッシュは、メモリ消費量に密接に関係します。Sparkは、データの再利用のために永続的なキャッシュを許容しており、それにより繰り返し計算処理によって引き起こされるオーバーベッドを回避しています。RDDを永続化するひとつの形態として、JVMのヒープ内のデータの全部または一部のキャッシュがあります。SparkのExecutorは、JVMヒープスペースを二つの領域に分割します。ひとつは、Sparkアプリケーションによってメモリ内に永続的にキャッシュされたデータを格納するために使用されます。もうひとつは、RDD変換時のメモリ消費のために使用されます。私たちは、 spark.storage.memoryFraction パラメータによってこのふたつの領域の比率を調整することができ、それにより、この設定値をかけたRDDヒープスペースサイズを超えないことを確認しながら、キャッシュされたRDDの総量をSparkにコントロールさせることができます。RDDキャッシュ断片の未使用部分も、JVMによって使用することができます。従って、SparkアプリケーションのためのGC分析は、両方のメモリ領域のメモリ使用量をカバーすべきです。

GCの遅延に起因する性能劣化が観られた場合は、Sparkアプリケーションが効果的な方法で限られたメモリ空間を使用しているかをまずは確認する必要があります。少ないメモリ空間でのRDDは、より多くのヒープ領域がプログラムの実行のために残され、GC効率が向上します。逆に、RDDによる過度なメモリ消費はOLD領域にバッファリングされたオブジェクトの数が多いために、大幅なパフォーマス低下を引き起こします。ここでは、ユースケースを基にこのポイントを深堀していきます。

例えば、単純な反復計算を実行するSparkのコンポーネントであるBagleをベースにしたアプリケーションがあるとします。ひとつのスーパステップ(イテレーション)の結果は、前回のスーパーステップに依存するので、各スーパーステップの結果はメモリ上に永続されることになります。プログラム実行中、イテレーション数が増大するにつれ、使用メモリ領域が急速に増加していき、GCが悪化する様子を確認しました。Bagelをよく見ると、ひとつのイテレーション後には使われていないにもかかわらず、前回のイテレーションで使われたメモリを解放することなく各スーパーステップでRDDがキャッシュされていることを発見しました。これでは、より多くのGC試行を引き起こすメモリ消費となってしまいます。我々は、この不必要なキャッシングを、SPARK-2661 で削除しました。このキャッシュ処理の修正後、RDDサイズは3回のイテレーション後に安定し、キャッシュスペースを効果的に制御することができました。(表1参照)。この結果、GC効率は大幅に向上し、プログラムの総実行時間は10〜20%短縮できました。

表 1: 最適化前後における、BagelアプリケーションのRDDサイズ比較

イテレーション回数 イテレーションのキャッシュサイズ 総キャッシュサイズ(改善前) 総キャッシュサイズ(改善後)
Initialization 4.3GB 4.3GB 4.3GB
1 8.2GB 12.5GB 8.2GB
2 98.8GB 111.3 GB 98.8GB
3 90.8GB 202.1 GB 90.8GB
結論

GCが、あまりに頻繁に、かつ長時間かして実行していることが見られる場合には、Sparkによってメモリスペースが効率的に使われていないことを示しているかもしれません。使われなくなったキャッシュRDDを明示的に破棄することによって、性能改善することができます。

ガベージコレクタの選択

アプリケーションが可能な限り効率的なメモリ利用をしているとしたら、次のステップは、ガベージコレクタの何を選択するかです。SPARK-2661を実装した後、我々は、4ノードクラスタをセットアップし、各Executerに88ギガバイトのヒープを割り当てて、スタンドアロンモードでSparkの実験を開始しました。私たちはまずデフォルトのSparkの並列GCからスタートしました。すると、Sparkアプリケーションのメモリオーバーヘッドが比較的大きく、またほとんどのオブジェクト適度に短いライフライクルで再利用されないがために、並列GCはフルGCを度々引き起こし、その度にパフォーマンスの低下も引き起こしました。さらに悪いことに、並列GCは、パフォーマンス·チューニングのための選択肢が非常に限定的なものしか提供されておらず、各領域の容量比率や、Old領域に移動される前にいくつコピーできるかといった、性能を調整するためにいくつかの基本的なパラメータしか使用できません。これらのチューニング戦略はフルGCを延期するのみで、並列GCのチューニングは長時間稼働するアプリケーションをわずかに改善することしかできません。そのため、この記事では並列GCチューニングでは進めません。表2は、並列GCの操作をしめしており、明らかにフルGCが実行されたときに、CPU使用率が最も低くなります。

表2:並列GC実行状況(チューニング前)

設定オプション -XX:+UseParallelGC -XX:+UseParallelOldGC -XX:+PrintFlagsFinal -XX:+PrintReferenceGC -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy -Xms88g -Xmx88g
ステージ f:id:teppei-studio:20150601234323p:plain
Task f:id:teppei-studio:20150601234442p:plain
CPU f:id:teppei-studio:20150601234454p:plain
Mem f:id:teppei-studio:20150601234507p:plain


CMS GCはSparkアプリケーションでのフルGCを排除するために何かするわけではありません。またCMS GCは並列GCよりもフルGCによる停止時間がより長くなり、アプリケーションスループットを大きく損ないます。

次に、我々はデフォルトのG1 GC設定でアプリケーションを実行しました。驚いたことに、G1 GCもありえないフルGC(表3の「CPU使用率」を参照してください。Job 3はほぼ100秒停止しています)を引き起こし、より長い停止時間によってアプリケーション全体の性能を劣化させました。表4にあるように、並列GCより総実行時間がわずかに長いが、G1 GCのパフォーマンスはわずかにCMS GCよりはいいものでした。

表3:G1 GC実行状況(チューニング前)

オプション設定 -XX:+UseG1GC -XX:+PrintFlagsFinal -XX:+PrintReferenceGC -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy -XX:+UnlockDiagnosticVMOptions -XX:+G1SummarizeConcMark -Xms88g -Xmx88g
Stage f:id:teppei-studio:20150601235438p:plain
Task f:id:teppei-studio:20150601235448p:plain
CPU f:id:teppei-studio:20150601235459p:plain
Mem f:id:teppei-studio:20150601235509p:plain


表4 3つのガベージコレクタのプログラム実行時間比較( 88GB ヒープ・チューニング前)

ガベージコレクタ 88GBヒープに対する実行時間
並行GC 6.5分
CMS GC 9分
G1 GC 7.6分

ログベースのG1コレクタのチューニング[4][5]

私たちはG1 GCを設定した後、次のステップとしてGCログに基づいてコレクタのパフォーマンスをさらに調整しました。

まず第一に、JVMにより詳細なログを出力させます。spark.executor.extraJavaOptionsに追加のフラグを設定します。一般的に、以下のオプションが必要です。

  • XX:+PrintFlagsFinal -XX:+PrintReferenceGC -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy -XX:+UnlockDiagnosticVMOptions -XX:+G1SummarizeConcMark

このオプションによって、Sparkのexecutorログで詳細GCログと効果的なGCオプションを追跡します。(executorログは、各workerノードの、 $SPARK_HOME/work/$ app_id/$executor_id/stdout に出力されます)。次に我々は、GCログに応じて問題の根本原因を分析し、プログラムのパフォーマンスを改善する方法を学ぶことができます。

以下のようにG1 GCログの構造をみてみましょう。例としてG1 GCにおける混合GCを見てみます。

251.354: [G1Ergonomics (Mixed GCs) continue mixed GCs, reason: candidate old regions available, candidate old regions: 363 regions, reclaimable: 9830652576 bytes (10.40 %), threshold: 10.00 %]

[Parallel Time: 145.1 ms, GC Workers: 23]

[GC Worker Start (ms): Min: 251176.0, Avg: 251176.4, Max: 251176.7, Diff: 0.7]

[Ext Root Scanning (ms): Min: 0.8, Avg: 1.2, Max: 1.7, Diff: 0.9, Sum: 28.1]

[Update RS (ms): Min: 0.0, Avg: 0.3, Max: 0.6, Diff: 0.6, Sum: 5.8]

[Processed Buffers: Min: 0, Avg: 1.6, Max: 9, Diff: 9, Sum: 37]

[Scan RS (ms): Min: 6.0, Avg: 6.2, Max: 6.3, Diff: 0.3, Sum: 143.0]

[Object Copy (ms): Min: 136.2, Avg: 136.3, Max: 136.4, Diff: 0.3, Sum: 3133.9]

[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.3]

[GC Worker Other (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 1.9]

[GC Worker Total (ms): Min: 143.7, Avg: 144.0, Max: 144.5, Diff: 0.8, Sum: 3313.0]

[GC Worker End (ms): Min: 251320.4, Avg: 251320.5, Max: 251320.6, Diff: 0.2]

[Code Root Fixup: 0.0 ms]

[Clear CT: 6.6 ms]

[Other: 26.8 ms]

[Choose CSet: 0.2 ms]

[Ref Proc: 16.6 ms]

[Ref Enq: 0.9 ms]

[Free CSet: 2.0 ms]

[Eden: 3904.0M(3904.0M)->0.0B(4448.0M) Survivors: 576.0M->32.0M Heap: 63.7G(88.0G)->58.3G(88.0G)]

[Times: user=3.43 sys=0.01, real=0.18 secs]

このログを見ると、G1のGCログは非常に明確な階層を持っていることがわかります。一時停止がいつ、なぜ発生したかがリストされており、平均CPU時間とCPU時間が最大になる時の様々なスレッドの経過時間の等級付けをおこないます。最後に、G1 GCはこの停止後のクリア結果と総消費時間をリストしています。

現在の G1 GC実行ログでは、下記のような特殊なブロックを見つけることができます。

(to-space exhausted), 1.0552680 secs]

[Parallel Time: 958.8 ms, GC Workers: 23]

[GC Worker Start (ms): Min: 759925.0, Avg: 759925.1, Max: 759925.3, Diff: 0.3]

[Ext Root Scanning (ms): Min: 1.1, Avg: 1.4, Max: 1.8, Diff: 0.6, Sum: 33.0]

[SATB Filtering (ms): Min: 0.0, Avg: 0.0, Max: 0.3, Diff: 0.3, Sum: 0.3]

[Update RS (ms): Min: 0.0, Avg: 1.2, Max: 2.1, Diff: 2.1, Sum: 26.9]

[Processed Buffers: Min: 0, Avg: 2.8, Max: 11, Diff: 11, Sum: 65]

[Scan RS (ms): Min: 1.6, Avg: 2.5, Max: 3.0, Diff: 1.4, Sum: 58.0]

[Object Copy (ms): Min: 952.5, Avg: 953.0, Max: 954.3, Diff: 1.7, Sum: 21919.4]

[Termination (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 2.2]

[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.6]

[GC Worker Total (ms): Min: 958.1, Avg: 958.3, Max: 958.4, Diff: 0.3, Sum: 22040.4]

[GC Worker End (ms): Min: 760883.4, Avg: 760883.4, Max: 760883.4, Diff: 0.0]

[Code Root Fixup: 0.0 ms]

[Clear CT: 0.4 ms]

[Other: 96.0 ms]

[Choose CSet: 0.0 ms]

[Ref Proc: 0.4 ms]

[Ref Enq: 0.0 ms]

[Free CSet: 0.1 ms]

[Eden: 160.0M(3904.0M)->0.0B(4480.0M) Survivors: 576.0M->0.0B Heap: 87.7G(88.0G)->87.7G(88.0G)]

[Times: user=1.69 sys=0.24, real=1.05 secs]

760.981: [G1Ergonomics (Heap Sizing) attempt heap expansion, reason: allocation request failed, allocation request: 90128 bytes]

760.981: [G1Ergonomics (Heap Sizing) expand the heap, requested expansion amount: 33554432 bytes, attempted expansion amount: 33554432 bytes]

760.981: [G1Ergonomics (Heap Sizing) did not expand the heap, reason: heap expansion operation failed]

760.981: [Full GC 87G->36G(88G), 67.4381220 secs]

ご覧の通り、最大の性能劣化はこのようなフルGCによって引き起こされ、「to-space exhausted」、「to-space overflow」もしくは類似のものとして出力されます(JVMのバージョンによって、出力形式は微妙に異なってきます)。原因は、G1 GCコレクタが特定の領域のGCを行った時に、利用中のオプジェクトをコピーする先としてのフリーの領域を見つけることができないことにあります。この状況は、避難失敗と呼ばれ、多くの場合フルGCにつながります。そしてどうやら、G1 GCでフルGCは並列GCよりもさらに悪化しているので、私たちはより良いパフォーマンスを実現するために、フルGCを回避しようとしなければなりません。 G1 GCでフルGCを回避するために、2つの一般的に使用される方法があります。

  1. InitiatingHeapOccupancyPercentオプションの値(デフォルトは45)を減少させ、G1 GCに混合GCを早期に開始させ、一方でGC頻度を増加させる
  2. ConcGCThreadsオプションの値を大きくし、混合GC段階での並列スレッドをより多く引き起こし、各停止時間の効率を高める。このオプションはいつくかの効果的なワーカースレッドのリソースを占有することについて注意が必要。

このふたつのオプションのチューニングによって、フルGC発生の可能性を最小限にします。フルGCを除去した後、性能は劇的に向上しました。しかしながら、GC中の長時間の停止はまだありました。さらなる調査で、ログ上に下記のものを発見しました。

280.008: [G1Ergonomics (Concurrent Cycles) request concurrent cycle initiation, reason: occupancy higher than threshold, occupancy: 62344134656 bytes, allocation request: 46137368 bytes, threshold: 42520176225 bytes (45.00 %), source: concurrent humongous allocation]

ここでは、標準的な領域の50%以上のサイズである巨大なオブジェクトがあります。Eden、Survivor、Old領域とは別に、G1は、巨大なオブジェクトを保持するための巨大なヒープ領域として知られる、4番目のオブジェクトのタイプを持っていました。それぞれの巨大な領域は、ひとつの巨大なオブジェクトだけを保持します。このような大きな領域を割り当てるのは時間がかかり、これらの領域はフルGCの間だけ扱われるため、そのためこのサイズのオブジェクトの作成を避けたいのです。G1HeapRegionSize の値を大きくすることで、巨大な領域を作成する可能性を減少させることができますが、デフォルト値はすでに最大値の32Mに設定されています。これは、これらのオブジェクトを見つけ、その生成を最小化させるための分析だけができることだということを意味しています。G1 GCの発達により、この最大値は最新のバージョンでは拡大しており、G1が1024か2048の領域に分割されるヒープ上で最高のパフォーマンスを達成できることが知られています。

次に、サイクルの開始から混合GCの終わりまでの単一のGCサイクルの間隔を分析することができます。時間が長すぎる場合は、ConcGCThreadsの値を大きくすることを検討できますが、CPUリソースを占有することに注意してください。

G1 GCではまた、ガベージコレクションの並列ステージ上でより多くの作業を行うことで、STW時間を短くする方法があります。前述したように、G1 GCはどのオブジェクトがどの領域にあるかをトレースするRSetを外部領域に持ち、G1 GCは並列ステージ上とSTWステージ上の両方でRSetsを更新します。もしG1 GCによるSTW時間を短くしようとしているのなら、G1ConcRefinementThreadsの値を大きくしつつ、G1RSetUpdatingPauseTimePercentの値を小さくすることができます。G1RSetUpdatingPauseTimePercentオプションは、全STW時間におけるRSetsの更新時間の望ましい割合を特定するために使われます。10%がデフォルトです。G1ConcRefinementThreadsオプションは、プログラム稼働中にRSetsを維持するためのスレッドの数を定義するために使われます。これら二つのオプションをチューニングすることで、RSetsの更新処理のワークロードをより多くSTWから並列ステージに移行することができます。

加えて、長時間稼働しているアプリケーションには、AlwaysPreTouchオプションを使い、アプリケーション起動時に必要な全てのメモリをOSに適用させ、動的アプリケーションを回避させることができます。これは、開始時間の遅延コストによる実行時性能を改善させます。

いくつかのGCパラメータチューニングの結果、表5の結果にたどり着きました。前の結果と比較すると、より満足のいく実行効率を得ることができました。

表5 G1 GC実行状態(チューニング後)

オプション設定 -XX:+UseG1GC -XX:+PrintFlagsFinal -XX:+PrintReferenceGC -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy -XX:+UnlockDiagnosticVMOptions -XX:+G1SummarizeConcMark -Xms88g -Xmx88g -XX:InitiatingHeapOccupancyPercent=35 -XX:ConcGCThread=20
Stage f:id:teppei-studio:20150602000527p:plain
Task f:id:teppei-studio:20150602000538p:plain
CPU f:id:teppei-studio:20150602000546p:plain
Mem f:id:teppei-studio:20150602000555p:plain
結論

Sparkアプリケーションのその他の代替案と比較して、G1 GCを試して見ることをお勧めします。きめ細かな最適化は、GCログを解析することによって可能です。我々はチューニングによって、アプリケーションの実行時間を4.3分に短縮することに成功しました。チューニング前と比較すると1.7倍の性能向上であり、並行GCと比べると、1.5倍前後向上しました。

Summary

メモリ·コンピューティングに大きく依存しているSparkアプリケーションの場合、GCのチューニングは特に重要です。問題がGCで出現する場合、GC自体をデバッグする前にやることがあります。最初にSparkプログラムの、RDDの永続化やキャッシュ解放といった、メモリ管理における非効率性について考慮してみてください。ガベージコレクタをチューニングする場合は、まずG1 GCを使用してSparkアプリケーションを実行することをお勧めします。G1コレクタはSparkでよく見られるヒープサイズの増加を制御する態勢を整えています。G1では、高いスループットと低レイテンシのために必要となるオプションは少ないです。

もちろん、GCチューニングに決まった法則があるわけではありません。様々なアプリケーションはそれぞれ異なった特徴をもち、予測不可能な状況に対処するためには、ログを基にしたGCチューニングの芸術と法医学を習得しなければなりません。結局、我々はプログラムのロジックとコードを最適化し、中間オブジェクトの生成やレプリケーションを少なくし、巨大なオブクジェクト生成の制御や、ヒープ外への長時間保持オブジェクトの格納などなどに取り組まないといけないのです。

G1 GCを使用することにより、我々は、Sparkアプリケーションの主要なパフォーマンス向上を実現しました。Sparkは今後、メモリマネジメントの責任をJavaガベージコレクションからSpark自体に移していきます。これは、Sparkアプリケーションのために必要なチューニング項目を減らしていくでしょう。とはいえ、今日時点ではガベージコレクタの選択は、ミッションクリティカルなSparkアプリケーションのパフォーマンス向上に役立つことでしょう。

Acknowledgement

During the tuning practice and writing of this article, we received guidance and assistance from Ms. Yanping Wang, senior engineer from Intel’s Java Runtime team.

※ Indicates graphs generated using internal Performance Analysis tools developed by Intel Big Data team.

※※ Indicates images from Oracle documentation. For details, see reference [2] [3]

References

[1] https://docs.oracle.com/cd/E13150_01/jrockit_jvm/jrockit/geninfo/diagnos/garbage_collect.html#wp1086917

[2] http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

[3] http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html

[4] http://www.infoq.com/articles/tuning-tips-G1-GC

[5] https://blogs.oracle.com/poonam/entry/understanding_g1_gc_logs

About the Authors:
Daoyuan Wang, Software Engineer from SSG STO Big Data Technology, Intel Asia-Pacific Research & Development Ltd., who is also an active Spark contributor in the Apache community.

Jie Huang, engineering manager of SSG STO Big Data Technology, Intel Asia-Pacific Research & Development Ltd.