JavaOne2014 3日目メモ (9/30)

今日も晴れていて、過ごしやすい気候。3回目のJavaOneでも雨に振られたことがないので、サンフランシスコは雨が少ない地域なのだろうか。

Building a Distributed Application for the Cloud with Akka Clustering and Java 8 [TUT6483]

PlayフレームワークとAkkaを組み合わせて、最近話題のリアクティブなシステムを実現しましょうという話。

リアクティブ・マニフェストについて

詳細については原文を参照のこと。

f:id:n_agetsuma:20141002083537p:plain

  • Responsive (応答性)
    • システムは可能な限り最適なタイミングで応答すること。
  • Resilient (障害からの回復)
    • システムは処理が失敗した場合にも、失敗したことを応答すること。これは高可用性やミッションクリティカルシステムだけに求めるのではない - 処理の失敗時に何も応答できないシステムはResilientではない。
  • Elastic (柔軟性)
    • システムは大量のリクエスト下においても、応答すること。
  • Message Driven (メッセージ駆動)
    • リアクティブなシステムは、疎結合で独立性が高く、配置されたロケーションに依存しないコンポーネント間の非同期メッセージングのやり取りによって成り立つ。
実現手段

リアクティブなシステムを実現するためには『決してブロックしてはならない。常に非同期にすること。』が求められる。この実現手段としてPlay2Akkaの組み合わせが紹介された。

  • Play2
    • 非同期でノンブロックングな処理が実装可能
    • ステートレスなのでスケールする
    • RESTful & WebSocketのサポート

Playやakkaを使わなくても、分散システムでスケーリングさせるためには、以下のようなことに気をつける必要がある。

  • いかに軽量にコンポーネント間の通信を行うか (最近はRESTやMQTTなど)
  • 非同期でかつ並行処理でないと、いくらマシンを増やしてもスケールしない
  • クラスタ機能
    • 故障検知、ロードバランス機能

色々とデモコードが紹介されたがうまく理解できなかったため、akkaを実際にいじってみるのが宿題。javaでも動くらしい。

Vert.x + WebSocket + Cloud = Awesome Map Tracking [CON1695]

Vert.xでwebsocketを実装して、以下の画面のように常に動くバス情報をブラウザ上の地図画像にアイコンとして表示するデモのセッション。RedHatが提供しているPaaSであるOpenShiftがVert.xに対応しているので、合わせてOpenShiftの紹介も少し。スライドはこちらで公開中。

f:id:n_agetsuma:20141002093136p:plain

セッションの冒頭で早速『今までのTomcatJBossと何が違うの?』という質問が飛ぶ。分散環境でフレキシブルに重点を置いた新しいアーキテクチャと答える。

Vert.xの特徴
  • 多言語対応 : JVM言語で動く言語であれば言語を選ばない
  • 非同期処理
  • NettyをベースとすたノンブロッキングI/O
  • イベントバス
    • 同一VM/リモートVMに配置されたVerticle(モジュールのようなもの)の通信手段を提供

akka、Vert.xなど、非同期とノンブロッキング、スケールを意識した比較的新しいアプリケーション基盤がC10K問題にぶつかるような高アクセスシステムで使えそうなのは理解したが、身近なところで使えそうな領域を探すのが帰ってからの宿題。

Going Native: Bringing FFI to the JVM [CON3979]

従来のJNIに代えて、もっと簡易にネイティブコードが呼び出せる仕組み JNR (Java Native Runtime)に関するセッション。スピーカーはJRubyの開発者であるCharles Nutterさん。JNRについてはgithubにて公開中

JRubyを作っていて困った事

Javaで作られているJRubyを作るにあたって、OSレベルへのアクセス、例えばファイルシステムアクセスや、グラフィック/暗号化などのネイティブライブラリへのアクセスで困っている。しかし既存のJNIは使いにくい面も多い。もっと簡単にネイティブコードが呼び出せる仕組みがあると良い。

JNRのコード例

現在のプロセスIDを取得するlibcのgetpid関数を呼び出す例。とても直感的なコード例になっている。

import jnr.ffi.LibraryLoader;
import jnr.ffi.annotations.IgnoreError;

public class GetPidJNRExcample {
    public interface GetPid {
        @IgnoreError
        long getpid();
    }

    public static void main(String[] args) {
        GetPid getpid = LibraryLoader.create(GetPid.class)
                                     .load("c");
        getpid.getPid();
    }
}
JNRの構成要素

ユーザレベルの関数だけでなく、色々なネイティブ関数にアクセスできる。例えば以下のようなもの。

今後に向けて
  • この仕組みをJava標準のFFI(Foreign function interface)として実現するには、JVMの協力が不可欠
  • 現状のところ、JDKの改善提案であるJEP 191としてドラフトを提案
  • Project PanamaとしてJDKレベルでAPIの定義や、JVMレベルの最適化、セキュリティへの考慮などを検討中
  • 将来的にはJSR(Java Specification Requests)として検討したいが、現状は未定

The Modular Java Platform and Project Jigsaw [CON5435]

