BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル ネイティブJavaの実情

ネイティブJavaの実情

キーポイント

  • Microservices on Kubernetes are the business case sweet spot for native Java because they have the most significant framework and Java runtime overhead.
  • Native Java adoption can happen incrementally, one microservice at a time.
  • The application framework should fully support native Java in production.
  • Native Java requires more effort to build, debug, test, deploy, and profile.
  • Converting an application to native Java only works if all its application libraries support native Java.

原文(投稿日:2022/04/19)へのリンク

この記事は、記事シリーズ「Native Compilations Boosts Java」の一部です。サブスクライブすると、RSSを通して、このシリーズの新しい記事に関する通知を受け取れるようになります。

Javaはエンタープライズアプリケーションの多くを占めています。しかし、クラウドでは、Javaは一部の競合と比べてコストが割高です。ネイティブコンパイルにより、クラウドにおけるJavaをより安価にすることができます。起動がはるかに速く、使用するメモリが少ないアプリケーションを作成することができます。

そのため、ネイティブコンパイルに対して、すべてのJavaユーザから多くの疑問が挙がります。ネイティブJavaは開発をどのように変えますか。いつネイティブJavaに切り替えるべきですか。いつすべきではないですか。そして、ネイティブJavaにはどのフレームワークを使うべきですか。このシリーズでは、これらの疑問に答えます。

序章

マイクロサービスアーキテクチャの人気の高まりは、有名な映画「トップガン」の台詞「俺には必要なんだ、スピードが」を思い起こさせます。クラウドサービスを実行する上で、コンテナの小型化、起動時間の短縮、リソース使用率の向上がますます重要になっています。

Javaは、起動時間がかかること、すべてが使われているかどうかに関わらず膨大な依存関係があること、大量のリソース要件について長い間批判されてきました。JVMとアプリケーションサーバを組み合わせると、要求はさらに重くなります。

従来のインタプリタされたJavaサービスは、真のマイクロサービスプラットフォーム、特にサーバレスAPIには理想的とは言えませんでした。

これは、ネイティブJavaが本当に輝くところです...

スイートスポットを見つける

ネイティブJavaは、Kubernetes、マイクロサービス、サーバレスコンポーネントに最適です。また、新しいサービスを開発したり、より大きなモノリスアプリケーションをより小さなサービスに分解したりする場合にも理想的なタイミングです。

ネイティブJavaの採用に「ビッグバン」アプローチをとる必要はありません。一度に1つのサービスで実行できます。このアプローチによりリスクが最小限に抑えられます。そして、テクノロジーが時間の経過とともに成熟するにつれて信頼を築いていくでしょう。

行動を起こすことに最初は圧倒されるかもしれませんが、今行われている従来のJava開発とそれほど変わりません。

Logicdropは、ビジネス自動化とデータインテリジェンスのためのオールインワンプラットフォームを開発しています。このプラットフォームにより、企業はクラウド上で独自のソリューションを設計・デプロイできるようになります。当社のプラットフォームは元々Spring BootとDroolsを使って開発されていましたが、ゼロから再設計しました。QuarkusとKogitoのみを使用しており、デプロイする対象の大部分がネイティブJava実行ファイルであることを前提としています。

ネイティブJavaに切り替える前は、クラウドネイティブインフラストラクチャでより多くのSpring Bootサービスを実行することは、コストは言うまでもなく、規模の点でも課題になってきています。機能に関係なく、コンテナは常に最大1GB以上のサイズでした。コンテナにはJVMが必要であり、全ての依存関係(使用されているかどうかに関係なく)が含まれているためです。起動時間は平均15~30秒で、リソース制約が厳しいため、ノードごとに実行できるサービスはほんの一握りです。

Quarkusに移行した後は、生成されたネイティブ実行ファイルは非常に小さくなり、起動が大幅に速くなり、全体的に使用するリソースが少なくなります。コンテナのサイズは50MB(圧縮)未満で、1秒未満でリクエストを受け入れる準備ができていた。これらのメリットにより、ネイティブJavaは、コストとパフォーマンスの両面で、サイズと起動時間が重要な環境に最適となりました。

スループットはそれほど問題ではなく、変更後もほぼ同じであることがわかりました。スケーリングが高速で、より多くのサービスをより少ないノードに詰めることができるため、様々なサービスに対して水平スケーリングが適応できます。

これが私たちのコスト削減の例です。AmazonのKubernetesサービスEKSの単一クラスタで、5つのノードで複数のSpring Bootサービスを実行すると、年間約5,000ドルの費用がかかります。ネイティブJavaに移行すると、必要なリソースが半分になるため、そのコストがほぼ50%削減できます。これは、すべてのクラスタで大幅なコスト削減につながりました!

いつネイティブJavaを使うか

