/home/by-natures/dev*

ソフトウェア開発者として働く人の技術的なメモ

MemoryAnalyzer で巨大なヒープダンプファイルを扱う

ヒープダンプを扱った際のメモです。

メモリを多く使うアプリケーションが OOM エラーで落ちる際に、ヒープダンプを出力すると後々に調査が可能だと知りました。アプリケーション起動時に XX:+HeapDumpOnOutOfMemoryError オプションを設定するか、JMX 経由でアプリケーション実行中に設定することも可能です。アプリケーションが終了する際にのみ寄与するので、実行時のオーバーヘッドはなく、OOM エラーが発生しうるアプリケーションにとっては有用だと書かれています:

docs.oracle.com

hprof は J2SE 時代からある、ヒープ領域・CPU のプロファイリングツールとのこと。ヒープダンプを取るだけではないんですね。

OOM が発生すると、.hprof 拡張子のついたファイルがワーキングディレクトリに出力されます。OOM 時のヒープダンプなのでファイルサイズが非常に大きく、今回出てきたのが7GBほどのバイナリファイルでした。インターネット上では数十GBものヒープダンプをどうやって扱ったら良いのかの Q&A がありました。

Memory Analyzer

ヒープダンプを扱うツールに、Eclipse プラグイン(とスタンドアロン版もある)Memory Analyzer があります。Memory Analyzer を起動するためのヒープ領域がデフォルトでは小さめなので、MemoryAnalyzer.ini を開いてヒープ領域の最大値を広げておかないと、Memory Analyer 自信がヒープダンプファイルを読み込んだ時に OOM を発生させてしまいますので注意が必要です。

Memory Analyzer をコマンドラインで扱う

Memory Analyzer を起動すると GUI が現れ、インタラクティブにヒープダンプを解析できます。しかしヒープダンプファイル自体が大きい場合、この GUI で消費するメモリすら削減したい場合には、付属する ParseHeapDump.sh というコマンドがあります。

MemoryAnalyzer/FAQ - Eclipsepedia

javaforu.blogspot.jp

ParseHeapDump.shLinux 版の Memory Analyzer にしか付いていませんが、中身は Memory Analyzer の起動スクリプト-consoleLog オプションをつけているだけでした:

"$(dirname -- "$0")"/MemoryAnalyzer -consolelog -application org.eclipse.mat.api.parse "$@"

これを真似て、Mac OSコマンドラインで実行したところ、正常に動作しました:

cd mat.app/Contents/MacOS/
./MemoryAnalyzer -consoleLog -application org.eclipse.mat.api.parse <hprof ファイル>

これを実行すると、指定したファイルと同階層に .index ファイルが大量に生成されます。.index ファイルが生成されたあとであれば、GUI 経由でヒープダンプをインタラクティブに眺めることができます。

コマンドラインでもヒープダンプファイルに応じてメモリを大量に消費しますが、この .index ファイルさえあれば概ねのデータは GUI で確認できるので、メモリを多く積んだマシンで上記コマンドで .index ファイルを生成しておけばよい…とどこかの Q&A に書いてありました。ちょっと怖いですが、適当な作業用サーバ上で .index を作って、手元に落として GUI で分析…といった流れなのでしょうか(なかなか面倒ですね…)。

アプリケーション実行中に JMX からヒープダンプを取得

OOM 時ではなく、アプリケーション実行中にヒープダンプを取得するのに JMX が使えます。

http://blog.bosch-si.com/categories/technology/2011/11/how-to-get-a-heap-dump-of-a-remote-machine-via-jmx/

NVD3 で multiBarChart を扱う場合、データには数値型を渡す

Java での開発は少しずつ慣れているのですが、JavaScript は入る現場ごとに使うフレームワークが変わる印象があります。4年前ほどにジョインしたプロジェクトでは ExtJS, 同時期に走っていた別のプロジェクトでは AngularJS を使っていて、チーム間の技術的な交流はほとんどありませんでした。最近は React.js をよく耳にしますが、現在のプロジェクトでも React.js を使っています。最初は JavaScript の中にタグを直接書く JSX というフレームワークに強い抵抗がありましたが、出てくる DOM と対応がよく取れていて、慣れるととても便利です。

一方、JavaScript にはグラフ描画のための d3.js というライブラリがあり、このラッパーである NVD3 というライブラリがあります。更にそれを React.js で使えるようにした react-nvd3 というライブラリがありますが、今回はこのライブラリで詰まった点があったので共有します。