前々から検討されているJDKへのモジュールシステム導入 Project Jigsaw に関するセッション。

互換性の維持と新機能の要望により増え続けるJDKのコアAPI実装にモジュール化システムを導入して、使いたいモジュールのみをロードする仕組みを作る。デモではrt.jarがなくなったよとのアピールがあり。

jlinkと呼ばれるツールで依存性を管理するらしいが、詳細は理解できなかった。JDK9のEarly Accessには今のところJavaOneでデモがあったjlinkやrt.jarの削除が盛り込まれていないので、JDK9のEAにマージされるのが待ち遠しい。

JSON Pointer and JSON Patch: Updates to the Java API for JSON Processing [CON5742]

  • 後日追記予定

Troubleshooting with Serviceability and the New Runtime Monitoring Tool HeapStats [BOF3108]

NTT OSSセンタが開発したJavaトラブル解析支援ツール HeapStatsBOF。NTT OSSセンタはNTTグループ会社向けのOSSのテクニカルサポートを提供しており、頻出トラブルとトラブル解決の迅速化を目的に開発されたHeapStatsの紹介が行われた。

よくあるトラブルについて

OSSセンタに寄せられたサポート依頼では、ヒープ関連のトラブルシュート依頼が特に多かった。

  • Heap/Perm/GC 31.3%
  • API 20.1%
  • JavaVMのクラッシュ 19.3%
トラブルシュートの課題

情報がない
ヒープのトラブルが多いが、トラブル発生時のヒープダンプが残っていない事も多く、-XX:+HeapDumpOnOutOfmemoryErrorをトラブル発生後に追加してもリークスピードが遅く、再現に数ヶ月かかるケースもあった。

既存ツールは重い
ヒープダンプは取得に時間がかかるためサービス影響が大きく、ヒストグラムではオブジェクト間の参照が見れないため解析情報が不足する。

HeapStats

HeapStatsの特徴は以下の通り。

  • トラブルシュートに必要な情報を漏らさず収集
    • JVM動作中に定常的に少しずつヒープ情報を収集
  • 軽量
    • HeapStatsはメジャーGC契機で情報収集するので、新たなStop the worldを発生させない。
    • SPECjvm2008では、HeapStatsアタッチによるオーバヘッドは4.51%
  • 可視化
    • HeapStatsエージェントで情報収集したバイナリ形式のヒープ情報を、GUIツールのHeapStatsアナライザで可視化

クラスヒストグラムの推移を示したHeapStatsアナライザ表示例
f:id:n_agetsuma:20141003074328p:plain

オブジェクト間の参照関係を示した表示例
f:id:n_agetsuma:20141003074400p:plain

HeapStats、Javaトラブルシューティングに強力です。

JavaOne2014 2日目メモ (9/29)

2日目。朝は霧が出ていて少し寒いが、日中になると日差しが出て半袖でも良いくらい暑くなる。今日から本格的にセッションの開始。

Java EE 8 [CON3015]

Java EE8のスペックリードであるLinda DeMichielさんによる、Java EE 8の現状の方針を話すセッション。

Java EE 8の大きなテーマは3つ。

  • HTML5/Web層の改善
  • Easy of Development (かんたんに開発) / CDI Alignment
  • モダンインフラへの対応 (クラウド対応)
HTML5/Web層の改善
Easy of Development / CDI Alignment
  • セキュリティインターセプタ
  • コンテナ管理Beanのシンプルな一本化。EJBでしかできない"MDB"をCDIで実行。
  • EJB2.x のクライアント向けビューをプルーニング(オプション化)
    • EJBHomeやEJBObjectなどのEJB2.xがオプション仕様となる
モダンインフラへの対応
  • Java EE Management APIの導入
    • RESTベースのAPIでAPサーバの管理・監視を標準化する 
  • Java EE Security 1.0
    • 現状使いにくいセキュリティ仕様を見直して作り直す

Tutorial: JVM Platform as a Service [TUT3525]

スエーデンのCITERUSという企業の技術者が、2012年にJavaが使えるPaaSを使ったプロジェクトを経験談を交えながら、各PaaSプラットフォームの特徴やデモを示すセッション。

なぜPaaS?
  • 何よりもtime-to-marketの達成がPaaSを使う一番の目的。
  • 過去の事例では、顧客のコアドメインがITと関連が薄く、ITインフラ(サーバとか)を持ってなかったのがPaaS適用を推進した。
dotcloudを使ってみて
  • Java、MongoDB、PostgreSQL、RabbitMQが使いたかったため、テクノロジスタックが合った
  • 実際に運用してみたら色々問題も出てきた
    • ヨーロッパ - アメリカのレイテンシによる性能問題、価格の値上げ改定
  • 結果的にjerasticを使ってサービスを提供しているスエーデン国内のPaaSサービスに移行し、各々の問題に対処した。
どうやってPaaSを選ぶ?
  • 提供されているテクノロジスタック(言語、DB、Webコンテナ等)
  • 成熟しているサービスか?SLAおよび、サポート条件。価格。
  • ロックインされるか? (例えばGoogle BigtableをPaaSで使う等)
  • 価格は変動する。常に同じだと思わない方が良い。

