JNLP Performance Tuning with Pack200

今時どうかとは思うんだけど、今参画しているプロジェクトでJava Web Startのアプリを提供している。先日これをユーザーテスト環境へ配ったところ「以前のバージョンと比較して遅い」と苦情がでているそうで、パフォーマンスチューニングをさせられるはめに。

そもそもパッケージソフトについているライブラリのサイズが前バージョンよりも全然でかいので、同じ時間で出せるわけねーだろ、とかフツーの人なら考えるのだが。そういう事実をお客さんに何も説明しないまま「お客さんが遅いって障害票あげているからなんとかしろ」って話を一方的に受けてくるのはエンジニアとしてどうかと思うんだが、まぁ俺はその場にいなかったので今更騒いでも後の祭り。こんな感じで中身がよく分かってない人たちがものをつくったり運営したりすると、まじめに調べて仕事している方はいろいろ納得いかないことが多くて、ほんとストレスがたまる。

でもまぁ、そんな納得のいかない話を黙って受けたのは、その原因にまったく思い当たらないわけでもなかったから。このアプリはJavaのセキュリティでフルアクセスが必要なのでjarsignerの署名が必須なのだけど、実は対象ファイルの数が数百と多く、サイズも大きいため(全部で300MBくらい)、Deploymentの際に署名したJARを準備するのがかなり面倒くさい。それを今まで手作業で回していたのだから、署名し忘れたりとかクライアントとサーバーでバージョン違いのものが置かれていて「Unmarshalできねーよ例外」がでるとか、リリース関連の問題にもなっていたわけで。

今回はそこら辺を改善するために、JNLP/JARダウンロード用のServletを書いた。こいつは初回アクセス時にサーバー側のJARを持ってきてコッソリjarsignerを呼び出して署名をつけ、クライアントに送りつける仕組み。なので万が一署名し忘れても裏で勝手に署名されるからエラーになることはほとんどないし、サーバー側とバージョンがずれることもない。ただし、署名している時間がかかることは当然として、さらには既に署名済みJARが存在するか、存在する場合は前に署名されてから元ファイルが更新されていないか(再署名が必要か)、などのいくつかのチェック処理が入るので、単純に要求されたファイルをWebサーバーからストリームするのと比較すると、あちらこちらのファイルを見に行かなければならない分、多少オーバーヘッドがある。

JARに署名する時間はJarSignerクラスに任せている関係でほとんど調整の余地がないので、他の部分で埋め合わせするしかない。それで署名が終わった後のダウンロード時間を短縮するいい方法はないかと調べていて見つけたのが、署名済みJARをそのままダウンロードさせる代わりに、Pack200フォーマットというより高圧縮な形式を並行で用意することでダウンロードするファイルのサイズを大幅に削減する方法。実はJava 1.5の時代からあったらしい。全然知らなかった。そういえばeclipseの更新サイトに転がっていたよpack.gz。あれか。

Pack200形式と署名付きJAR

Pack200形式のファイルを作るにはJARを作ってからpack200コマンドにjarファイルを食わせれば良い。長年JavaやってるけどJDKにそんなコマンドあったのか!って感じでした、俺。

こんな感じか。

[~] cd classes
[~/classes] jar cf ../hoge.jar *
(略)
[~/classes] cd ..
[~] pack200 hoge.pack.gz hoge.jar

Pack200というのはJava特有の最適化を行うことで、JARの中身を不可逆圧縮する仕組みらしい。もちろん不可逆といってもJPEGとかMP3のように劣化してしまうのではなく、Java的に見て意味が変わってしまうような変更はされない。ただ、圧縮率を稼ぐために各種並べ替え等が発生し、バイナリレベルではかなり変わってしまう。これではバイナリに対してデジタル署名しているJARをpack200にかけるとせっかくのデジタル署名が全部無効になってしまい、使い物にならない。

JARのNormalize

署名済みJARのデジタル署名を壊さないためには、JARのnormalize作業が必要になる。一度Pack200形式に圧縮して、それをunpackすると元のJARファイルが出てくるわけだけど、このときに出てくるJARファイルはpack200向けに最適化されたレイアウトに変更されている(だから不可逆圧縮なんですね)。このことをnormalizeするという。これをもう一度pack200しなおしてもレイアウトの変更は行われないので、つまるところ、normalizeされたJARに対してjarsignerを実行すれば、pack200しても署名が無効にならずに済むというわけだ。

とはいっても、normalizeするだけのためにpackして即unpackするのも馬鹿馬鹿しい。だからそういうことをしないでも済むように、pack200には–repackというオプションが用意されている。これをつけて呼び出すとpack.gzを作るのではなく、JARファイルのnormalizeだけを実施する(–repackの場合pack.gzは作成されない)。

[~] ls -al commons-cli-1.0.jar
-rw-r--r--  1 debugjunkie  staff  30117  7 13 18:24 commons-cli-1.0.jar
[~] pack200 --repack commons-cli-1.0.jar
[~] ls -al commons-cli-1.0.jar
-rw-r--r--  1 debugjunkie  staff  29754  7 13 18:25 commons-cli-1.0.jar

この後にjarsignerを呼べば良い。

[~] jarsigner -keystore hoge.key -storepass hogepass commons-cli-1.0.jar hoge

んで、最後にpack200。こんな感じで、笑っちゃうくらい小さくなる。

[~] pack200 commons-cli-1.0.pack.gz commons-cli-1.0.jar
[~] ls -al commons-cli-1.0*
-rw-r--r-- 1 debugjunkie staff 32903 7 13 18:30 commons-cli-1.0.jar
-rw-r--r-- 1 debugjunkie staff 13912 7 13 18:31 commons-cli-1.0.pack.gz

署名もちゃんと検証可能。

[~] jarsigner -verify commons-cli-1.0.jar
jar verified.

Warning:
This jar contains entries whose certificate chain is not validated.

Re-run with the -verbose and -certs options for more details.

ただしjpgとかpngとかリソース系のものがたくさん入っているJARファイルだとpack200はむしろ逆効果で、倍以上のサイズになったりする場合もある。だから、Deploymentの前にちゃんとサイズを見ておく必要がある。通常のJARの方が小さい場合はpack.gzをおかないようにする方がいいだろう。

Server側の返し方

JNLPでは<property name=”jnlp.packEnabled” value=”true”/>をjnlpファイルに書いておくと、HTTP要求のaccept-encodingヘッダにpack200-gzipが付加されて送られるようになる。pack.gzをサポートしますよ、とクライアントから意思表示してもらうのであって、JNLPに「オマエはpack.gzを取りに来いや」とファイル名を直接書くのではないことに注意する。pack.gzはあくまでも最適化の一環であり、pack200をサポートしないクライアントも含めた全てのクライアントが正しくファイルを受信できるよう、pack200-gzipがaccept-encodingにない場合は通常のJARファイルを返せるようにしなければいけない。

Server側ではpack200-gzipが指定されていて、該当JARファイルに対応するpack.gzファイルがあればそちらを返し、なければjarファイルを返すようServletを実装しておけば良いだろう。このときのHTTPヘッダはJARファイルの場合は


content-type: application/x-java-archive

pack.gzの場合は


content-type: application/x-java-pack200
content-encoding: pack200-gzip

と返すようにする。あとはjavawsがよきにはからって処理してくれる。はず。

ちなみにJDKのSampleにこの辺をサポートしているJnlpDownloadServletなるものがあるらしいから、それを使うのがいいのだろう。自分はつい先日までそのサンプルの存在を知らなくて、自分で書いてしまったけど。

早くこの世からなくならないかなぁ、Java Web Start。

Advertisements