NVD3 では様々なグラフが簡単に扱えます。その中の一つに棒グラフがあります:

var props = {
  type: "multiBarChart",
  datum: [{
    key: "num",
    values: [{ x: "A0", y: "5" },{ x: "A1", y: "5" },{ x: "A2", y: "5" },{ x: "A3", y: "5" }]
  },{
    key: "num2",
    values: [{ x: "A0", y: "1" },{ x: "A1", y: "1" },{ x: "A2", y: "1" },{ x: "A3", y: "1" }]
  },{
    key: "num3",
    values: [{ x: "A0", y: "2" },{ x: "A1", y: "2" },{ x: "A2", y: "2" },{ x: "A3", y: "2" }]
  }],
  containerStyle: {
    width: 500,
    height: 300
  }
};

...

return (<div><NVD3Chart {...props}/></div>);

これを実行すると、以下のような棒グラフが出てきます:

f:id:bynatures:20160914025026p:plain

上に Grouped, Stacked という選択肢があるのですが、左が種類ごとに出力する棒グラフ、右が積み上げの棒グラフです。ここで Stacked を選択すると…

f:id:bynatures:20160914025333p:plain

オレンジの領域が他の種別に覆いかぶさってしまっています。上のプログラムを見ると、y 軸の値が "5" のような文字列になっているのが分かります。これが問題の原因だったのですが、Grouped では正常に動いていたため、気づくのが遅れました。(実際はプログラムから上のオブジェクトを生成していたので、ここまで簡略化してようやく気づきました。。)

y 軸の値を整数値にすることで以下のような正しい積み上げ棒グラフが得られます(単に y軸の tickFormat で変換するだけではダメで、元々の与えるデータを整数値にしないと駄目でした):

f:id:bynatures:20160914030846p:plain

z軸…というか値のかぶせ方を決める計算が誤っているのだ思い、ソースコードを見てみましたが、NVD3 の GitHub repo の multiBar.js に該当する処理がありそうですが、処理が複雑で追いきれていません。

意図しない挙動を避けるためにも、棒グラフ・折れ線グラフの y軸など、数値が想定されている箇所には強制的に数値へ変換してしまうのがよいような気がしています。+"a" など、何かの値に単項演算子+ をつけると数値か NaN へ変換してくれるので便利です。

var a = "str";
+a;   // =>  NaN
+a  ||  0;   // =>  0

質問して結局自己解決してしまった StackOverflow の質問:

stackoverflow.com

JMX を jconsole/SpringBoot で使う

JMX を業務で触れたため、改めて調べたのでメモ。

とりあえず SpringBoot で動かす

JMX に接続するためのクライアントに jconsole というツールがあります。Stackoverflow で "What is the best or most commonly used JMX Console" という質問を見ても jconsole が最も人気のようです。jconsole は JDK と一緒にインストールされるようなので、多くの環境ですぐに使えるのも良いのかもしれません。

試しに jconsole コマンドを打ってみます:

$ jconsole --help

f:id:bynatures:20160913024538p:plain

おもむろに GUI が立ち上がり、ヘルプメッセージが表示されました。PID を直接渡したり、ホスト:ポートのペアを渡したりできるようです。今回は SpringBoot の適当なアプリケーションを立ち上げて、その PID に接続してみることにします:

$ jconsole 14336    # 私の環境のアプリケーションの PID です

f:id:bynatures:20160913024839p:plain

何やら出ますが気にしないで接続(リモートだと SSL を使って通信できるようです)

f:id:bynatures:20160913024931p:plain

これで jconsole が立ち上がり、Java アプリケーションの JMX に接続ができました。リモート環境であれば SpringBoot の起動オプションに JMX を有効にする設定と JMX 用のポートを指定すれば、ローカルの jconsole を使って、リモート環境の JMX にアクセスが可能でした。

これでリモートアプリケーションのリソース状況が知れたり、スレッドダンプが見れるだけでも十分便利なのですが、一番右タブの「MBean」が見慣れません。

MBean とは?

MBeanはJava Virtual Machine上で走るリソース(アプリケーションやJava EE技術サービス(トランザクション・モニタやJDBCドライバなど))との連絡窓口の役割を果たす。MBeanは、関心のある統計数値(パフォーマンス、リソース使用量、問題など)を収集すること(プル)、アプリケーションの設定値を取得または設定すること(プッシュ/プル)、および障害や状態変化などのイベントを通知すること(プッシュ)に使える。

