Compressed GraalVM Native Images: the best startup for Java apps comes in tiny packages

原文はこちら。
The original article was written by Loïc Lefèvre (Enterprise Cloud and Information Management Advisor, Oracle).
https://medium.com/graalvm/compressed-graalvm-native-images-4d233766a214

GraalVM 20.3.0 Native Imageが生成する実行可能ファイルは、Ultimate Packer for eXecutablesのようなツールを使って、起動時間にほとんど影響を与えずに1/3〜1/4に圧縮できる。DRAGON StackマネージャというJava CLIアプリケーションは、20MBに満たないサイズの中に多数の機能があることで、開発者体験を改善する。

TL;DR

データ圧縮のことを最初に耳にしたのは、初めてPC(512MBのHDDで非常に大きな容量だった時代です)を持ってすぐ後のことでした。その当時、コマンドラインツールとして実行するpkziparjainrarを学びました。その後、圧縮に関する本を見つけ、圧縮の背後にある理論に驚いて何度も読み返しました。

圧縮に関する本を読み終えて数年後、とある出来事でデータ圧縮の筆者のビジョンが永遠に変わりました。大企業では滅多に起こらないけれども、デモシーンの世界ではよくあることのように思えることに、私はただただ息が詰まりました。その優美さの瞬間をここで共有したいと思っています。

ドイツのビンゲン・アム・ライン(Bingen am Rhein)市では、2009年4月の今月、Breakpoint 2009のデモパーティーが開催されています。RGBAとTBCが彼らの作品 Elevated を披露する番です。デモは3分35秒にも及びます。アニメーション、音楽、効果が本当に美しいのですが、これが4,096バイトの実行ファイルで制作されていることに気づいたとき、非常に関心しました。

そう、たった4KBです。言うまでもなく、このデモがカテゴリ優勝しました。ソースコードを見ると、この小さなサイズの背後にある秘密はCrinklerという名前のWindowsの実行ファイル圧縮ツールであることがわかります。もっと具体的に言うと、Windows用の圧縮リンカですが、そのターゲット(8KB、4KB)ゆえに、32ビットの実行ファイルのみを生成できるようになっています。

Crinkler
https://github.com/runestubbe/Crinkler

Image for post

時を戻して2020年、DRAGON Stack managerという名前のオープンソースプロジェクトを始めました。

DRAGON Stack manager
https://loiclefevre.github.io/dragon/

このプロジェクトの目的は、プロジェクトのスキャフォールディングやクラウドリソースのプロビジョニングなど、通常は手作業で行われる多くのステップを自動化することで、自律型バックエンドを使ったアプリケーションの開発を簡素化することです。主にJavaで開発されており、GraalVMのネイティブイメージ機能により、DRAGONツールは非常にシンプルに使用することができます。これは主としてJavaで開発しており、GraalVM Native Image機能を使うことでDRAGONツールは非常に間単に利用できます。

基本的に、DRAGONツールはパブリッククラウドリソースを管理できるOracle Cloud Infrastructure SDK for Javaを統合したCommand Line Interface (CLI) として提供されます。

Oracle Cloud Infrastructure SDK for Java
https://docs.cloud.oracle.com/en-us/iaas/Content/API/SDKDocs/javasdk.htm

このDRAGONツールでは、プロジェクトのソースコード(ReactのフロントエンドとSpring BootのPetclinicサンプルアプリケーション)も生成できます。これらのソースコードはプロビジョニング済みのAutonomous DatabaseとREST Data Serviceとの接続が事前構成済みの状態で提供されます。ここでの開発者体験は非常によいので、是非お試しください。Windows、Linux、macOSバイナリが利用できます。

From Zero to WOW in 5 minutes
https://loiclefevre.github.io/dragon/#/?id=from-zero-to-wow-in-5-minutes

Image for post

開発者体験の重要なポイントの一つは、CLI ユーティリティがいかに軽量であるかということです。これはスタンドアロンの実行ファイルで、現在約20MB です。ダウンロードは非常に簡単で、小さなファイルを取得するだけで、スタンドアロンなので特定のランタイムを必要としません。起動も動作も速いです。

