Blog

GCメモリ初心者の新卒がScalaでGCメモリを勉強してみた


この記事はCyberAgent エンジニア Advent Calendar 2015の14日目の記事です。 昨日は13卒の鈴木さんのブログでした、明日は同期のchoheyさんです。

はじめに

こんにちは、サイバーエージェントアドテクスタジオ15新卒のフィ(@dxhuy)と申します。 アドテク新卒の中で「YYY初心者の新卒がXXXでYYYを勉強してみた」というタイトルが流行っていますので、パクらせていただきました。

ネットワーク初心者の新卒がDockerでネットワークの勉強をしてみた

Scala初心者の新卒が頑張ってLispを作ってみた

現在RightSegmentという子会社でData Management Platform (DMP)を作っています。データの会社なのでHadoopのバッチの開発とか、フロントサーバでJavaの開発などをしています。

GCメモリについて

会社に入る前にはスクリプト系の言語を使うことが多く、メモリを自前で管理するなども気にしなくていい 時代でした。会社に配属され、javaとscalaがメインで開発 を行いますが、java/scalaではメモリ管理もほとんどいらないですが、GCをちゃんと理解して、そこで性能に影響しないようにGCチューニング、また コードでGCにプレッシャーをかけすぎないように気にしないといけない時代になりました。

GCはJVMの運用者にとって、特にデータベース、大事なウェブサーバなどの「Pause Time(止める時間)が長くなれば死ぬ」のようなサービスはとても怖い言葉です。このの記事を参考すればGCの運用の大変さがよくわかると思いますので是非ごらんください: これがCassandra

つまり、GC上のメモリをちゃんとチューニングしないと、性能が安定しない、普段は軽い処理なのに、ピーク時などにレスポンス遅延が発生することが 多く発生します。「GCをうまくチューニングすればいいじゃん」と思っている方がいると思いますが、アプリケーションの特徴により、たとえばヒープ がでかくて、ライフサイクルが短いオブジェクトが多いアプリとか、ヒープが小さくて、長く生きているオブジェクトが多いなどでたくさんのパターンもあって、それぞれチューニング仕方も違うし、とても大変です。

そこでGCにもう頼りたくない、自前でメモリを扱いたい方がいるでしょう。JVMでは、GCを使わずに、メモリを直接する方法が「オフヒープメモリ」と呼び、この記事では、Scalaでオフヒープメモリの検証などを紹介したいと思います(はじめがだいぶ長くになってしまい申し訳ありません m(_ _;)m)

JVMのオフヒープについて

Java アプリケーションは Java ランタイムの仮想環境で稼働しますが、ランタイム自体は、ネイティブ・メモリーをはじめとするネイティブ・リソースを使用する言語 (C など) で作成されたネイティブ・プログラムです。ネイティブ・メモリーとは、ランタイム・プロセスが使用できるメモリーのことで、Java アプリケーションが使用する Java ヒープ・メモリーとは区別されます。オフヒープはそのネイティブメモリを利用するという意味です。ネイティブヒープはJVMヒープとは違い、基本的には明示的な制限がないのが特徴です。(制限した方がよいかは別として)

JVMではオフヒープを使うために:DirectByteBufferを利用、misc.UNSAFEを利用、JNIを利用の三つの種類があります。

  • DirectByteBuffer

このコードを見ると使い方の簡単さがわかりますね、すごくCでmallocの使い方のそっくりですね。 アロケートされたメモリ領域にputInt, putDouble, putLong, putShort, putCharという五つのデータ型を入れることができます。 使いやすいですが、DirectByteBufferを使うデメリットも幾つかあります:

  •  アロケートされたバッファーは最大2GB
  • アクセスされるたびにバウンドチェックが行われ、大きなパフォーマンス影響ができます(メモリサーフのため)
  • sun.misc.Unsafe

上のDirectByteBufferの使い方を比較すると:Unsafeがより低いAPIを使うことがわかりますね。 その代わりに、アロケートされたメモリアドレスもわかって、ページサイズを決めることもできます。(C懐かしい!) アロケートされた領域にデータ入れる方法はDirectByteBufferと一緒です。 ただUnsafeの大きなデメリットは、そのクラスの名前とそっくりです、セーフじゃないです!!!! まだ解放されないメモリをつかっちゃたりしてとか、まだ使っているメモリ領域を解放しちゃったりとかいろいろ強いケースがあります。 また気にしないとメモリリークになる可能性も高いです。

  • Cコードでjniを通して使うこと

Cコードを見るだけではjavaでやるメリットがわからなくなりますww。またメモリセーフもなく、低レイヤのAPIを使わないといけないなどのデメリットもあります。

Scalaでオフヒープを使いたい!

じゃ本題にもどりますが、Scalaでオフヒープを使いたい場合は、何が推奨でしょう。もちろん上の三つの方法もありますが、scalaぽくのやり方があるなら使いたいでしょうね。scalaぽくの使い方があります! どん!https://github.com/densh/scala-offheap