やはり Wikipedia だとよく分かりません… いくつかのブログや文献を見ると、特定の記法に従うクラス(bean)に対して、JMX を通じて情報を取得したり、アクションを与えることができる機構のようです。

通常は **MBean と名前の付いたインタフェースを用意し、それを実装した具象クラスを MBean Server に登録する…という流れのようです。SpringBoot では便利なアノテーションが用意されているため、試しに適当につけてみます:

@SpringBootApplication
@ManagedResource(objectName = "MyApp:name=MyMbean")
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @ManagedOperation(description = "Returns something.")
    public String getSomething() {
        return "Something";
    }
}

参考ブログ:MBeans using spring and annotations

@ManagedResource(objectName = "MyApp:name=MyMbean") と、@ManagedOperation(description = "Returns something.")アノテーションを付けた getSomething メソッドを追加しています。この状態で jconsole を開き、MBean のタブに行くと、MyApp が登録されているのが分かります:

f:id:bynatures:20160913031207p:plain

getSomething ボタンを押すと、返り値の Something が帰ってきます:

f:id:bynatures:20160913031234p:plain

 

MBean には他にも、イベントに応じて通知を送信したり、他の MBean の状況を定期的にポーリングするなどの設定もできるようです。JMX を理解したとはまだ到底言えませんが、思ったよりずっと気軽に使える技術であると分かっただけでも収穫でした。

Jackson パッケージは fasterxml が新しい

少し前に、Jackson パッケージを使おうとしてハマったことがありました。

Jackson には com.fasterxml.jackson グループのモノと org.codehaus.jackson のモノがあります。Jackson は 2.0.0 以降からネーミングスペースが前者の com.fasterxml.jackson に移ったため、IDE で Jackson をインポートしようとすると、fasterxml, codehaus のどちらを選ぶかを尋ねられることがあります。ここで間違って codehaus を選ぶと古い Jackson が動いてしまい、なんとなく動きはするけれど細かい挙動が違ったりエラーが発生したりします。

私が出くわしたのは、JsonPOJO へ変換する際に、コード上は抽象クラスで定義しておいて、実際は特定のパラメータを見て具象クラスでデシリアライズする処理でした。ハマったのは、REST APIJson を変換するのは問題なかったのですが、DB から Json を文字列として読み込み、それをデシリアライズする際にうまくいかないためでした。エラーも Can not construct instance of AbstractConfig といった分かりにくい内容でした:

16/03/24 17:17:20 ERROR (...Abbr...) org.codehaus.jackson.map.JsonMappingException: Can not construct instance of AbstractConfig, problem: abstract types can only be instantiated with additional type information

Maven でライブラリ管理をする際も、fasterxml, codehaus のどちらかに寄せるようにしないとバグの原因になりかねません。恥ずかしながら問題が起こるまではあまり気をつけていなかったので、こうしてメモするに至ります。

 

Hive 関連のライブラリも同じような問題があります。例えば、HiveStatement クラスを IDE で補完しようとすると、二つの候補が出てきます:

  • org.apache.hadoop.hive.jdbc.HiveStatement
  • org.apache.hive.jdbc.HiveStatement

これは後者が新しいパッケージで、Hive 1.0.0 から hadoop の下ではなく、Hive 単体としてパッケージが独立したようです。

 

以上2つのパッケージ問題について Stackoverflow で質問したポストです:

stackoverflow.com

stackoverflow.com

HiveServer2 のメモリ使用量はパーティション数が関係する

まだ詳しく調べられていないのですがメモ代わりに。

 

先日 HiveServer2 を利用する機会があり、負荷がどのぐらい掛かるかを確認していました。Cloudera の説明によると、同時コネクション数にも依存しますが数GB〜数十GBのヒープ領域が必要だとあります:

Configuring HiveServer2

合わせてこんな注意書きもありました:

These numbers are general guidance only, and may be affected by factors such as number of columns, partitions, complex joins, and client activity among other things.

カラム数、パーティション数、複雑なジョイン処理などに影響して必要なリソース量が変わるということです。

 

今回実行したのは MapReduce が動作する COUNT 関数を入れた SELECT 文と、結果の行数が大きな SELECT 文です。今考えると、これらは上記のいずれにも該当しないので、HiveServer2 に負荷の掛かるクエリではなかったのですが、実際に HiveServer2 上で負荷を確認しても、メモリ使用量がそれほど増えているようには見えませんでした。

