Static Compilation of Java Applications at Alibaba at Scale

原文はこちら。
The original article was written by Sanhong Li, Ziyi Lin, Chuansheng Lu and Kingsum Chow (Alibaba JVM team).
https://medium.com/graalvm/static-compilation-of-java-applications-at-alibaba-at-scale-2944163c92e

Background

クラウドコンピューティングの目的は、サービスとしてのコンピューティングリソースを提供することにあります。クラウドコンピューティングの中核の原則は、アプリケーション実行のために必要なリソースのみを使い、必要に応じてスケールすることです。クラウドコンピューティングの利点を活用するためには、開発者はこの原則に従ってアプリケーションを設計し、記述する必要があります。

マイクロサービスアーキテクチャはモノリシックアプリケーションを多くのマイクロアプリケーション(マイクロサービス)に分解します。これはクラウドコンピューティングプラットフォームをターゲットにしたアプリケーションにとって魅力的なアプローチです。初期の負荷を処理するために必要なマイクロサービス・インスタンスの個数から始め、需要が高まったときにインスタンスの個数を増やしてスケールアウトすることができます。クラウドの水平スケール能力を活用して耐障害性を向上させることができます。

Javaプラットフォームは、最も広く使われているプラットフォームの一つになりましたが、その人気にもかかわらず、Javaは多くの批判を受けています。例えば、

  • Javaは起動が非常に遅い
  • Javaは多くのメモリを必要とする
  • Javaの構文は冗長である

などです。特に、Javaの起動時間が長いことは、水平方向のスケーラビリティを阻害しています。ビジネスの観点からは、顧客はリクエストの結果を受け取るまでにアプリケーションの起動に長い時間を待つ必要がある可能性があります。水平方向に拡張可能なプラットフォーム上でのJavaアプリケーションの起動時間を高速化することが我々のモチベーションです。そのために、サーバレスコンピューティングではGraalVMネイティブイメージを採用しています。

GraalVM Native Image
https://www.graalvm.org/docs/reference-manual/native-image/

GraalVM native image at Alibaba

長年にわたり、AlibabaではJavaの利用が増えてきました。多くのアプリケーションはJavaで書かれています。約1万人のJava開発者が10億行以上のJavaコードを書いています。Alibabaは、活気に満ちたオープンソースのエコシステムに基づいて、Javaソフトウェアのほとんどをカスタマイズしています。Alibaba Cloudでは、これらのJavaプログラムはオンライン取引、決済、物流業務向けに開発されています。これらの多くは、オンラインリクエストに対応するために、Kubernetesネイティブの環境上で動作するマイクロサービスとして開発されています。

Alibabaでは、GraalVMのネイティブイメージテクノロジーを利用して、マイクロサービスアプリケーションを静的にELF実行ファイルにコンパイルすることで、Javaアプリケーションでネイティブコードの起動時間を実現しています。これは、上述の水平スケーリングの課題に対処するために必要です。

現在のシナリオでは、このサーバーレスアプリケーションはSOFABootフレームワークベースで開発されており、そのfat jarのサイズは120MBを超えます。

SOFABoot is a framework that enhances Spring Boot and fully compatible with it, provides readiness check, class isolation, etc.
https://github.com/sofastack/sofa-boot

Spring、Spring Boot、Tomcat、MySQL-Connectorといった、エンタープライズJavaで典型的なコンポーネントが含まれています。このフレームワークを使ったアプリケーションをSOFABootアプリケーションと呼んでいます。SOFABootアプリケーションは元々、分散アーキテクチャ用に設計されたAlibaba Dragonwell(OpenJDKベース)の上で動作しており、オンライントランザクションを取り扱い、RPCで他の多くの異なるアプリケーションと通信していました。

昨年開催された世界的なオンラインショッピングの祭典(Double 11とかNov 11とも呼ばれています)では、ネイティブイメージとしてコンパイルされたSOFABootアプリケーションを多数デプロイしました。これらのアプリケーションは、トランザクション量が最も多い日に、当社の本番環境で実際のオンラインリクエストに対応することができました。

SOFABootアプリケーション以外にも、静的にコンパイルされたアプリケーションをAlibaba Cloudに導入する可能性を検討してきました。Alibaba Cloudのfunction computingプラットフォーム上にMicronautデモアプリケーションのネイティブイメージ版をデプロイすることに成功しました。

Micronaut for Spring Example
https://github.com/micronaut-projects/micronaut-spring/tree/master/examples/greeting-service
Function Compute
https://www.alibabacloud.com/ja/products/function-compute

以下でGraalVMネイティブイメージを使って静的コンパイルをし、本番環境でパフォーマンス向上を実現するために克服した課題について説明します。

How We Did That

