Sunday, July 28, 2013

JavaとキャッシュとMixer2

激辛担担麺ランチ食ったら目が覚めるどころかかえって猛烈な睡魔に襲われております。

さて、今日はキャッシュ技術の話。先に言っておきますが、すごく長いです。そして大した結論ではないです。

ソフトウェア、ハードウェアの性能向上にはキャッシュ技術は欠かせません。これは開発言語も分野も問いません。 CPUにだってキャッシュ的な機能要素はあります。WebアプリではDBの検索結果をキャッシュして使いまわすことで DBの負荷を下げ全体の性能を上げるというのは常套手段です。

キャッシュとkey-valueストアとがごっちゃに論じられる場合もあります。実質的には同じようなことなのでどうでもいいんですけどね。 key-valueストアの代表格の一つにmemcachedがありますが、読んで字のごとく"キャッシュ"ですし。

これだけ便利で普及しているキャッシュという技術ですが、いざ使おうとすると結構面倒です。 どんどんキャッシュにつっこんでいたらキャッシュストアがあふれてしまって容量オーバー。よくある話です。 古いものから自動的に消えるようになってるから容量オーバーの心配無しとか言ってたら、 それがキャッシュヒット率の低下を招いていしまってキャッシュ機構の存在意義がなくなったり。 本物のデータのほうを変更したにもかかわらずキャッシュのデータのほうを変更あるいは削除しそこねてバグったり。 また、キャッシュしたいモノとそれにひもづけるキャッシュキーは正確に一対一でないと 間違ったキャッシュデータをキャッシュに保存し、 あるいは間違ったデータをユーザーにお届けしちゃうことになります。 何をキャッシュキーにするか?は慎重に考えねばなりません。

Javaアプリの場合はさらに面倒なことがあります。

キャッシュしたくなるようなデータは、多くのケースでは単なるString(例:JSON形式)だったりintやlongだったりします。その場合ディープコピーかシャローコピーかを意識する必要がありません。しかしその調子でやると、Javaクラスのインスタンス=たとえばいわゆるPOJO的なもの=をキャッシュ対象とする場合の落とし穴に気づかないことがあります。

二人のユーザー(≒二つのスレッド)が同時に同じデータをキャッシュから取得しようとしたとき、 キャッシュは二人に「同じデータ」を返します。 しかしJavaの場合は変数のコピーは基本的にリファレンス(参照)に過ぎないので、 キャッシュから受け取ったデータを一人が加工するともう一人のほうに渡されたデータも変わってしまいます。不思議と言えば不思議、当然と言えば当然の現象です。 それをよしとする要求ならそれでよいのですが、Mixer2のhtmlテンプレートロード機能の性能UPのためにキャッシュを使うという要求なのであれば話は別です。

AさんとBさんがほんの数マイクロ秒差で同じページにアクセスしたとしましょう。Aさん用画面を処理するスレッドがキャッシュから取得したHtml型インスタンスに「こんにちはAさん」を埋め込んだ直後、別スレッドがキャッシュから受け取った同じインスタンス(のリファレンス)に「こんにちはBさん」を埋め込んでしまう可能性が発生するのです。それはまずい。

Javaで実装されたキャッシュ機構として有名なものにEHCacheがあります。そのドキュメントにはこんなことが書いてあります。日本語訳by私なのであまり信用しないように(笑)

Ehcache | Documentation | Using Ehcache

Copy Cache

A Copy Cache can have two behaviors: it can copy Element instances it returns, when copyOnRead is true and copy elements it stores, when copyOnWrite to true.
コピーキャッシュパターンには二つの方法があります:

  • copyOnReadがtrueのとき、エレメントのコピーを戻り値として返す。
  • copyOnWriteがtrueのとき、エレメントのコピーをキャッシュに保存する。
注:エレメント≒キャッシュに保存しているデータのこと