ネイティブJavaは非常に素晴らしいです。GraalVMにより、Javaが他の「より軽量で高速な」スタックと同等となり、それと共に馴染みのあるJava構造を維持することができます。そして、クラウドでは「より軽く、より速く」が重要です!

ネイティブJava実行ファイルもより安全になります。GraalVMは、未使用のクラス、メソッド、フィールドを削除することにより、セキュリティ上の問題となる可能性がある領域を減らします。

新たにマイクロサービスを開発する場合、ネイティブJavaの理想的な出発点となります。確立された実証済みのネイティブライブラリを活用するためにゼロから作成できるためです。

何をネイティブJavaに移行するかを決定するとき、これらの要件は良い出発点となります。

  1. サービスはスタンドアロン
  2. 起動時間とスケーリングは重要か
  3. 外部依存関係はネイティブJavaと互換性があるか

このシリーズのGraalVMの記事で説明しているように、動的Java機能(リフレクションなど)を適切に処理するには、追加の設定が必要になる場合があります。この追加のメタデータがないと、ライブラリをネイティブ実行ファイルの一部として使ったときに失敗する可能性があります。そのため、私たちの経験から、JavaライブラリはネイティブJavaと互換性があるか、ないかのどちらかです。

厳選されたライブラリのセットを提供するフレームワークを使うと、ネイティブJavaで機能するものと機能しないものを知ることができます。残念ながら、他のJavaライブラリを使うのは難しいでしょう。現在、ライブラリがネイティブJavaと互換性があるかどうかを判断する唯一の方法は、ネイティブ実行ファイルで実行することです。ほとんどの場合、不具合があればすぐにわかります。

Apache IgniteはネイティブJavaでは不具合があるライブラリです。低レベルのJava APIを使っているためです。特定のSpring Bootサービスでキャッシュとして現在も使っていますが、ネイティブ実行ファイルではRedisに置き換えられています。

どのライブラリがネイティブJavaと互換性があるかを知ることは、ネイティブJavaの適切な候補を決定する上で重要な要素になります。互換性のないライブラリの場合、代替を使用するか、機能を再実装します。

幸い、ほとんどのJavaアプリケーションは通常、フレームワークにすでに備えているものと同様のタイプの機能(ロギング、REST API、JSONなど)に依存しています。例えば、これらのAPIはQuarkusにすでに存在し、ネイティブJavaと互換性があります。

  • 永続性(NoSQLおよびRDBMS)
  • 可観測性(弾性、プロメテウス、イエーガーなど)
  • AWS SDK
  • セキュリティ
  • SOAP (Apache CXF)
  • REST (RESTEasy, Jackson, etc.)
  • サポート (Swagger, Logging, etc.)

上記のリストは、一般的に使用されるライブラリのかなりの数がネイティブJavaで動作することができ、また、実際に動作していることを示しています。そして、リストは増え続けています!

ただし、すべてのサービスがネイティブJavaに最適であるとは限りません。移行が、それが提供する価値を超えて厄介となるようなライブラリとコードがあります。これらのサービスはそのままにして、後で再評価することをお勧めします。

私たちの経験から、ネイティブJavaへの移行は次の場合には意味がありませんでした。

  • 起動時間、スケーリング、リソース要件が重要でなかった
  • 特殊なライブラリで、ネイティブに相当するものがないか、ネイティブフレンドリーでなかった
  • リフレクションや動的プロキシなどの動的Javaが多用されていた

動的Javaに関する注意:GraalVMは動的プロキシをサポートしていません。ネイティブ実行ファイルはビルド時にすべてのクラスを使用できるようにする必要があるためです。リフレクションについてはサポートされています。しかし、ビルド時に要素を解決できない場合は、リフレクションと動的プロキシオブジェクトの使用状況をトレースする通常のJVMで実行できるエージェントがあります。

複雑さ、労力、リスクがネイティブJavaに移行することの直接的なメリットを上回ったときは常に、後で戻ってこれるようにそれらのサービスをバックアップしておきました。こういった対処をしたサービスは、ほんのわずかでした。

フレームワークの選択

ネイティブフレームワークを選択することは、スターターポケモンを選択することに似ています。それぞれに長所と短所があります。そのため、1つを選択する際に、長期的な使用について慎重に検討する必要があります。

ネイティブJavaは、プレーンJava開発に使うことができます。ただし、ほとんどの組織は、フレームワークに基づいて構築することを選択すべきです。フレームワークによって、ボイラープレートコードが削減され、時間と労力を節約する厳選されたAPIセットが提供されるためです。さらに、各フレームワークによって、ネイティブの実行ファイルを構築するプロセスからユーザが遮断され、複雑さと学習曲線をさらに削減することができます。