PaaSは良い面ばかりがフォーカスされがちですが、歴史の浅いサービスだと価格改定が頻繁にあることが想像できてなかったので、良い勉強になりました。

HTTP 2.0 Comes to Java: What Servlet 4.0 Means to You [CON5989]

HTTP2.0(SPDY)の登場に伴い、ServletJava SE9のAPIへの機能追加について紹介するセッション。まだ JSR369 Servlet4.0 として検討がはじまったところで、いくつかのコード案が紹介されたが写真取れず。

Java SE 9側でのHTTP2.0クライアントのアイディアについては、以下のようなコード例が紹介された。

HttpRequestGroup group = HttpRequestGroup.create();
HttpRequest req = group.createRequest()
    .setRequestMethod("POST")
    .setRequestURI(new URI("http://www.foo.com/a/b"))
    .setRequestBody("param1=1,param2=2")
    .onResponseHeader("X-Foo", (request, name, value) -> {
        System.out.println("receive an X-Foo header")
     })
    .sendRequest()
    .waitForCompletion();

HTTP2.0では、1つのコネクションが1対1でのリクエスト/レスポンスモデルではなく、複数のリクエストが1コネクションで並行して送受信されるため、リクエストのグループ化(HttpRequestGroup)が導入し、その後のリクエスト送信にもメソッドチェーンや、リクエスト応答のヘッダ処理にラムダ式を書いたり、最近のJavaっぽいAPI案となっている。

Modular Architectures Using Microservices [CON6132]

モジュラアーキテクチャの考え方を、クラウド環境でOSGiを使うためにリモート呼び出しなどをサポートするAmdatuを使って実現する方法を紹介したセッション。

まず、amdatuによる実装を示す前に、ソフトウェアをなるべくモジュール化して連携させるモジュラアーキテクチャとマイクロサービスの考え方を紹介した。

モジュラアーキテクチャとは

原則として以下の2つを要件として紹介した。

  • Adaptivility (change ahead)
    • 適応性。ビジネス・環境の変化にソフトウェアの変更が追従できるか。
  • Reuse
    • 再利用。コピペしない。オブジェクト指向もコピペをなるべくなくして、コンポーネントを上手に区切り、なるべくコードを再利用しやすいようにするのが始まりだった。
マイクロサービスとは
  • マーチンファウラ氏のブログの内容を時折引用。
    • 個々のコンポーネントはそれぞれ自分のデータを持つ。小さなサービスごとにデータを分ける。
    • DDDでコンテキスト境界として紹介されている単位
    • 分散サービスの連携においては、トランザクションを使わずにeventual consistency(実質的なxxx)によって現実的な一貫性の保持を目指す
    • サービスは自らのライフサイクルを持つ
    • RESTやメッセージングなどの軽量なコミュニケーション手段を使う
      • 分散サービスの連携に、ESBのような複雑性を持ち込まない
  • 分散システム固有の設計には注意する必要
    • どれか1つが落ちていても、サービスが縮小継続できるように
    • モニタリングに手間が掛かるのは、分散の代償
OSGiについて
  • モジュラアーキテクチャを実現する上で、バージョンの管理とデプロイの分離が必要
  • OSGiでバンドルごとのバージョン管理、デプロイ分離を実現
  • MANIFEST.MFへのexport/import記述により、API実装の詳細を隠してモジュール公開することが可能
Amdatu

リモート分散環境でOSGiを使うためのソフトウェア。OSSとして公開。自分でリモート接続コードを書かなくても、内部的にうまくやってくれてユーザは各OSGiバンドルの開発に集中できるらしい。

まとめると、マイクロサービスの実現例の1つとして、OSGiを使って、リモート分散をAmdatuに使いましたよというセッションでした。

Banking on OpenJDK: How Goldman Sachs Is Using and Contributing to OpenJDK [CON5177]

Goldman Sachs社のOpenJDKへの取り組みに関するセッション。

Goldman SachsJava
  • 1998年頃からJavaを技術評価として利用開始
  • 2004年から自社コレクションフレームワークを作り始め(githubで公開中)
  • 2013年からOCAにサインして、OpenJDKへのコントリビュートを開始した
  • 190GB以上のヒープを持つシステム、大量の分岐があって、JITコードキャッシュに影響を与える特殊な環境が特徴的。
なぜOpenJDKに力を入れ始めたのか
  • ソースが見れるのが何よりも重要
    • 商用でクラッシュしても解析できる
    • HotSpotの内部で改善できそうな部分を自分たちで見つけることができる
  • パッチを投げることが、自分たちのためにも、コミュニティのためにもなる
どんな風にOpenJDKを使っている
  • トラブルシューティング、新規能の研究・検証に使用
    • JVMTIでアサーションエラーが発生したバグ解析とパッチ作成の事例を紹介
  • 今のところ、プロダクション環境では使っていない

