Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

SpringBoot/Quarkus/Micronautの性能を検証してみた ~その1 起動編~

こんにちは。@phonypianistです。
本投稿はアクロクエスアドベントカレンダー 12月21日 の記事です。

最近、Quarkusアプリを本番適用しました。
QuarkusはJavaアプリを作るための軽量なフレームワークで起動が速いって聞くけど、実際どれくらい速いんだろう?と気になったので、Spring Bootや、類似OSSのMicronautと比べてみました。

背景

JavaフレームワークといえばSpringBootが主流ですが、起動が遅かったり、必要なメモリが多かったりしています。 これは、アプリ起動時にリフレクションを用いてDI(Dependency Injection)を行っているのが要因の1つです。

マイクロサービス、コンテナネイティブなアプリケーションは、負荷の状況に応じて、シームレスにスケールアウトできる必要があります。 アプリケーションの起動速度が遅かったり、メモリ消費量が多かったりすると、負荷に対してシステムの性能が追随できません。

この問題を解決し高速化するために、QuarkusやMicronautといった新しいフレームワークが登場しています。

ja.quarkus.io

micronaut.io

QuarkusもMicronautも、コンパイル時にDIの解決を行うことで、起動速度や処理速度の向上、およびメモリ使用量の削減ができます。

簡単に、SpringBoot、Quarkus、Micronautの特徴を以下にまとめます。

フレームワーク 特徴
SpringBoot
Quarkus
Micronaut
  • メモリ使用量が少なく、起動時間が短くなるようにしたJavaフレームワーク
  • SpringBootと一部互換性を持っており、SpringBootから移行しやすいように作られている

QuarkusやMicronautでは、GraalVMを使用してネイティブアプリも作ることができます。 これにより、アプリの起動時間を飛躍的に短縮し、メモリも節約することができます。
※Spring Bootもネイティブアプリが作れるようになりましたが、いくつか課題があるため今回は対象外としています。

今回は、アプリの起動における性能について、SpringBoot、Quarkus、Micronautでどれくらい異なるのか、調べてみました。 QuarkusとMicronautは、ネイティブアプリも計測してみました。

前提

環境構成

AWS Fargateを用いて、以下のアプリを起動します(それぞれ1コンテナずつ、計5コンテナを起動)。

  1. SpringBoot (Java)
  2. Quarkus (Java)
  3. Quarkus (Native)
  4. Micronaut (Java)
  5. Micronaut (Native)

性能比較構成

アプリの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では、キャリア採用を行っています。
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長
  少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。 www.wantedly.com