選択されたフレームワークは、GraalVMを完全に取り込むことができ、ネイティブJavaをサポートする豊富なエコシステムを提供し、組織の理にかなう方法でネイティブ実行ファイルをシンプルにビルドできる必要があります。そのことを念頭に置くと、今それができるのはQuarkus、Micronaut、Helidonの3つのJavaフレームワークだけです。

一部のフレームワークではJVMで「従来どおり」実行するしながら、一部でGraalVMの最適化を利用することもできます。これは、アプリケーションやサービスを完全なネイティブで実行できない場合の優れたフォールバックになります。

私たちは利用できるフレームワークを評価した後、Quarkusを選択しました。これは、起動して実行するのに最速のフレームワークでした。Java標準を活用し、ドキュメントは素晴らしく、必要なすべての機能がすぐに利用できるように提供されています。コミュニティは非常に役に立ち、協力的でした。そのため、わずか2か月で、私たちのバックエンドチームは全てSpring BootからQuarkusに切り替えました。

ネイティブJavaの採用

ネイティブJavaに移行することは、予想するほど恐ろしいことではありません。開発エクスペリエンスは基本的に同じです。ただし、一部のプロセスを、ネイティブ実行ファイルをより適切に提供するためにわずかに変更する必要があります。

日常の開発では、通常どおりJavaサービスを開発します。Javaコードを記述し、IDEやコマンドラインツールを使ってテストとデバッグをします。ネイティブ実行ファイルをビルドする際に、そのプロセスに手順が追加され、新たな考慮が必要となります。

ネイティブ実行ファイルをターゲットとする典型的なライフサイクルは次のようになります。

  • 開発者のマシンで通常通りサービスを開発、デバッグ、テストする
  • より厳しく妥協しないテストを実施する
    • APIペイロードの構造をテストして、それらが完全であることを確認する
    • 「本番環境で実行するように」エンドポイントをテストして、すべてのコードが網羅されていることを確認する
  • 各環境・OS向けのネイティブ実行ファイルをビルド、テスト、デプロイする

従来のJava開発と異なり、ネイティブJava実行ファイルの構築はリソースを大量に消費します。大規模なワークステーションであっても、各サービスの構築には2~10分かかる場合があります。

また、従来のJava開発とは異なり、単一のWARファイルやJARファイルを作成するだけでは十分ではありません。各OS向けに独自のネイティブ実行ファイルが必要となります。そして、ネイティブ実行ファイルはコードとプロパティをインライン化するため、環境ごとに独自のネイティブ実行ファイルが必要となります。たとえば、Swaggerはステージング環境では公開され、本番環境では公開されないかもしれません。そのため、ステージングの実行ファイルはSwaggerの依存関係を含めて構築する必要があります。一方で本番実行ファイルは必要ありません。実行時に処理できないプロパティや設定についても同じことが言えます。 Linuxコンテナのみをターゲットとする場合、ビルドのバリエーション数は少なくなります。

ビルド

必要な場合にのみ、開発者のマシンでネイティブJava実行ファイルをビルドすることがベストです。これは、重要な機能がまさにマージされようとする直前であったり、デバッグが必要な問題が発生した場合となります。そうでなければ、CI/CDパイプラインに活用して、さまざまなターゲットのビルドとテストの負荷を軽減することで、プロセスの煩わしさを軽減し、開発者の負担を軽減できます。

ネイティブ実行ファイルを含むコンテナがはるかに小さく、必要なリソースもはるかに少ないことを前述しました。これにより、単一の共有環境だけに依存するのではなく、複数のプレビュー環境をクラスタにデプロイできるようになりました。開発者は、特定の設定向けにネイティブにビルドされたすべてのサービスを、独立した環境で一斉にテストできるようになりました。「誰かを踏む(誰かの妨げになる)」ことはありません。これは従来のJavaでも可能ですが、はるかにコストがかかります。すでに逼迫しているクラウドリソースがさらに必要となるためです。

たとえば、私たちは、開発、ステージング、本番のみという通常の3つの環境から始めました。ネイティブの実行ファイルを使って、20を超えるプレビュー環境を作成できます。各環境では、必要なすべてのサービス(現在は最大20のサービス)に対してビルドと設定がされます。そのため、20サービスのみの容量を持った単一の開発環境を共有するのではなく、20以上のプレビュー環境を並行して実行し、合計400のサービスを公開できるようになりました。

デバッグ

問題がネイティブ実行ファイルにある場合は、ネイティブ実行ファイルをデバッグする必要があります。これには、追加のセットアップとツールが必要であり、GraalVMを使用できるようにする必要があります。ただし、一度設定すると、今人気のIDEを使ってJavaをデバッグするのと大きな差はありません。

デバッグは、実行中のネイティブプロセスに接続し、IDEをJavaソースファイルにリンクし、最後にコードをステップ実行することから始まります。プロセスにアタッチされると、ブレークポイントの設定、ウォッチの作成、状態の検査など、通常のすべてのアクションができるようになります。