これを Stackoverflow に質問したところ、やはりパーティションを多く消費するクエリだとメモリ使用量が大きくなるのでは、とのことでした:

stackoverflow.com

MapReduce であれば、パーティション数によって Mapper / Reducer の数が変わって負荷に直接影響しそうだなとイメージできるのですが、MapReduce を発行する HiveServer 側で負荷が上がる理由がまだ理解できていません。今 象本(4版)の輪読会もしているので、もう少し勉強して、この辺りの感覚がつかめるようになりたいです。

YARN の DRF スケジューリングについて

会社で Hadoop The Definitive Guide(いわゆる象本)の第4版を読んでいるのですが、その中の YARN の章で、ジョブスケジューリングのアルゴリズムである Dominant Resource Fairness (DRF) の説明があります。

YARN で複数のリソース(CPU, メモリなど)を扱う場合に、DRF では "dominant resource" を決めます。例えば、クラスタ全体が (100 CPU, 10 TB のメモリ) からなる場合に、アプリケーションA が (2CPU, 300GB) を要求すると、アプリケーションA はクラスタ全体の (2%, 3%) を要求しています。アプリケーションB が (6CPU, 100GB) を要求すると、これは全体の (6%, 1%) にあたります。ここで、それぞれの最大値を dominant resource として定め、アプリケーションA は 3%, アプリケーションB は 6% を要求する、と判断します。つまりアプリケーションB はアプリケーションAの2倍のリソースを要求することになります。

まとめるとこうなります:

  • Application A requests containers of (2 CPUs, 300 GB)
    • (2%, 3%) -> dominant is 3%
  • Application B requests containers of (6 CPUs, 100 GB)
    • (6%, 1%) -> dominant is 6%

この後の説明に、「YARN は アプリケーションA に 2倍のコンテナを割り当てる」とあるのですが、ここが分かるようで分からないようで… 輪読会中に参加者で悩んでしまいました。アプリケーションBが2倍のリソースを必要とするのだから、アプリケーションB に2倍のコンテナが割り当たるのでは?という疑問です。

この質問を Stackoverflow に投げたところ、以下の非常に丁寧な回答がありました:

stackoverflow.com

DRF は、各アプリケーションに dominant resource が均等に割り当たるようにコンテナを分配します。そのため、アプリケーションA に2倍のコンテナを割り当てると、3% の2倍で6%となり、B の dominant resource と一致する、という訳です。

 

Stackoverflow をよく使うのですが、丁寧な回答があると非常に勉強になりますね。

PermGen 領域に OOM エラー

OOM エラーに出くわしたのですが、よく発生する Heap 領域のものとは違うエラーが発生しました:

java.lang.OutOfMemoryError: PermGen space

Hive 関係のアプリケーションで発生していて、temporary function を大量に利用する処理を入れたばかりだったので、temporary function によって読み込まれた JAR とクラスがこの PermGen 領域に加わり、OOM エラーが発生したようです。

PermGen の理解

"PermGen" の正確な理解が怪しかったので少し調べたところ、とても詳しく解説されているブログがありました:

d.hatena.ne.jp

PermGen 領域 HotSpot VM で利用されますが、通常のヒープ領域とは別に確保され、クラスやメソッドのデータ、及びそのメタデータが格納されるとあります。領域の拡張も -XX:PermSize-XX:MaxPermSize オプションなどで、ヒープ領域とは別に設定する必要があります。

jmap コマンドで稼働しているプロセスのメモリ情報が取得出来るので、今回はこれで PermGen に割り当てられているメモリ容量を確認して、-XX:MaxPermSize オプションで容量拡張しました。

$ sudo jmap -heap <プロセスID>

Java8 では PermGen -> Metaspace

この jmap コマンドですが、Java SE 8 で実行すると、以下のような結果になります:

$ sudo jmap -heap <プロセスID>
...

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 1073741824 (1024.0MB)
   NewSize                  = 22020096 (21.0MB)
   MaxNewSize               = 357564416 (341.0MB)
   OldSize                  = 45088768 (43.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
...

PermGen という言葉が見つからないのですが、Java8 では "Naive メモリ" という領域へ移動し、名前も Metaspace という名前に変更されています。これを詳しく解説されているブログがありました:

equj65.net

もやは -XX:MaxPermSize などの PermGen に関するオプションは意味がないようですが、デフォルトでは Metaspace には容量制限が実質ない状態なので、MaxMetaspace オプションを指定していなければ、今回のような OOM は発生しなさそうです。