GraalVMネイティブイメージは、従来の(JITコンパイル)Javaと静的コンパイルされたJavaの間のギャップを埋めるための開発者向けの素晴らしいツールセットを提供するとともに、前者から後者への移行方法を提供します。この章では、直面した課題と、JavaアプリケーションをネイティブイメージにコンパイルするためにAlibabaで開発したアプローチに焦点を当てます。また、ソリューションの多くをGraalVMコミュニティに還元しました。

ネイティブイメージでは従来のJava機能のほとんどをサポートしていますが、従来のJITコンパイルされたJavaから静的コンパイルされたJavaプログラムへの自動移行を妨げるいくつかの制限がまだ残っています。

GraalVM Native Image Compatibility and Optimization Guide
https://github.com/oracle/graal/blob/master/substratevm/LIMITATIONS.md

そのため、ネイティブイメージでは、プログラマーが追加情報を提供したり、アプリケーションの元の実装を修正したりして、プログラムを期待通りにコンパイル、実行できるようにする必要があります。SOFABootアプリケーションを適応させる際に直面した課題は以下の通りです。

ビルドに時間がかかる

静的コンパイルは大量のメモリリソースと時間を必要とします。ビルド時間は長時間にわたります。初期にはSOFABootアプリケーションのビルドに約100GBのメモリと4000秒が必要でした。調べてみると、ビルド時間の大部分は静的分析フェーズのtype flow分析に充てられていることがわかったため、高速ビルドが必要なシナリオでは、精度は低いけれども軽量なCHA解析を採用しました。し正確性に欠けるけれどもより軽量なCHA分析(Class Hierarchy Analysis)に置き換えました。その結果、メモリ消費量は100GBから20GBに、ビルド時間は4000秒から1000秒を下回るようになりました。ビルド時間が1/4になったことで、アプリケーションのデプロイを高速化することができました。

Optimization of Object-Oriented Programs Using Static Class Hierarchy Analysis
http://web.cs.ucla.edu/~palsberg/tba/papers/dean-grove-chambers-ecoop95.pdf

クラスの初期化

JITコンパイルされたJavaプログラムでは、クラスの初期化は実行時に行われます。ネイティブイメージの場合、実行時のパフォーマンスの向上のために可能な限りビルド時に初期化を実行できます。

Initialization of Classes and Interfaces (JDK 8)
https://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.4
Initialization of Classes and Interfaces (JDK 14)
https://docs.oracle.com/javase/specs/jls/se14/html/jls-12.html#jls-12.4
Class Initialization in Native Image
https://github.com/oracle/graal/blob/master/substratevm/CLASS-INITIALIZATION.md

ビルド時のクラス初期化は必ずしも安全ではなく、プログラマーがクラス初期化のタイミングを手作業で調整する必要があります。クラスの初期化は連鎖的に行われる可能性があるため、連鎖の前のクラスの初期化を延期せずに、1つのクラスの初期化を実行時に延期すると、ビルド時にクラスの初期化エラーが発生する可能性があります。例えば、以下のコードでは、A->B->C というクラス初期化チェーンを使用しています。

アプリケーションの正確性のために、クラス C は System.currentTimeMillis() の呼び出しのために、実行時に初期化されなければなりません。その結果、クラスAはこのクラス初期化チェーンのルートであるため、ユーザはクラスAも実行時に初期化を行わなければなりません(クラスAが初期化されると、Bの初期化、そして最終的にはCの初期化が呼び出されます)。しかしながら実際のシナリオでは、開発者がビルド時に誤ってクラスCの初期化を見かけた場合に、クラスAがその問題の根本原因であると判断する、つまり開発者が誤ってクラスAをビルド時に初期化するように構成したと判断するのは困難でした。ネイティブイメージはこの種の問題を解決するために、インストルメンテーションに基づいた初期化トレース機能を提供しますが、クラスがインストルメンテーションできない場合、例えばブートストラップ・クラスローダがクラスをロードする場合には失敗してしまうため、我々のソリューションでは、Hotspotのコードを変更してVM レベルでクラスの初期化チェーンを追跡するようにし、開発者がクラスの初期化チェーンを追跡して、この種のミスの根本原因を特定できるようにしました。その結果、Java開発者はAOT(Ahead-of-Time)コンパイルを広く利用することができるようになりました。

ダイナミック・クラス・ローディング

ダイナミック・クラス・ローディングとは、ビルド時にはわからないクラスのバイトコードを使って、実行時にクラスを定義してロードすることです。ダイナミック・クラス・ローディングは、アプリケーション、ライブラリ、フレームワークで広く使われています。代表的な例には、動的に生成されたコンストラクタアクセサに依存するJavaのシリアライズ/デシリアライズのメカニズム、プロキシにcglibを使用するSpring、SQL文のために動的に生成されたクラスを使用するDerbyなどがあります。以下の4ステップで動的クラスの読み込みをサポートしています。我々はこの機能をコミュニティにコミットしました。

  1. (同じクラスが異なる実行でも常に同じ名前を持つことを保証するため)動的に生成されたクラスの名前を固定名に変更する。
  2. Native Image agentにメソッドインターセプターを実装し、固定名パターンを持つ動的に生成されたクラスをファイルシステムにダンプする。
  3. ダンプされたクラスをビルド時にネイティブイメージにコンパイルする。
  4. (動的に生成されたクラスを定義するのではなく)ネイティブイメージの実行時に用意された「動的に生成された」クラスを見つける。