幸いなことに、ネイティブJavaの旅を始めてからは、このツールは長い道のりを歩んできました。たとえば、Visual Studio CodeにはQuarkusとGraalVM向けの優れた拡張機能があります。この拡張機能では、完全なJava開発機能とデバッグ機能が提供されており、GraalVMランタイムを含んでいます。この拡張機能はVisualVMとも統合されているため、ネイティブの実行ファイルを分析できます。

GraalVM FAQによると、IntelliJ、Eclipse、NetbeansもGraalVMをサポートしています。最後の手段として、いつでもGNUデバッガーを使ってネイティブ実行ファイルをデバッグできます。

テスト

ネイティブJava実行ファイルのテストは、従来のJavaサービスのテストと似ています。しかし、微妙な差異を認識することが不可欠です。

ネイティブ実行ファイルをテストすることの直接の欠点は、静的で閉ざされているという性質です。ここでは、モックライブラリなど、Javaの動的な性質に依存する実証済みのテストアプローチを使用することはできません。また、ソースコードを変更した場合には、まずネイティブ実行ファイルの新しいビルドが必要となります。これも、従来のJavaよりもはるかに遅いプロセスです。

GraalVMではまた、可能な限り多くのコードがインライン化および/あるいは削除されます。これは多くの問題を引き起こす可能性があります。

誤ったコード削除の例として、Jackson JSONシリアル化があります。開発中に私たちが実施したJUnitテストでは、シリアル化が正常であることが報告されていました。しかし、ネイティブ実行ファイルには特定のネストされたモデルがなく、例外がスローがありませんでした。その理由は、GraalVMが未使用であると考え、実行ファイルから一部のモデルを削除したためです。修正は簡単でした。JSONペイロードで使われる可能性のあるクラスをGraalVMに登録します。これにより、ネイティブ実行ファイルからの除外を防ぐことができました。また、テストを拡張し、ペイロードを徹底的にチェックし、さらにスモークテストを追加しました。

リフレクションなどの動的機能は、注意深く監視すべきもう1つの領域です。場合によっては、例外がスローされないとか、実行ファイルがデプロイされるまで機能に関する特定の問題が表面化しないことがあります。

さらに、エンドポイントを実行するテストは、ネイティブの実行ファイルに対して、期待される機能と正しいペイロードを保証するための優れた方法であることがわかりました。JVMとネイティブ実行ファイルのどちらで実行されているかどうかに関係なく、特定のサービスのエントリポイントでテストを開始することは、最も重要な機能を検証するための良い方法です。

まとめ

ネイティブJavaへの移行は、私たちの当初の目的ではありませんでした。既存のプラットフォームをよりクラウドネイティブに再構築し、将来の機能に備え、Kubernetesクラスタを大規模に活用したかっただけでした。

Quarkusを選択したことは、これまでで最高の決断の1つであったと確信しています。これにより、ネイティブJavaの採用がとても簡単になりました。事前の計画と努力により、いくつかのプロトタイプを作成した後、ネイティブJavaに真っ向から飛び込むことだけができることではないとすぐにわかりました。また、それは最小限の努力で組織的に起こっていました!

確かに課題があります。従来の開発とデリバリが変化するでしょう。しかし、それは今のJavaサービスの開発方法とそれほど変わらないでしょう。ネイティブJavaへの移行は、私たちにとっては既存プロセスへの追加にすぎませんでした。

結局のところ、どのマイクロサービスも一般的には、起動時間の短縮とリソース要求の削減の恩恵を受けるでしょう。特にKubernetesでネイティブJavaを使用する利点が、コスト削減および計測可能な効率性が相まって、ネイティブJavaに移行した理由となっています。

ネイティブJava実行ファイルは、Javaを次のレベルに引き上げます。機会があり、条件が適切であれば、移行してGraalVMの使用を開始する努力をする価値は十分にあります。

 

この記事は、記事シリーズ「Native Compilations Boosts Java」の一部です。サブスクライブすると、RSSを通して、このシリーズの新しい記事に関する通知を受け取れるようになります。

Javaはエンタープライズアプリケーションの多くを占めています。しかし、クラウドでは、Javaは一部の競合と比べてコストが割高です。ネイティブコンパイルにより、クラウドにおけるJavaをより安価にすることができます。起動がはるかに速く、使用するメモリが少ないアプリケーションを作成することができます。

そのため、ネイティブコンパイルに対して、すべてのJavaユーザから多くの疑問が挙がります。ネイティブJavaは開発をどのように変えますか。いつネイティブJavaに切り替えるべきですか。いつすべきではないですか。そして、ネイティブJavaにはどのフレームワークを使うべきですか。このシリーズでは、これらの疑問に答えます。

作者について

この記事に星をつける

おすすめ度
スタイル

BT