OpenJDKしてのソースが公開により、筆者自身もAPIやHotspotでのトラブル時に大変助かっている。Redhat Enterprise Linuxを使っているシステムではOpenJDKがバンドルされていて気軽に使えるので、もっと使われるところが増えると嬉しい。

JavaOne2014 1日目メモ (9/28)

JavaOne2014の初日はコミュニティが主体のユーザグループフォーラム(UGF)と、キーノートがサンフランシスコのモスコーンセンタにて行われた。カメラとPCを繋ぐケーブルを家に忘れたので、写真は後日追加予定。

以下、荒い部分も数多くあるがセッション参加メモ。

Lambdas and Laughs [UGF9672]

プロジェクトラムダの振り返りに関するセッション。

ラムダ式とは?ストリームAPIとは?Optionalは?など、Java8に入った新機能についてコードを見ながら振り返り、JavaOneでの関連セッションを紹介する。ユーザグループフォーラムのセッションらしく、途中でウケ狙いのスライドがたくさん入っているが、何回もスベる。

内容については様々なJava8特集で既出のため、特記事項なし。

GlassFish Roadmap and Executive Panel [UGF9120]

GlassFishのロードマップと、今後の方針に関するパネルディスカッション。アジェンダについては、GlassFishのホームページにもまとめられている。

ユーザグループフォーラムにも関わらず、オラクルのJava EE関連のエヴァンジェリストReza Rahmanさんや、この後のストラテジキーノートでも講演するCameron Purdyさん、日本でもWebLogicの製品発表関連で見かけるMichael Lehmannさんなど、オラクルとしての方針を話そうな立場の人たちがパネラーに並ぶ。

GlassFish 製品の基本的な戦略 (アナウンス済み内容と同じ)
  • Oracle GlassFish Server(商用サポート)としては、以前アナウンスしたとおり今後収束され、最終的に2019年にExtended Supportも終了する
  • GlassFish OpenSource Editionとしては今後も継続する。ただし、商用サポートは提供しない。
GlassFish4.1の新機能
  • プラットフォームのアップデート
    • Java8対応、CDI1.2、WebSocket1.1
  • Tyrus(WebSocket実装)
    • セッションリミット、プロキシのサポート、クライアント再接続
  • Jersey
    • 新しい診断(diagnostics)APIの導入
    • ServerSentEventクライアント再接続機能
  • 品質の安定化
Java EE8の今のところの方針
  • HTML5サポート/Web層の改善
    • JSON-Binding、JSON-Processing、Server Sent Event(SSE)、Action-based MVC、HTTP2.0サポート
  • Easy Of Development & Simplification : 開発生産性向上
    • セキュリティインタセプタ、メッセージングのシンプル化、WebSocketスコープなど
  • Modernize the infrastructure : モダンインフラへの対応
    • RESTベースの管理・監視APIの導入
    • より簡易なセキュリティAPIの導入。ユーザ管理、ロール管理、認証・認可など
Java EE8ロードマップ
  • Java EE 8 JSR 366で公開されている内容と同じ
  • 2016/3にFinal releaseとなる予定

パネルディスカッション内で触れられていたが、GlassFishWebLogicと共用しているモジュールも多く、これからも開発の継続されることが改めて強調されていた。

The OpenJDK Project: Your Java. Our Java. [UGF9755]

OpenJDKの今後の方針とコントリビュート方法に関するセッション。JEP(JDK Enhancement Proposals)に挙がっているJDKの改善案の紹介が行われた。いずれもドラフト段階だが興味深い。

紹介されたキーワードを以下のまとめる。

  • Shenandoah : 新しいGCストラテジ
    • 100GBヒープのような巨大ヒープでも、10ms以下の停止を目指す
List<Integer> list = #[ 1, 2, 3 ];
  • Measurement API
    • 色々な物事の値を単なるintやdoubleのような数値ではなく、クラスとして定義して抽象化したもの
    • githubのコード例により何となくイメージできる。例えば以下は心拍数を示す。
public static void main(String[] args) {
    HeartRate rate =  HeartRateAmount.of(BigDecimal.valueOf(90), BPM);
    System.out.println(rate);
}
  • HTTP2.0 Client
  • 軽量なJSONパーサ などなど

Strategy keynote / Technical keynote

Strategy Keynote
  • キーノートの前日に子供たちにプログラミングを教えるイベントがあったらしく、キーノートは子供たちの成果発表から始まる。
  • 子供向けだけではなく、コミュニティやJCPを通じてコミュニケーションを取る姿勢をアピール
Java SE
  • Java 8 buzz
    • Java8は色々な記事にも取り上げられ、書籍もたくさん出版されて確実に広まっている。
  • OpenJDKへのコントリビュート
    • オラクル以外のコミュニティによる貢献が引き続き行われている。ダグ・リーさんによるコンカレントAPIへの継続的な貢献など。
  • Goldman sachsによるJava8の適用事例
    • 自社のコレクションフレームワークのテストコードに適用したところ、105kL→95kLとなった
    • 匿名内部クラスをラムダ式に書き直したのが主な内容
  • Java SEのロードマップ
    • JDK8u40 (2015年前半)
      • パフォーマンス改善、他言語サポートの改善、アクセシビリティの改善など
      • Java SE Advancedへの継続的な機能追加
    • JDK8u60 (2015年後半)
      • バグフィックスおよびJava SE Advancedへの継続的な機能追加
      • シンプルにいうと『未定』と感じた
    • JDK9 (2016年)