cglib – Byte Code Generation Library is high level API to generate and transform Java byte code.
https://github.com/cglib/cglib
Assisted Configuration of Native Image Builds
https://github.com/oracle/graal/blob/master/substratevm/CONFIGURE.md
Support dynamic class loading and serialization
https://github.com/oracle/graal/pull/2323

Slow GC performance

静的コンパイルにおいて、GCはいまなお不可欠なコンポーネントです。ネイティブイメージにおけるデフォルトのGCは、ヒープ空間を2個に分割(YoungとOld)する純粋なCopy GCです。Javaのスレッドはyoung空間にオブジェクトを割り当て続け、young空間がいっぱいになると、全ての生存するオブジェクトをyoung空間からold空間に待避することで’Young GC’を実行します。old空間がいっぱいになると、Javaヒープにある生存する全てのオブジェクトを圧縮して空き領域をリリースすることにより、’Full GC’を実行します。

この方法は比較的シンプルで多数の小さなワークロードには有用ですが、Springベースのサービスのような、より大きなワークロードをサポートしようとすると、full GCの時間と頻度が悩みの種になります。あるJavaサービスで1回のGCで1.5秒を超える停止時間を観察しましたが、これはオンラインアプリケーションでは受け入れられません。そのため、以下のようなネイティブイメージのGCコンポーネントの改善を実施しました。

Young世代にあるオブジェクトに年齢情報を追加

年齢(Age)がオブジェクトのグループのメモリチャンクに追加され、これらのオブジェクトがyoung GCで生き残った場合にAgeを1ずつ増やす。Ageが特定の閾値に達した生存オブジェクトのみをold世代に昇格する。

バックグラウンドのスレッドを使ってメモリのアンマップを非同期に実施する

ネイティブイメージはメモリチャンクを使ってJavaオブジェクトを保持するが、空きチャンクをOSに解放してフットプリントを小さくしたい場合、チャンクのマッピングを解放するだけである。通常のJavaアプリケーションではメモリの解放に長時間を要する可能性があることがわかったので、この操作を非同期に実施し、STWの休止に関わらないようにしている。

young GCのカードテーブルに基づいてイメージのルートをスキャン

特定のワークロードでは、最終的な実行可能なイメージは、静的コンパイルの後に大きくなる可能性がある。これは通常、膨大な GCのルート・セットを保持しており、あらゆる GC のために徹底的にスキャンしなければならないためである。ネイティブイメージのGCの既存の設計では、これは多くの時間を要する可能性があるため、イメージ・ルートのためのカードテーブル (card table) を追加した。これにより、young GC実行では、直近のGC一時停止以降に変更された参照のみをスキャンするようにした。

こうした変更をGraalVMプロジェクトにコミットしました。

[SVM GC] Add Survivor Space to SVM GC
https://github.com/oracle/graal/pull/1912

Performance Gains

Startup time speedup

ネイティブイメージ利用時の課題に対処するよう変更した後に、静的コンパイルしたSOFABootアプリケーションのパフォーマンスデータを本番環境で収集しました。図に示す通り、起動時間が60秒から3秒、つまりJavaアプリケーションの起動時間が1/20に、さらにGC休止時間が100ミリ秒以下に抑えられました。

Sofaboot Application Startup Time Comparison

静的コンパイルしたMicronautベースのアプリケーションもAlibaba Cloudのfunction computingプラットフォームで実行しました。その結果も魅力的です。native_image_helloは静的コンパイルされたアプリケーションで、springboot_helloは同じアプリケーションではありますがJARとしてデプロイされ、これまでのJavaランタイム上で動作します。下図に結果を示しますが、native_image_helloはspringboot_helloに比べて起動時間で1/100、メモリ使用量で1/6で、80%コストを節約できます(billed durationは利用者がクラウドプラットフォーム上で課金される時間)。これらの2個のアプリケーションのレスポンス時間はほぼ同じです。

Traditional Java Function vs Statically Compiled Function

GC performance

GCに施した上記の改善により、通常のJavaマイクロサービスの休止時間のp90が1.5秒以上から約100ミリ秒にまで削減できました。

GC time improvements

Conclusion

クラウド用のサーバーレスアプリケーションの開発方法を模索している場合、特に最高の起動性能と低メモリフットプリントを求める場合には、GraalVMネイティブイメージを評価する価値があります。

本番環境での結果に大変満足しています。今後もGraalVMコミュニティと連携してイノベーションを推進していきたいと考えています。

コメントを残す

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

WordPress.com ロゴ

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

Google フォト

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

Twitter 画像

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

Facebook の写真

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

%s と連携中