UPX (Ultimate Packer for eXecutables) を使用することで、このような小さなサイズを実現しています。UPXはポータブル、無料、オープンソースであり、革新的な解凍機能を備えた複数の実行形式に対応する高性能な実行ファイルパックツールです。

UPX: the Ultimate Packer for eXecutables
https://upx.github.io/

UPXはUCLというANSI Cで書かれたポータブルな可逆圧縮データ圧縮ライブラリのデータ圧縮アルゴリズムを使用しています。UCLは、実行可能ファイル(dll、soなどのライブラリを含む)のための多くの可逆圧縮アルゴリズムを実装しており、以下のような利点をもたらします。

  • 解凍が簡単かつ高速
  • 解凍のためのメモリは不要
  • 解凍ツールを200バイトのコード以下にできる
  • 優れた圧縮率
  • 数種類の圧縮レベルが利用可能。ただ、高圧縮率であればより多くの圧縮時間を要する。大して解凍の速度には影響しない。
  • 商用ソフトウェアディストリビューションだけでなく、非商用利用にも圧縮された実行ファイルを利用可能。

GraalVM Native Imageを使うと高速な起動時間ですぐに利用でき、ウォームアップ不要なプログラムの実行ファイルを作成できます。またGraalVM Native Imageで作成された実行ファイルをUPXで圧縮することもできます。

これで、GraalVM Native ImageとUPXの組み合わせの利点をイメージできるようになりました。

  • JDKを必要としない非常に小さなスタンドアロンディストリビューション
  • 優れた起動時間はそのまま
  • 低いメモリフットプリント
  • 低いストレージフットプリント
  • アプリケーション、Dockerイメージ、またはFnベースのFunctionを高速にダウンロードするためのネットワーク帯域幅リソースは少なくてすむ(詳細は後ほど)。

Compressing your native executable

UPXはいくつかのプラットフォームで利用できますが、macOSでは利用できません。良いニュースは、LinuxやWindowsのホスト上でmacOSのネイティブ実行ファイルを圧縮することができるということです。

このプロセスでは、まずネイティブ実行ファイルを構築し、必要に応じてそれを特定のニーズに合わせて拡張します。例えば、Rceditツールを使ってWindowsの実行ファイルに独自のアイコンを追加するといった具合です。最後のステップで、UPXを実行してネイティブ実行ファイルを圧縮します。

rcedit
https://github.com/electron/rcedit/releases/latest

UPX の使い方は簡単で、以下のように圧縮対象のファイルを指定してコマンドを実行すれば、より小さなバイナリが生成されます(-k オプションを付けるとオリジナルのファイルも残します)。

upx -7 -k myapp

UPXは、標準のNRV(Not Really Vanished)アルゴリズムを使用して実行ファイルを圧縮するためにいくつかのアプローチを使用することができます。1(最速)から9(最も積極的に圧縮)、そして --best オプションを使う10番目のレベルまでが利用可能です。コード、リソース、アイコンなどの実行可能なセクションのフィルタリングをサポートしており、通常、解凍を犠牲にしてより良い圧縮を提供する(より多くの時間とより多くのメモリを必要とします)LZMAアルゴリズムを使用することもできます。最後に、UPXを最大限に利用したい人のために、もっと圧縮するために --brute--ultra-brute のような特別なフラグが存在しますが、これらを使うと圧縮に要する時間は指数関数的に増加します。

UPX の圧縮の限界はどのあたりなのでしょうか?UPX圧縮でネイティブ画像の実行ファイルのサイズをどの程度まで圧縮できるか見てみましょう。

圧縮レベルがバイナリのサイズにどう影響するか実験してみました。下図は、DRAGON Stack managerツールでのテスト結果を示しています。棒グラフは圧縮された実行ファイルのサイズの違いを反映しており、オレンジ色の折れ線グラフはUPXが圧縮に必要とする時間を示しています。