scala-offheapはオフヒープをscalaぽく使えるようになるライブラリです。下ではUnsafeを使いますが、macroを使ってscalaぽく、メモリセーフの使い方ができるようになるライブラリです。 そのライブラリがやってくれることは大きく三つ:

  1. @data のクラスマクロにより、自分の好きなメモリレイアウトを定義でき、シリアライズ、デシリアライズをしてくれます
  2. オフヒープ配列をサポートしてくれます
  3. Regionという概念の導入により、Regionの中のオブジェクトはRegion使い終わったら解放してくれて、メモリセーフでとても便利です

じゃ簡単なコードを書いてみましょう:

まずは@dataマクロの使い方から:

上の例ではInt,Intの8バイトのメモリレイアウトを定義して、それをアロケートするコードです。簡単でしょうね。次はArrayをオフヒープでアロケートしてみましょう:

次はRegionを使ってみましょう:

じゃもうちょっと手を動かしたいですので、オフヒープを検証するために、オンメモリのKVSで作って検証しましょう。

Scala-offheapを使ってKVSを作ってみよう

今回作ったKVSの仕様は簡単で、キーがString、値がStringで put, putIfAbscent, get, delete の簡単な4つの操作をサポートするものを作ってみましょう:

今回はメモリの方の検証だけをしたいため、並列のアクセスとかも気にしなかったので、Atomic操作もサポートしないKVSです。

scala-offheapではStringのデフォルトでシリアライズをサポートもなく、マップのサポートもないので、そこが問題ですね。 じゃ、どうせちょっとオンヒープを使わないといけなさそうなので、putしたキーと値をキーをハッシュして、そして値をOffheapにアロケートして、 アロケートされたアドレスを、ハッシュキーとペアとしてオンヒープのMapで管理すればいいですね。 それだとオンヒープのマップがMap[Long,Long]となってだいぶメモリオーバーヘッドを軽減することもできるじゃないかと考えます。 じゃ実装を進めましょう:

まずはハッシュですが簡単にMurMur3を使います:

次はputの操作は以下となります:scala-offheapはStringのシリアライズをサポートしないなので@dataマクロ使えないですが、配列サポートしますのでArray[Char]をオフヒープでアロケートすればよいですね。ただ元の文字列からオフヒープの配列にコピーする必要がありますので、putする値が大きければ大きくほどオーバーヘッドが大きくなりますので、そこがだいぶパフォーマンスダウンする原因になりそうですね。

のこりのputIfAbscent,delete,getも実装簡単なのでコードだけを貼って説明を飛ばします >.<:


実装完了しました!!

でオンヒープと比較したいのでConcurrentHashMapを使ってオンヒープの実装も簡単に書きました

じゃ性能比較してみましょう。性能比較するために、まず実行速度を検証してみます。50000回のループの中で、長さ10000の文字列(オフヒープに乗せるメモリが大きけれあ大きくほど検証しやすいのでちょっと大き目にしました)をランダムのキーにインサート・ゲットします。


結果は、どん!

つまりオフヒープを使うと性能が1.4倍落ちますね。 シリアライズも自分でしないといけないのでその結果はおかしくないと思います。オフヒープってメリットなくない?って思っている方もいるでしょう。

じゃOffHeapのメリットをわかるように別の検証を行いたいですね。OffHeapのメリットはメモリがきつくなるときにGCの安定さですよね。そこで 上の検証をもう一度実行しますが、ヒープをきつくしてもう一度やってみましょう。ヒープの調整はbuild.sbtでメモリをちっちゃくしてみました(ちっちゃくしすぎるとOffHeapは大丈夫ですがOnHeapのほうはOOMで死んでしまうので検証にならないため、OOMにならないようにぎりぎりの700mbに設定してみました)

もう一回リランしてみると、どん!

おお、ヒープをきつくなるとOnHeapの方が遅くなることがわかりましたね。gc.logを出してみたらOnHeapの方は圧倒的にFullGC回数が発生し、そこで性能が落ちることもわかりました。gc.logはgistに置いておきました:https://gist.github.com/huydx/cf1b647db7bdab07404c . 回数を数えてみると:

オフヒープの方のメモリのフラグメンテーションが発生するかも時間があれば検証したいですが、今回は時間足りず検証できませんでした、申し訳ありません >.<.

最後に

scalaではオフヒープメモリを使いたければscala-offheapというライブラリがすごく便利ですので是非使ってみてください。 オフヒープを使うと:

  • ヒープの余裕があるとパフォーマンスがちょっと普通のオンヒープのデータ構造と比べて遅くなります
  • その代わりに、GCの影響がないため最悪のケースでも許可範囲のLatencyが得られます(銀行系などのケース結構大事)
  • メモリを直接使うため、オンヒープよりメモリサイズのオーバーヘッドが少ない
  • ややこしいデータ構造だとメモリのシリアライズとデシリアライズは自前でしないといけないのでめんどくさい

上の特徴があるため、いつもオフヒープがいいというわけではありませんので、必要なケースに使えばよいかと思います。

 

 

Author

アバター
admin