Java ME
  • 普段使ってないので聞いてもよくわからず。
Java EE
  • 基本的には先ほどのGlassFishユーザグループフォーラムの内容と同じ
  • GlassFish4.1の機能、今のところ決まっているJavaEE8の方針について

Technical Keynote

  • Javaは来年で20周年
    • 2004年のJava5、2014年のJava8の大幅な改訂を振り返る
    • Javaは2030年になっても継続的に使われることを考慮して設計を続けている
  • 言語仕様やJVMの改善を盛り込むProject Valhalla、ネイティブコード呼び出しを改善するProject Panamaについては時間切れで説明なし

今年のキーノートは過去の振り返りが中心で新しい発表はなく、OpenJDKコミュニティやJCPで流れているJava9やJavaEE8の内容はまだまだ議論の途中でオラクルによる公式な周知はまだない印象を受けました。

HeapStatsをChefでインストールする

HotSpotJVMの監視・解析OSSツールであるHeapStatsをChefでインストールできるようにクックブックを作ってみた。


n-agetsu/chef-heapstats · GitHub

HeapStatsとはなんぞやについては、JJUGナイトセミナーなどのスライドキャスレー技術ブログ JavaVM監視・解析ツール HeapStatsを使ってみた参照。特に後述のブログには公式のwikiよりも詳しくまとまっている。

簡単にいうと、ヒープダンプのように一気に負荷をかけなくても、FullGC契機に少しずつヒープ情報をロギングしてくれる優れものである。

HeapStatsはアタッチ先のJVMへのオーバーヘッドを抑えるため、対応しているマシンが対応しているCPU命令セット(sse4 or avx or いずれも未対応)に応じてインストールするrpmファイルが異なる。このクックブックを使うと、マシンに合ったrpmをダウンロードしてインストールする。

前提条件

HeapStatsの動作条件は以下の通り。

  • Linux x86/x86_64 (クックブックではCentOS or Fedoraのみ対応)
  • JDK6u18以上、JDK7、JDK8
  • OpenJDKを使うときにはシンボル情報が必須 (java-xxx-debuginfoパッケージ)
  • pcre 6.6 以上
  • Net-SNMP 5.3.1 以上

HeapStatsクックブックの使い方

JDK7、WildFly、HeapStats agentの3つをChefでインストールする例を以下に示す。

vagrantやchef、knife-solo、berkshelfのインストールについては、Web上の記事やChef 実践入門本に詳しく紹介されているので省略。vagrant-berkshelfはハマってうまくいかなかったので、素直にknife-soloでセットアップする。

1. Cookbookの作成

適当な作業ディレクトリにクックブックを生成する。

cd work
knife solo init .
2. Berksfileの編集

knife solo init .の実行により、カレントディレクトリにBerksfileファイルが作成されている。ここにjava/WildFly8/HeapStatsのクックブックへの依存を定義する。

$ vim Berksfile
source "https://supermarket.getchef.com"

cookbook 'java'
cookbook 'heapstats', git: 'git@github.com:n-agetsu/chef-heapstats.git'
cookbook 'wildfly', git: 'git@github.com:bdwyertech/chef-wildfly.git'
$ berks install

cookbook 'heapstats'で指定している部分が今回作ってみたHeapStatsクックブック。berks installにより、依存先クックブックは./cookbooksにダウンロードされる。

3. Nodeオブジェクトの作成

対象マシンに何をインストールするか定義するnodeファイルを作成する。ファイル名のcentos.jsoncentosの部分は、マシンのホスト名 or IPアドレスを指定。

run_listと各クックブックのattributeを定義する。作ってみたHeapStatsのクックブックの依存先はjavaのみで、内部的にinclude_recipe 'java'して呼び出している。*1

wildflyクックブックにおいて、HeapStatsの有効化に必要なJava起動オプション-agentlib:heapstatsを追加している。他のAPサーバの場合でも、Java起動オプションに-agentlib:heapstatsを追加できればHeapStatsは有効化される。

heapstatsのfile/heaplogfile/archivefileはHeapStatsが出力する各種ファイルの出力先を指定している。ログが一カ所にまとまるように、wildflyのログディレクトリを指定した。

$ vim nodes/centos.json
{
  "run_list": [
    "recipe[heapstats]",
    "recipe[wildfly]"
  ],
  "java": {
    "install_flavor":"openjdk",
    "jdk_version":"7",
    "openjdk_packages":["java-1.7.0-openjdk", "java-1.7.0-openjdk-devel", "java-1.7.0-openjdk-debuginfo"]
  },
  "wildfly": {
    "jpda": {
      "enabled":false
    },
    "java_opts": {
      "other": ["-agentlib:heapstats"]
    }
  },
  "heapstats": {
    "file":"/opt/wildfly/standalone/log/heapstats_snapshot.dat",
    "heaplogfile":"/opt/wildfly/standalone/log/heapstats_log.csv",
    "archivefile":"/opt/wildfly/standalone/log/heapstats_analyze.zip"
  }
}