A copy on read cache can be useful when you can't let multiple threads access the same Element instance (and the value it holds) concurrently. For example, where the programming model doesn't allow it, or you want to isolate changes done concurrently from each other.
同じエレメントのインスタンス(とそれが内部に持つ値)に対し、複数のスレッドがアクセスすることを許可したくない場合、キャッシュ読み込み時のコピー設定を使うとよいでしょう。たとえばプログラミングモデルがそれを許さない場合や、エレメントを確実に隔離しつつ同時発生的な変更を可能にしたい場合です。

Copy on write also lets you determine exactly what goes in the cache and when. i.e. when the value that will be in the cache will be in state it was when it actually was put in cache. All mutations to the value, or the element, after the put operation will not be reflected in the cache.
キャッシュ保存時のコピー設定を使うと、どんなデータがいつキャッシュに保存されたのかを厳密に把握することができます。キャッシュに保存されているものは保存した時点の状態のままです。エレメントに対するどんな変更も、それがキャッシュへのput処理の後のことなのであればキャッシュの中のエレメントには反映されません。

A concrete example of a copy cache is a Cache configured for XA. It will always be configured copyOnRead and copyOnWrite to provide proper transaction isolation and clear transaction boundaries (the state the objects are in at commit time is the state making it into the cache). By default, the copy operation will be performed using standard Java object serialization. We do recognize though that for some applications this might not be good (or fast) enough. You can configure your own CopyStrategy which will be used to perform these copy operations. For example, you could easily implement use cloning rather than Serialization.
コピーキャッシュパターンの具体例はXA(分残トランザクション)向きに設定されたキャッシュでしょう。 copyOnReadとcopyOnWriteを常にオンに設定することで、適切なトランザクション隔離と明確なトランザクション境界を提供します (コミット時のオブジェクトの状態はキャッシュに入れるときの状態と同じであるということ)。 デフォルトでは、コピー処理ではJavaオブジェクトのシリアライゼーションが使用されます。もちろん、アプリケーションによってはそれは良い(or速い)方法ではないことを、我々は認識しています。独自のCopyStrategyを設定しておけば、お望みのコピー処理方式を使用することができます。 たとえば、シリアライゼーションではなくクローン処理を使うように簡単に設定できます。

Ehcache | Documentation | Configuration

copyOnRead and copyOnWrite cache configuration

A cache can be configured to copy the data, rather than return reference to it on get or put.
キャッシュからgetまたはputするときにデータのリファレンスではなくコピーを使うかどうかを設定できます。
(途中略)
The default configuration will be false for both options.
デフォルトではcopyOnRead,copyOnWriteの両方ともfalseになっています。

長々と引用してしまいましたが、重要なポイントがさらりと書いてあります。

  • コピーを使うように設定したとしても、ほっとくとserializationを使うので場合によっては性能が悪かったりちゃんとディープコピーできなかったりするかもですよ。ちゃんとCopyStrategyも実装/設定してね。
  • デフォルトでは、ディープコピーじゃなくてリファレンスを、キャッシュから返したりキャッシュに保存したりしますよ。

EHCacheがイケてないということではもちろんありません。 このへんが汎用キャッシュライブラリとしてできる限界なのでしょう。 CopyStragetyパターンに沿ってディープコピーの方式を好きに実装&設定できるのですからむしろ高機能とも言えます。

ところで、キャッシュサーバやそのクライアントライブラリは今やいろんな実装があります。 しかしコレといった共通の規格がありません。そこで、Java言語用の共通APIが規格されつつあります(2013/7現在まだドラフト)。 それがJSR-107 JCacheです。ちなみに、JSR-107の中の人はEHCacheの中の人のシャローコピーです。

さて、ここでようやくMixer2の話になります。

テンプレートエンジンの多くがキャッシュ機能を持っています。velocityしかり、thymeleafしかりです。 同じ画面を表示するために同じテンプレートをパースする処理を繰り返すくらいなら、 パース処理後の状態をキャッシュに保存しておけばそのぶんの処理を省略できる=性能がよくなる、という寸法です。 多くの場合はキャッシュエンジンの内部にMapかなにかでキャッシュを維持しているだけです。

