こんにちは。@phonypianistです。
本投稿はアクロクエスト アドベントカレンダー 12月21日 の記事です。
最近、Quarkusアプリを本番適用しました。
QuarkusはJavaアプリを作るための軽量なフレームワークで起動が速いって聞くけど、実際どれくらい速いんだろう?と気になったので、Spring Bootや、類似OSSのMicronautと比べてみました。
背景
JavaのフレームワークといえばSpringBootが主流ですが、起動が遅かったり、必要なメモリが多かったりしています。 これは、アプリ起動時にリフレクションを用いてDI(Dependency Injection)を行っているのが要因の1つです。
マイクロサービス、コンテナネイティブなアプリケーションは、負荷の状況に応じて、シームレスにスケールアウトできる必要があります。 アプリケーションの起動速度が遅かったり、メモリ消費量が多かったりすると、負荷に対してシステムの性能が追随できません。
この問題を解決し高速化するために、QuarkusやMicronautといった新しいフレームワークが登場しています。
QuarkusもMicronautも、コンパイル時にDIの解決を行うことで、起動速度や処理速度の向上、およびメモリ使用量の削減ができます。
簡単に、SpringBoot、Quarkus、Micronautの特徴を以下にまとめます。
フレームワーク | 特徴 |
---|---|
SpringBoot |
|
Quarkus |
|
Micronaut |
QuarkusやMicronautでは、GraalVMを使用してネイティブアプリも作ることができます。
これにより、アプリの起動時間を飛躍的に短縮し、メモリも節約することができます。
※Spring Bootもネイティブアプリが作れるようになりましたが、いくつか課題があるため今回は対象外としています。
今回は、アプリの起動における性能について、SpringBoot、Quarkus、Micronautでどれくらい異なるのか、調べてみました。 QuarkusとMicronautは、ネイティブアプリも計測してみました。
前提
環境構成
AWS Fargateを用いて、以下のアプリを起動します(それぞれ1コンテナずつ、計5コンテナを起動)。
アプリの1コンテナあたりのリソースは、1vCPU、2GBメモリを割り当てました。
検証に使用するアプリ
データベースのテーブルにあらかじめ入っているレコードを取得・返却するアプリです。 REST APIを提供し、クライアントからそのAPIが呼び出されると、JPAを用いてデータベースからレコードを取得し、クライアントに返却します。 SpringBoot、Quarkus、Micronautそれぞれ同じ処理内容にしてあります。
使用するミドルウェア・ライブラリのバージョンは以下の通りです。
- Java:17
- SpringBoot:3.0.0
- Quarkus:2.13.1.Final
- Micronaut:3.7.1
それぞれのソースコードは以下の通りです。
SpringBoot
UserController.java
@RestController @RequestMapping("user") public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @GetMapping Iterable<User> getAllUsers() { return userService.getAllUsers(); } @GetMapping("/{userId}") User getUser(@PathVariable("userId") String userId) { return userService.getUser(userId); } }
UserService.java
@Component public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public Iterable<User> getAllUsers() { return userRepository.findAll(); } public User getUser(String userId) { return userRepository.findById(userId).orElseThrow(() -> new RuntimeException(userId)); } }
UserRepository.java
@Repository public interface UserRepository extends CrudRepository<User, String> { }
Quarkus
UserResource.java
@Path("/user") @Produces("application/json") public class UserResource { @Inject UserService userService; @GET public List<User> getAllUsers() { return userService.getAllUsers(); } @GET @Path("/{userId}") public User getUser(@PathParam("userId") String userId) { return userService.getUser(userId); } }
UserService.java
@ApplicationScoped public class UserService { @Inject UserRepository userRepository; public List<User> getAllUsers() { return userRepository.findAllUsers(); } public User getUser(String userId) { return userRepository.findByUserId(userId); } }
UserReposirory.java
@ApplicationScoped public class UserRepository implements PanacheRepository<User> { public List<User> findAllUsers() { return findAll().list(); } public User findByUserId(String userId) { return find("user_id", userId).firstResult(); } }
Micronaut
UserController.java
@Controller("/user") public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @Get Iterable<User> getUsers() { return userService.getUsers(); } @Get("/{userId}") User getUser(@PathVariable("userId") String userId) { return userService.getUser(userId); } }
UserService.java
@Singleton public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public Iterable<User> getUsers() { return userRepository.findAll(); } public User getUser(String userId) { return userRepository.findById(userId).orElseThrow(() -> new RuntimeException(userId)); } }
UserRepository.java
@Repository public interface UserRepository extends CrudRepository<User, String> { }
計測内容
5つのアプリを起動して、それぞれの起動速度(起動にかかった時間)と、メモリ使用量を計測します。
起動にかかった時間は、CloudWatchログに出力される、アプリ(各フレームワーク)の起動時間を確認します。 メモリ使用量は、CloudWatchメトリクスの値を確認します。
1つのアプリに対して5回起動し、最大と最小を除く3回分の、起動にかかった時間とメモリ使用量の平均値を計算しました。
計測結果
起動完了までの時間は、以下のようになりました。
起動完了までの時間は、やはりQuarkusもMicronautも、ネイティブアプリが爆速です。
Javaアプリの中では、Quarkusが他のフレームワークと比べると速くなっています。
続いて、起動直後のメモリ使用量です。
メモリ使用量も、やはりネイティブアプリがJavaアプリの半分の量の消費で抑えられています。特にQuarkusのネイティブアプリはたった12MBの消費で抑えられています。
Javaアプリでは、SpringBootよりQuarkusやMicronautの方が、少し消費量が低くなっています。
まとめ
やはり、ネイティブアプリは起動時間もメモリ使用量もパフォーマンスが良いですね。
Javaアプリの中では、Quarkusが他のフレームワークよりもパフォーマンスが良いようです。
※アプリの構成や環境によって、変わることがあります。
今回は、起動時のパフォーマンスについて検証しました。
次回は、リクエスト処理性能について検証したいと思います。
それでは。
Acroquest Technologyでは、キャリア採用を行っています。少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。 www.wantedly.com
- ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
- Elasticsearch等を使ったデータ収集/分析/可視化
- マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
- 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長