Image for post
Windowsでのネイティブイメージに対するUPX 圧縮ファクターの効果

まずWindowsからです。青の棒は圧縮サイズの変化を示しており、元々69.1MBだったものが、DRAGON Stack managerリリースで現在利用している圧縮レベル -9 だと18.8MBにまで減少しています。

紫の棒はLZMAアルゴリズムを圧縮レベル -7 もしくは --best (これは10番目のレベルに相当します)を併用した場合の効果を示しています。それぞれ4.47倍(15.47 MB)と4.53倍(15.23 MB)のサイズ削減効果が得られました。

最後に、--brute--ultra-brute を使っても --lzma に比べてサイズを小さくすることはできませんが、圧縮時間は劇的に増加することがわかります。これらを使わないことをおすすめします。

Linuxの場合、以下のようなグラフになりました。

Image for post
Oracle LinuxでのネイティブイメージにおけるUPX圧縮ファクターの効果

macOSの場合、圧縮はすでにLinux上で済ませています。バイナリサイズのグラフは以下のようになりました。

Image for post
macOSでのネイティブイメージにおけるUPX圧縮ファクターの効果

--best オプションだとGraalVMネイティブイメージの高速起動という利点を維持しつつ、(実際に)最高の圧縮率を提供することを、3プラットフォームで確認しています。

Runtime impact of compression

圧縮された実行ファイルをいくつか実行してみて、圧縮による実行時の影響が見えるかどうかも確認しました。このテストは以下のコマンドの実行で構成されています。

dragon --help

このコマンドは DRAGON Stack CLI が受け付けることのできる引数を表示しますが、その間に HTTPS リクエストを実行して GitHub で利用可能な新しいバージョンを探します。実行結果結果は以下の通りです。

Image for post
GraalVM Native Image実行時間に対するUPX圧縮レベルの影響

ネイティブイメージの実行をウォームアップ(ここでは3回)を考慮・対応するHyperfineツールを使って取得しました。ご覧のように、圧縮の影響はずっと低く、 --best 圧縮レベルで圧縮しながらウェブページをダウンロードするのでも265ms以下です。これはCLIアプリケーションとしてはかなり許容できるものです。LZMA圧縮は、java -jarコマンド自体を実際に起動する場合と比較しても、ほんの少しだけ遅いようです。そのため、CLIに使うのは絶対に避けてください。確かにこのユーザーエクスペリエンスは理想的ではありません。

hyperfine
https://github.com/sharkdp/hyperfine

A follow-up to the “CLI applications with GraalVM Native Image” post

先日のGraalVMブログの投稿で、Oleg Šelajev氏がMicronautとPicocliを使ったGraalVMネイティブイメージの利点について述べていました。

CLI applications with GraalVM Native Image
https://medium.com/graalvm/cli-applications-with-graalvm-native-image-d629a40aa0be
https://logico-jp.io/2020/11/21/cli-applications-with-graalvm-native-image/
Micronaut
http://micronaut.io/
Picocli
https://picocli.info/

それに続いて、そのブログエントリ内で作成されたCLIアプリを圧縮してみるのも面白いかなと思いました。新しくリリースされたばかりのMicronaut 2.2.0バージョンを使って得た結果がこちらです。

以下は、デフォルトのJVMメモリ設定でネイティブイメージを使用した場合の詳細なリソース使用量です。

Image for post
Micronaut での素数計算のサンプル(非圧縮、つまりオリジナル)

以下がハイライトです。

  • アプリケーションによる最大メモリ利用量(最大RSS): 39 MB
  • 総所要時間: 10 ms
  • CPU利用率: 80%

UPXで圧縮してみましょう。
· オリジナルサイズ: 49.75 MB
· –best での圧縮後のサイズ: 13.3 MB

でリソース利用がどのように進化するか見てみましょう。

Image for post
Micronaut での素数計算のサンプル(UPXで圧縮)
  • アプリケーションによる最大メモリ利用量(最大RSS): 81.85 MB
  • 総所要時間: 170 ms
  • CPU利用率: 100%