もちろん独自実装も使えるようにするためにインターフェイスを提供していることもあります。

Mixer2もこれらにならって独自のキャッシュインターフェースを提供することも考えたのですが、あえてそうはしませんでした。 代わりに選んだのがJSR-107 JCacheです。このinterfaceクラスを持っているjarファイル名で言うとcache-api-0.8.jar(数字はバージョンなので変わる可能性あり)です。

先日リリースしたmixer2-cachableでは、 EHCacheのような汎用キャッシュライブラリを使うか、あるいは独自に実装するかによらず、とにかくjavax.cache.Cache(通称JCache)として提供されるinterfaceを実装したクラスをプラグインして使えるようになっています。また、同梱したCache実装をデフォルトで自動的に使用可能なので、それでよければそれ以上頭を使う必要もありません。

CachableMixer2Engineクラス経由で提供されるloadHtmlTemplate()やその種のメソッドは、設定したCache実装を通してhtmlテンプレートのロードが可能です。何より重要なのが、必ず、キャッシュのget/putの過程ではHtml型インスタンスのディープコピーを使うように実装済みであることです。しかも、高速かつ正確なcopy()メソッドを使っています。

これによって、たとえばEHCacheを使ったときでもcopyOnWrite/copyOnReadのon/offに関係なく確実に高速ディープコピーが使われるので、キャッシュ上のデータを安全かつ高速に使うことができます。CopyStrategyに沿って実装をどうのこうのと考える必要もありません。

また、CacheableMixer2Engineはロードしようとするテンプレートそれ自体のSHA-1ハッシュを計算してそれをキャッシュのキーとして使っています。キーの生成方法としてはとても安全なのですが、実はそれが性能上の欠点でもあります。せいぜい数十kbyte程度であろうHTMLテンプレート文字列のSHA-1を計算すること自体は大したコストではないのですが、テンプレートがFileに入っている場合、それを全部String等に読み込むコストが発生してしまうのです。 しかしこれは、Mixer2のテンプレートロード機能は、テンプレートの型としてjava.io.FileのほかInputStreamにもStringにも対応しているので、どれであっても同じようにキャッシュ(のキー)を扱えるようにするための措置でもあります。このあたりは今後、キャッシュキー生成の高速性と安全性と簡易性を両立させるうまい実装が思いつけばどうにかなるかもしれません。次のバージョンアップにご期待ください(いつだ?)。

ところで、キャッシュ機構といえばこんなのもあります。

つまり、DIコンテナが、というか、Javaアプリケーションフレームワーク自体がキャッシュ機構を提供しているケースです。 こういうものとMixer2を組み合わせる場合はmixer2-cacheableなんてほっといてフレームワークに合わせるのもよいでしょう。