HeapStatsクックブックで指定可能な属性についてはREADME.mdに記載している。

4. プロビジョニング

knife solo bootstrapで、対象サーバへのchefのセットアップknife solo prepareと、クックブックの実行knife solo cookの両方が動く。

$ knife solo bootstrap centos --bootstrap-version 11.12.0

centosの部分は、プロビジョニング対象マシンのホスト名を示す。IPアドレスでも可。
bootstrap-versionの指定がないとエラーになって動かない場合はがある。詳しくはstackoverflowに記載がある。

これでインストールは完了。JVMにHeapStatsがアタッチされた状態のWildFly8が起動している状態となる。

今後の課題

試しに作ってみたものの、以下のような課題があるので今後追加予定。

  • HeapStatsの各種出力ファイルへのログローテーション (logrorate.d) 対応
  • OpenJDKを使用する場合のdebuginfoインストール漏れのチェック

*1:HeapStatsにはOpenJDKのdebuginfoが必要になるが、javaコミュニティクックブックではpackageリソースにoptionsが設定できず、--enable-repo=debugが設定できなかったため、HeapStats側のレシピで/etc/yum.repos.d/CentOS-Debuginfo.repoを編集してdebuginfoのリポジトリを有効化している。fedora向けも同様。

JDK8からあるちょっと嬉しいGCログオプション

JDK8およびJDK8u20では、GCログに関連する2つの便利な機能が追加されている。いずれの機能も2014/8現在最新のJDK7 update 67 には含まれていないが、JDK7u80にてバックポートされる予定。

GCログにpidと日付を含める (JDK8より)

JAVA_OPTS="$JAVA_OPTS -Xloggc:/var/log/wildfly/gc_%p_%t.log"
  => 実際のファイル名例 : gc_pid31455_2014-08-31_14-20-16.log.0

GCログのフォーマットに%pを入れるとpid形式のプロセスIDが付与される。また%tを付与すると"_2014-08-31_14-20-16"のようにGCログファイルを作成した日付時分秒が追加される。かつてGCログはJavaを再起動すると同じファイルが上書きされて消えてしまうため、出力先を-Xloggc:gc.log.`date +%Y%m%d%H%M%S`のようにOSコマンドによってファイル名に日付を付与していたが、この機能の追加によりOSコマンドへの依存がなくなるので便利。

jcmd GC.rotate_logによるローテーション (JDK8u20より)

JDK8u20よりjcmdにGCログをローテーションさせるコマンドが追加されている。

jcmd <pid> GC.rotate_log

以前よりGCログにはサイズローテーション機能が付与されていたが、Javaの場合ログのリロード機能がないため、logrotate.dなどによる日付ローテーションが困難であった。GC.rotate_logの導入により、以下の起動オプションと組み合わせると日付契機のログローテーションが実現できる。

-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=0

UseGCLogFileRotationおよびNumberOfGCLogFilesが設定されていない状態でjcmd GC.rotate_logを実行すると、GCログローテーションが有効となっていないことを示すエラー(Target VM does not support GC log file rotation.)となる。また、GCLogFilzeSize=0を設定し、ファイルサイズ契機のローテーションを止めている。

NumberOfGCLogFilesの指定がGC.rotate_logの実行に必須であるため、 ログの世代数管理をlogrotate.dに任せるのは難しい。またファイルのコピーや世代数を超えたファイルの削除はJVMが行ってくれるため、logrotate.dを使わずにまずはお試しとして直接cron.dailyに入れてみた。

# touch /etc/cron.d/cron.daily/gclog_rotate
# vim /etc/cron.d/cron.daily/gclog_rotate

#!/bin/sh
sudo -u wildfly /usr/java/jdk1.8.0_20/bin/jcmd `cat /var/run/wildfly/wildfly.pid` GC.rotate_log

/var/run/wildfly/wildfly.pidより起動中のWildFlyのプロセスIDを確認している。WildFlyでなくても、/etc/rc.d/init.dに登録している場合は/var/run配下に現状のプロセスIDを示すファイルが出力されると思う。

まとめ

  • JDK8よりGCログフォーマットに%p(pid)や%t(時間)が含められる
  • JDK8u20よりjcmd GC.rotate_logGCログローテションできる
  • いずれも現状はJDK7に含まれていないが、JDK7u80 にバックポート予定

Hibernateのバッチ更新効果を実測してみる

Hibernateには内部的にjava.sql.Statement.addBatch()およびexecuteBatch()を使ってバッチ更新でSQLを発行するオプションhibernate.jdbc.batch_sizeがある。
デフォルトは無効となっているが、1トランザクションで大量のエンティティを登録する処理において、どれくらい効果的か実測してみる。