では、最速で圧縮できる圧縮レベル (-1) の結果です。

  • アプリケーションによる最大メモリ利用量(最大RSS): 86 MB
  • 総所要時間: 220 ms
  • CPU利用率: 100%

圧縮は起動時間に影響するようです。圧縮されたネイティブイメージは数百ミリ秒で動作しますが、バイナリサイズに依存しているようです。典型的なCLIアプリケーションではこの遅延はほとんど目立つことはないでしょうが、サーバーレスプラットフォーム用にネイティブイメージを作成している場合は、このトレードオフに注意する必要があります。

Bonus point: Micronaut 2.2.0のCLIもまたネイティブイメージの実行ファイルであり、これも圧縮できます。例えばLinuxでは66.5 MBからおよそ17 MBになります。

…and what about Docker images or Fn functions?

先に述べたように、圧縮が有用なもう一つの分野は、JavaアプリケーションのDockerイメージです。Oracleで働き、JavaとFn project(オンプレミスとOracle Cloud Infrastructure上のマネージドサービスとして利用可能)のエバンジェリストとして有名なDeveloper AdvocateであるDavid Delabasséeは、Javaベースのアプリケーションを含むDockerイメージのサイズを縮小することに関連して、JCON2019で素晴らしいプレゼンテーションを行ってくれました。

彼のインサイトは本当に有用で、彼のおかげで、彼の有名なスライドで説明した結果のいくつかをすぐに再現することができました。

Image for post
David Delabasséeの有名なスライド(数多くの有名なスライドのひとつ)

318 MBから32 MBになりました。

これはGraalVMネイティブイメージに移行する前の話ですが、最終的にDockerイメージのサイズが約9MBになりました。

さて、次は何をするのでしょうか?もちろんネイティブイメージを圧縮しましょう。

圧縮されたネイティブイメージはわずか2.52 MBですが、glibcのようなOSライブラリに動的にリンクされているため、実行にはOS環境が必要です。例えば、スリムな linux の docker イメージや、OS がなくてもライブラリがある distroless コンテナを利用できます。より小さなイメージを作るためにできることは、静的リンクされたネイティブイメージを作成して、依存関係を全く持たないようにすることです。これには、GraalVM Static Native Imagesのドキュメントに記載されている手順を踏む必要があります。

Static Native Images
https://www.graalvm.org/reference-manual/native-image/StaticImages/

静的ネイティブイメージはコンパイルに必要なmuslとzlibライブラリに依存します。最終的には、スムーズに動作し、lddにより、これ以上の依存関係がないことがわかります。

Image for post

静的イメージをUPXで --best オプションを付けて再度圧縮することで、このDockerfileを使って動作するDockerイメージを生成できます。

Image for post
最小のDockerfile

実行には1秒もかかりません。

Image for post
  • アプリケーションの最大利用メモリサイズ(最大RSS): 63.6 MB
  • 総所要時間: 790 ms
  • CPU利用率: 4%

Dockerイメージのサイズは、318 MBではなく、2.64 MBです。なんと1/120の大きさです。

Image for post

Conclusion

GraalVM Native Imageはゲームチェンジャーです。ネイティブ実行ファイルを圧縮することで、UPXによる解凍は非常に高速で、メモリをあまり消費しないため(--best オプションを使用)、起動時間の面で合理的な妥協をしながらも、この技術から得られるメリットをさらに高めることができます。さらに、最新バージョンのGraalVM 20.3.0では、マルチプラットフォーム(Windows、Linux、macOSをサポート)での処理が可能となり、圧縮に関する障壁が取り除かれました。

Image for post

サイズが20MB以下のJavaコマンドラインアプリケーションを持つとどのように感じるでしょうか?知りたい方はDRAGON Stack manager CLIをお試しください。圧縮されたネイティブイメージは、Windows、Linux、macOSのいずれのプラットフォームにも対応しています。

DRAGON Stack manager CLI
https://github.com/loiclefevre/dragon/releases

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中