そんなわけで、Mixer2のテンプレートロード機能にキャッシュを使いたい場合は、以下の3つのパターンにまとめることができます。

  • mixer2-cacheableをデフォルトのまま使う。数十kbyte程度の小さなテンプレートが数百種類、という程度の要求仕様であれば十分でしょう。
  • mixer2-cacheableを使いつつ、cacheオブジェクトはJCache(JSR-107)を実装した別のものを使う。CacheableMixer2Engine.setCache(別のキャッシュインスタンス) のようにすれば差し替えられるようになっています。(DIコンテナでの設定例はこちら
  • mixer2-cacheableは使わない。Mixer2Engineを継承した別クラスを独自実装し、そこにキャッシュ機構を組み込む。あるいはアプリケーションフレームワーク提供のキャッシュ機構を経由するようにする。

そして究極の選択は、テンプレートのキャッシュなんて気にしないことです。Mixer2のテンプレートロード機能は結構速いです。 かなり大きなテンプレートであっても一桁ミリ秒で済んでしまいます。ならば、100msec単位のコストがかかりがちなほかの機能(外部通信、DB検索等)をチューニングするほうがよほど性能向上を見込めるでしょう。

Saturday, July 27, 2013

mixer2とSpringMVC用のViewResolver

mixer2-cachableとともにmixer2-1.2.10もリリースしました。

mixer2-cachableやその他のキャッシュ実装との連携をしやすくするための手直しが入っているほか、 もう一つ重要な機能が追加されています。 SpringMVC用のViewの基底クラスと、ViewResolverです。

これまで、SpringMVCでmixer2を使う場合にはControllerクラスの各メソッドでHtmlを組み立ててMixer2XhtmlViewに仕込んで直接返す、つまりメソッドの戻り値の型がMixer2XhtmlView、という方法を推奨していました。去年書いた下記の記事なんか、もろにそれです。

ところが、この方式、コントローラ自体がビューのテクノロジに依存することになってしまうため、ビューの差し替えが難しくなる=MVCの分離が容易というSpringMVCの良さを殺してしまう=ということになります。

SpringMVCのコントローラのメソッドの戻り値は、

  • Viewの名前≒ViewResolverが解釈しうるString値
  • ModelAndviewまたはそれを拡張したクラスのインスタンス(org.springframework.web.servlet.ModelAndView)
  • 独自のView実装(つまりorg.springframework.web.servlet.Viewをimplementsしたもの)のインスタンス
のどれでもかまわないという柔軟性の高さは素晴らしいです。しかしそれに甘えすぎるのもよくないわけです。

そこで登場するのがViewResolverインターフェイス(org.springframework.web.servlet.ViewResolver)です。 これをきちんと実装すると、コントローラはViewではなくViewの名前をStringで返すだけでよくなります。あとはViewResolverにおまかせです。

今回、こういったことをきちんと解決するために、 Mixer2XhtmlViewResolverAbstractMixer2XhtmlView をそれぞれ追加しました。使い方はそれぞれのjavadocの冒頭にあるとおりです。

また、SpringMVC用のアーキタイプで自動生成されるHelloWorldプロジェクトや、 フルーツショップサンプルアプリケーションも、すでにMixer2XhtmlViewResolverを使った形にリファクタリングされています。合わせて参考にしてください。

mixer2-cacheableをリリースしました。

mixer2-cacheableをリリースしました。 ソースはこちら。 ドキュメントはjavadocくらいしかないので、とりあえずここでざっくり書いときます。

maven使いの方はこんな感じ。 (追記:cache-apiとmixer2本体も別途指定が必要ということを忘れてましたので今朝加筆。) それ以外の方はいつものようにhttp://mixer2.org/dist/にあるアーカイブを使ってください。 mixer2-cacheable-1.0.1-jars.zip にはmixer2本体や依存jarも同梱されています。

単なるMixer2Engineのラッパーなので、使い方は簡単です。 いままでorg.mixer2.Mixer2Engine を使っていた部分を CacheableMixer2Engine を使うようにするだけです。

たとえばSpringMVC上でMixer2EngineをこんなふうにDIしていた場合、
変更前:
変更後:
とするだけです。

エンジンが読み込もう(loadHtmlTemplate/checkAndLoadHtmlTemplate)とするxhtmlテンプレート文字列のSHA-1ハッシュ値が自動的にキャッシュのキーとして使用されます。

デフォルトでは、同梱のorg.mixer2.cacheable.SimpleCacheが使用されます。 これはjavax.cache.Cache(JSR-107)仕様に対するごく簡単な実装になっています。 キャッシュの格納先としてConcurrentHashMapを使っているだけです。

JSR-107準拠のキャッシュ実装に差し替えることももちろん可能です。 たとえばSpringMVCを使っている場合、 これでCachableMixer2Engineに指定のキャッシュ実装のインスタンスがDIされます。

気になる性能ですが、これはhtmlテンプレートのサイズによってだいぶ異なります。 だいだい2倍から4倍程度の性能向上は見込めるでしょう。 ただし、mixer2がテンプレートのロードで頼っているJAXBのunmarshalerそれ自体が結構速いので、 たとえば80msecかかっていた処理が10msecになるみたいな劇的なことにはなりにくいです。 もともと6msec程度で済んでいたテンプレートロードが2msecになるとか、そのくらいの効果ととらえるほうがよいでしょう。

Friday, July 26, 2013

syntac highligher 壊れた

このブログのsyntax highliterのセッティングをおかしくしてしまったみたいです。コードのサンプルを貼り付けた記事がなんかぐちゃぐちゃに。。。

Monday, July 22, 2013

mixer2 version1.2.8をリリースしました

mixer2 1.2.8をリリースしました。 このリリースでは、Mixer2Engineに新たに checkAndLoadHtmlTemplate() メソッドを追加しました。

大きな違いはテンプレートのロードに失敗したときの挙動です。たとえば、 本来なら<br />と書くべき改行タグを<br>と書いてしまったxhtmlをロードしようとすると、

  • 従来のloadHtmlTemplate()は、nullを返す。例外情報はログに出るだけ。 これはテンプレートは正確なものが渡されるという前提の場合に有用です。
  • checkAndLoadHtmlTemplate()は、Mixer2JAXBExceptionをスローする。 これはテンプレートが間違っている可能性も考慮しなければならない場合に有用です。

Mixer2JAXBExceptionは単なるJAXBExceptionのラッパーです。 xhtmlのパースエラーの情報を得やすくするために、いくつかのメソッドを追加してあります。 詳しくはjavadocをご覧ください。

Tuesday, July 16, 2013

mixer2 version1.2.7をリリースしました

version 1.2.7をリリースしました。mavenのセントラルリポジトリからいつでもダウンロード可能です。 pomの書き方はこちら。 非maven使いの方のためのzipもいつもどおりこちらからダウンロード可能です。

1.2.7は次のような特徴があります。

  • copy()メソッドの高速化。
  • 新たにjaxb2-basics-runtime-0.6.4.jarへの依存性の発生
  • commons-langへの依存の廃止

copy()メソッドの高速化により、タグ型オブジェクトの使いまわしがより高速化されます。 また、loadHtmlTemplate()した結果のHtml型インスタンスをそのままcopy()して使いまわせば、 テンプレートキャッシュ機能の実装も簡単でしょう。 簡易的な性能測定では、中程度の量のhtmlテンプレートをloadHtmlTemplate()するのと、 そのHtml型インスタンスをcopy()するのとでは、copy()のほうが5倍から6倍程度早いという結果が出ています。

ただし、clone()メソッドやcopyTo()メソッドは使用しないようにしてください。 html5のaria-*属性やdata-*属性をうまくディープコピーに含められないというバグがあります。 copy()メソッドはそのようなバグはありませんのでcopy()を使用してください。

Monday, July 15, 2013

mixer2のサンプルアプリのプロジェクト分割バージョン

大きなWebアプリをチーム開発する場合、プロジェクトの分割がポイントになります。 そのためにServlet3.0のWeb fragmentのような仕様もあるのですが、 JSPを使う限り、htmlをjspに置き換えるという面倒な作業はまぬがれません。

mixer2をうまく使うと、プロジェクトの分割も比較的容易になります。 そんな話を今年1月のSpring勉強会にてお話した のですが、そのとき、出来上がっていたはずのデモの起動に失敗するという失態(笑)。

そんな失敗を糧に、改めてちゃんと動くように作った(はず)のがこちらです。 https://github.com/nabedge/mixer2-sample/tree/master/m2fruitshop mixer2-sampleリポジトリをcloneして、その内部のm2fruitshop配下の各ディレクトリを、mavenプロジェクトとしてIDE(eclipse等)にインポートしてください。m2fruitshop-webを起動すれば動くはずです。

少々説明が雑ですが、お暇な方はチャレンジしてみてくださいね。

Tuesday, July 9, 2013

mixer2のsnapshotバージョンの使い方

mavenプロジェクト限定ですが。

mixer2の正式バージョンはmavenのセントラルリポジトリに登録されていますので、 pom.xmlファイルにdependencyタグを書けば、特に何も考えなくてもjarとその依存jarは自動的にダウンロードされてくれます。

しかし今回のようにバグが発覚し(汗)、それを直している最中のSNAPSHOTバージョンを使いたい場合は、 pomにsnapshot用リポジトリを書いておけばよいです。こんな感じです。


    4.0.0

    com.example
    project-name
    0.0.1-SNAPSHOT

    
        
            sonatype.oss.snapshots
            Sonatype OSS Snapshot Repository
            http://oss.sonatype.org/content/repositories/snapshots
            
                false
            
            
                true
            
        
    

    
        
            org.mixer2
            mixer2
            1.2-SNAPSHOT
        
    

2013年7月現在、mixer2は1.2系統として開発されています。mixer2では、pomのversionタグをいちいち書き変えるのが面倒なので、 x.y.z-SNAPSHOTではなくx.y-SNAPSHOTの形式で開発を続けています。 いまの最新正式バージョンが1.2.6ですので、現在の1.2-SNAPSHOTは1.2.7になる予定の最新開発バージョンだと思ってください。

Sunday, July 7, 2013

mixer2-1.2.6でhtmlタグのオブジェクトのディープコピーの性能が向上しました

追記
1.2.6がバグってました。すいません。1.2.6のcopy()やclone()メソッドは、html5タグのdate-*属性やaria-*属性をうまくコピーできません。 他の一般属性は大丈夫です。直し方のアイデアはあるのですが、ちょっと時間が取れません。7月中にはなんとかしますので、 それまでは1.2.5以前をご利用ください。はー、テストが甘かった。反省します。

mixer2-1.2.6をリリースしました。数時間後にmavenセントラルリポジトリから使用可能になります。 maven使わない派の方のためにjarのセットがこちらにあります

リリース頻度が早すぎるので少し落ち着かせる、と宣言してから1か月ちょいしかたっていませんが、 気になっていた部分の改善ができてしまったので、そこは、まあ。(^^;

気になっていたのはcopy()メソッドの性能です。これまではよく見かけるserialize/deserializeを駆使した手法を使っていたので、どうしてもディープコピー処理に時間がかかっていました。 1.2.6では、各htmlタグにマッピングされる全てのクラスにclone()、hashcode()、equals()メソッドが追加されています。 従来のcopy()メソッドは内部ではclone()を呼び出しているだけになっています。もちろん正確にディープコピーします。

これにより、htmlテンプレートをloadHtmlTemplate()した結果のHtmlオブジェクトをキャッシュしておき、二回目以降はキャッシュのディープコピーで済ませることで、アプリケーションの性能を高めやすくなります。従来はcopy()メソッドが重かったため、どんなキャッシュ機構を作っても大した効果は得られませんでした。

ごく簡単なキャッシュ機構を作って試したところ、いちいちloadHtmlTemplate()するよりも30%前後は性能がよくなりそうです。もちろんhtmlテンプレートの内容によって数値は異なるでしょう。また、htmlテンプレート全体ではなく、その一部のタグをcopyしながら使いまわすような処理を書く場合でも、この性能向上の恩恵は受けられます。

それから、キャッシュキャッシュ言ってますが、逆に、1.2.5まではあった、javax.cacheインターフェースを使ったキャッシュ機構を、1.2.6では完全に廃止しました。@depricatedだったメソッドも完全に削除されています。これはキャッシュ機構はmixer2本体に持たせるよりも、使う側で独自に作ってもらうほうがよいと判断したためです。mixer2が依存するjarは本当に必要最低限にしておくほうがライブラリとして使いやすいはずです。 もしやるとしても、たとえばmixer2-cachewrapperみたいな別プロジェクト/ライブラリとしてキャッシュ機構を付加したラッパーライブラリを提供する形になるでしょう。(誰か作ってください^^;)

さて、さすがにもう眠いんで寝ます。