テスト環境

WildFlyMac上で起動、DBはVirtualBox上のCentOSで起動しているので、取得できるデータはあくまで参考値レベル。綺麗な検証環境ではない。

  • Mac OS X (Core i5 1.7GHz)
  • OracleJDK 1.7.0_60
  • PostgreSQL9.3
  • WildFly8.1.0 final (Hibernate4.3.5バンドル)

テスト内容

1トランザクションでのエンティティ登録数を10、100、500、1000、3000、5000、10000エンティティと数を増やしていき、batch_size=30を設定した場合としない場合の処理時間を比較する。

テストコード

一括エンティティ登録するEJB。大量エンティティ登録時にHibernate一次キャッシュによるOutOfMemoryErrorとならないように、バッチサイズに合わせてflush&clearでDBにSQLを発行させてキャッシュをクリアした。

@Stateless
public class CustomerService {
    
    @PersistenceContext(unitName = "PostgresPU")
    private EntityManager em;
    
    public void bulkimport(int cnt) {
        for (int i = 0; i < cnt; i++) {
            Customer c = new Customer("test user" + i, "tokyo");
            em.persist(c);
            if (i % 30 == 0) {
                em.flush();
                em.clear();
            } 
        }
    }
}

シンプルにサーブレットからEJBを呼び出して時間を計測。

@WebServlet("/BulkImport")
public class BulkImportServlet extends HttpServlet {

    @Inject
    private CustomerService customerService;
    
    protected void processRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        long start = System.currentTimeMillis();
        customerService.bulkimport(10);
        long finish = System.currentTimeMillis();
        
        long time = finish - start;
        System.out.println("time : " + time);
        
        response.setContentType("text/plain;charset=UTF-8");
        try (PrintWriter out = response.getWriter()) {
            out.println("import done.");
        }
    }
    // 省略 ...

persistence.xmlは以下のようにシンプルな設定。batch_sizeなしのテストをするときは、hibernate.jdbc.batch_sizeをコメントアウトした。

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
             http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">
    
  <persistence-unit name="PostgresPU">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
      <jta-data-source>java:jboss/jdbc/PostgresDS</jta-data-source>
        <properties>
          <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQL9Dialect" />
          <property name="hibernate.hbm2ddl.auto" value="create-drop" />
          <property name="hibernate.jdbc.batch_size" value="30" />
        </properties>
  </persistence-unit>
</persistence>

測定結果

あくまで1つの参考値に過ぎないが、batch_size=30の設定でエンティティの一括登録が早くなることがわかる。

(処理時間の単位はミリ秒)

一括登録エンティティ数 batch_size=30 batch_sizeなし
10 321 297
100 417 504
500 1432 1878
1000 2023 2888
3000 4262 5331
5000 8999 12048
10000 13196 30902

f:id:n_agetsuma:20140702211948p:plain

まとめ

一度に大量のエンティティを追加・更新することが事前にわかっている場合は、hibernate.jdbc.batch_sizeの設定は効果がありそう。エンティティの操作数が数十〜多くても1000エンティティくらいのWebアプリケーションではそれほど意識しなくても良いと思う。

メモ

addBatch()とexecuteBatch()の仕組みを知ろうとpostgresql-jdbc-9.3-1101のソースを眺めていて気がついたが、addBatch()はスレッドセーフではないので、素でJDBCを使って1つのStatementオブジェクトにマルチスレッドでaddBatch()すると、意図しない動作になりそう。

詳細はorg.postgresql.jdbc2.AbstractJdbc2Statement.javaのaddBatch(String p_sql)を参照。まだaddBatch()が一度もされていないかのnullチェックが同期化されていないため、タイミングによっては二重初期化されて登録済みのSQLが消えそうに見える。

JPAで少しずつデータを処理する方法を考える

OutOfMemoryErrorの主な要因例として、DBMSからデータを取得しすぎがあります。

LASYフェッチによるN + 1 問題を回避するために、結合先テーブルの要素を一気に持ってくるJOIN FETCHを使ったところ、引き換えにJavaヒープ使用量が多くなるのはよくあるケースです。

以下のような、1つのIssueに対して複数のIssueAttributeを持つ1対多関連のエンティティの操作を例に、OutOfMemoryErrorを少しでも避ける方法を考えてみます。

@Entity
public class Issue implements Serializable {
    private static final long serialVersionUID = 1L;
    
    @Id
    @GeneratedValue
    private int issueId;
    private String title;
    
    @OneToMany
    @JoinColumn(name = "issueId")
    private List<IssueAttribute> attributes = new ArrayList<>();

ページングして少しずつ取ってくる

JPAではHibernate固有のScrollableResultsのようなAPIがないため、SQLのoffsetを示すsetFirstResult()とlimitを示すsetMaxResult()を組み合わせてページングします。後述していますが、JOIN FETCHではsetFirstResult()とsetMaxResult()の組み合わせが効かないため、普通のJOINを使っています。

private static final int FETCH_SIZE = 100;
// 途中省略 ...
String sql = "SELECT i FROM Issue i JOIN i.attributes ia";
int offset = 0;
List<Issue> chunk = pagingQuery(em.createQuery(sql), offset, FETCH_SIZE);
while (!chunk.isEmpty()) {
    for (Issue i : chunk) {
        for (IssueAttribute ia : i.getAttributes()) {
            // なんらかの処理
        }
    }
    // ページングによって取得したエンティティをデタッチ
    em.flush();
    em.clear();
    offset += FETCH_SIZE;
    chunk = pagingQuery(em.createQuery(sql), offset, FETCH_SIZE);
}

private List<Issue> pagingQuery(Query query, int offset, int limit) {
    query.setFirstResult(offset).setMaxResults(limit);
    return query.getResultList();
}
@BatchSizeの設定

JOINでは結合先テーブルのエンティティがLASYフェッチになってしまうため、N + 1問題のように関連先の要素を取得するSQLが多く発行されてしまいます。Hibernate固有の機能ですが@BatchSizeを使うと、関連先エンティティ取得SQLの実行回数を減らすことができます。

@Entity
public class Issue implements Serializable {

    @OneToMany
    @JoinColumn(name = "issueId")
    @org.hibernate.annotations.BatchSize(size = 100)
    private List<IssueAttribute> attributes = new ArrayList<>();

LASYフェッチでは1つのIssueごとに関連先のIssueAttributeを取得するSQLが実行されますが、上記のように@BatchSizeを設定した場合、100個のIssueエンティティごとに関連先のIssueAttribute取得するようになります。

JOIN FETCH ではページングがうまくいかない

手元のHibernate4.3.5&PostgreSQL9.3ではJOIN FETCHとsetFirstResult()およびsetMaxResult()を組み合わせると、以下のような警告が出力され、offsetとlimitが投げられるSQLに反映されずに一気にデータを取ってしまい、意図したようにページングができません。

19:28:32,647 WARN  [org.hibernate.hql.internal.ast.QueryTranslatorImpl] (default task-1) HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
19:28:32,648 INFO  [stdout] (default task-1) Hibernate: /* SELECT i FROM Issue i JOIN FETCH i.attributes ia */ select issue0_.issueId as issueId1_1_0_, attributes1_.attributeName as attribut1_2_1_, attributes1_.issueId as issueId2_2_1_, issue0_.priority as priority2_1_0_, issue0_.reporter_id as reporter4_1_0_, issue0_.title as title3_1_0_, attributes1_.attributeValue as attribut3_2_1_, attributes1_.issueId as issueId2_1_0__, attributes1_.attributeName as attribut1_2_0__, attributes1_.issueId as issueId2_2_0__ from Issue issue0_ inner join IssueAttribute attributes1_ on issue0_.issueId=attributes1_.issueId

JPA2.1仕様では、fetch joinとsetMaxResultsまたはsetFirstResultの併用時の挙動は定義しないとされています。

The effect of applying setMaxResults or setFirstResult to a query involving fetch joins over collections is undefined.

3.10.7 QueryExecution

StackOverFlowにこのWARNに関するQ&Aがあり、何行とってくるとエンティティが組み立てられるかわからないので、とりあえず毎回全部取ってくる実装になっているようです。

エンティティをこまめにデタッチする

JOIN FETCHをしながら少しでもJavaヒープの消費を抑えるには、EntityManager.clear()を使って少しでも処理済みの不要なエンティティをデタッチする方法もよく見かけますが、効果は限定的です。

String sql = "SELECT i FROM Issue i JOIN FETCH i.attributes ia";
// ↓ ここでヒープ消費が最大になる場合では効果薄
List<Issue> issues = em.createQuery(sql).getResultList();
int i = 0;
for (Issue issue : issues) {
    for (IssueAttribute ia : issue.getAttributes()) {
        // ここの処理が時間がかかったり、
        // この中でたくさんオブジェクトを展開する場合は効果あり
    }
    i++;
    if (i % 1000 == 0) {
        em.flush();
        em.clear();
    }
}

getResultList()を実行した行で、既に取得した全てのエンティティがJavaヒープに展開されているため、この後でclear()しても最大消費量を抑えることはできません。取得したデータを処理する段階で、時間がかかったり、たくさんオブジェクトを展開する必要がある場合であれば効果が得られると思います。

補足 LasyInitializationException

@OneToManyのデフォルトやJPQLのJOINによって行われるLASYフェッチを使用してた場合、処理の途中でEntityManager.clear()を実行すると、以下のような例外を招きます。

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: net.agetsuma.jpatest.entity.Issue.attributes, could not initialize proxy - no Session

まとめ

  • setFirstResult()とsetMaxResult()を組み合わせてページングする
  • ページングとJOIN FETCHは相性が悪い
  • そのため普通のJOINを使うが、N + 1問題を防ぐためにHibernate固有機能の@BatchSizeを併用する
  • EntityManager.clear()でこまめにデタッチ方法は効果が限定的

いろいろとハマったので、ページングに関する仕様がJPAに入ると嬉しいなと思う、今日この頃です。