Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

バッチアプリケーションの起動時間短縮 #340

Closed
ShumpeiYamada36 opened this issue Nov 6, 2023 · 7 comments · Fixed by #878
Closed

バッチアプリケーションの起動時間短縮 #340

ShumpeiYamada36 opened this issue Nov 6, 2023 · 7 comments · Fixed by #878
Assignees
Labels
要望 新機能や要望
Milestone

Comments

@ShumpeiYamada36
Copy link
Contributor

概要

Spring Batchでバッチアプリケーションを実装した際、起動時のBean初期化処理などでどうしても数秒以上の時間がかかってしまう。
実行時間の制約がシビアなバッチにおいてはこれが問題になることもあるので、そのようなケースでの対処方法について検討する。

詳細 / 機能詳細(オプション)

基本は概要に記載した通り。
一つの対策としてBeanの遅延初期化設定を入れることが考えられるが、別プロジェクトにてこの対策を実施した際は大きな改善には繋がらなかった。
Spring Batch 5から対応したGraalVMを用いたネイティブアプリ化を行えば大幅な起動時間の短縮につながると期待されるが、実装や動作環境などに対して特別な考慮は必要ないか調査する必要がある。
上記が現実的でない場合、Spring Batch以外の実装方法の候補はあるかも調査する。

完了条件

起動時間の短いバッチアプリケーションの実装方法が示されている。

@rnakagawa16
Copy link
Contributor

rnakagawa16 commented Dec 13, 2023

GraalVMの動作検証

大元のURL:https://spring.pleiades.io/spring-boot/docs/current/reference/html/native-image.html

GraalVMの利用方法

・Dockerを用いたやり方
・Gradleのプラグインを用いたやり方←今回はこちらの利用を前提に進める

GraalVMを用いたネイティブアプリ化

・GraalVMに対応したJDKのダウンロード
 https://medium.com/graalvm/using-graalvm-and-native-image-on-windows-10-9954dc071311
 上記URLの手順に従ってGraalVMのインストールを行う
 java -version gu -versionが正しく表示されていればOK
 
 gu install native-image
 
 を実行してnative-imageを利用できるようにする。
 gradleのプラグインを有効にするにはnative-imageが正しくインストールできていることが必要。
 
 VSCode上のターミナルから ./gradlew nativeCompileを実行してネイティブアプリ化する。
 しかし、これはVisual Studioの環境下でのみのサポートのためVSCodeでは実現できないことが分かった。
 
 image
 
 

@rnakagawa16
Copy link
Contributor

現時点での評価軸

起動時間

メモリ使用量

環境

・VSCode上で動作するか

・追加のインストール作業が必要ではないか

@tsuna-can-se tsuna-can-se modified the milestones: v1.0, v0.7 Jan 23, 2024
@tsuna-can-se tsuna-can-se modified the milestones: v0.7, v0.8 Mar 18, 2024
@rnakagawa16
Copy link
Contributor

ネイティブイメージのlog4j2のLoggerが対応していない問題の対応策を調査している
①log4j2 → java.util.loggerへの代替
②graalVMのコンパイル時にlog4j-coreのjarファイルをクラスパスに追加
③一部ライブラリを動的にコンパイルするreflection-config.jsonによる動作

①log4j2 → java.util.loggerへの代替

mybatisが java.util.loggerに対応していないため動作しないことが判明
https://mybatis.org/mybatis-3/ja/logging.html
よってロギングファザードはlog4j2もしくはlogbackによる動作にする必要がある

②graalVMのコンパイル時にlog4j-coreのjarファイルをクラスパスに追加

以下のようにlog4j-coreをクラスパスに追加

configurations {
	myConfiguration
}

dependencies {
       .....
       	myConfiguration files('C:/Maia/maia/samples/web-csr/dressca-backend/batch/libs/log4j-core-2.23.1.jar')
	myConfiguration sourceSets.main.runtimeClasspath
}

graalvmNative {
  binaries {
    main {
      javaLauncher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(17)
        vendor = JvmVendorSpec.matching("Oracle Corporation")
      }
      mainClass = 'com.dressca.batch.BatchApplication'
    }
  }
}

def myFatJar = tasks.register("myFatJar", Jar) {
    dependsOn 'compileJava', 'processResources', ':infrastructure:jar', ':application-core:jar', 'compileAotJava', 'processAotResources', 'processAot'
	duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    from {
        configurations.myConfiguration.collect { it.isDirectory() ? it : zipTree(it) }
    }
    manifest {
        attributes(
            'Main-Class': 'com.dressca.batch.BatchApplication',
            'Class-Path': configurations.myConfiguration.files.collect { it.absolutePath }.join(' ')
        )
    }
}

tasks.named("nativeCompile") {
    classpathJar = myFatJar.flatMap { it.archiveFile }
}

しかし、エラー内容に変わりはなかった。
nativeCompile時のjarファイル適用では発生するエラーに変わりはないよう。
exeファイル実行時にjarファイルを含ませるような方法がなければ正常に動作しないよう。

③一部ライブラリを動的にコンパイルするreflection-config.jsonによる動作

log4j2を動的にコンパイルする

org.apache.log4j.loggerのライブラリを動的コンパイルすることはできているが、
(おそらく)ロギングファザードのlog4j2ライブラリの動的コンパイルに対応していないため、
simpleLoggerへのキャストが動作せず、エラーが発生する。

logbackに変更し、動的にコンパイルする

ch.qos.logback:logback-classicのライブラリがnativeComplileで自動生成されるreflect-config.jsonに記載されていた。

  // batch/build/resources/aot/META-INF/native-image/reflect-config.json
  {
    "name": "org.springframework.boot.logging.logback.ColorConverter",
    "allPublicConstructors": true
  },
  {
    "name": "ch.qos.logback.classic.encoder.PatternLayoutEncoder",
    "allPublicConstructors": true,
    "queryAllPublicMethods": true,
    "allPublicMethods": true
  },

一度log4j2からlogbackへ変更しながら動作確認を実行

mybatis のloggerが動作しない問題

reflect-config.jsonにmybatisのロガークラスを動的コンパイルで実行するよう設定

  {
    "name" : "org.apache.ibatis.logging.LogFactory",
    "allDeclaredConstructors" : true,
    "allPublicConstructors" : true,
    "allDeclaredMethods" : true,
    "allPublicMethods" : true
  },
  {
    "name" : "org.apache.ibatis.logging.slf4j.Slf4jImpl",
    "allDeclaredConstructors" : true,
    "allPublicConstructors" : true,
    "allDeclaredMethods" : true,
    "allPublicMethods" : true
  }

MapperScanアノテーションによって設定していたMapperクラスが正常に読み取れないエラーが発生
(おそらく、別サブプロジェクトに記載されており正しく読み取れていないものと考えられる)
動的コンパイルするためにはすべてのMapperクラスを一つずつreflect-config.jsonに追加していく必要がある。
そのため、build.gradle上でMapperクラスを読み取り、動的コンパイルに追加する方法について調査。

task generateReflectConfig(type: JavaExec) {
    main = 'ReflectConfigGenerator'
    classpath = sourceSets.main.runtimeClasspath
    args 'com.dressca.infrastructure.repository.mybatis', 'reflect-config.json'
}

tasks.named('nativeCompile').configure {
    dependsOn generateReflectConfig
}

build.gradle上にタスクを定義し、nativeCompile時にreflcet-config.jsonに記載するよう設定する方法が有効か調査中。

@rnakagawa16
Copy link
Contributor

rnakagawa16 commented Mar 27, 2024

GraalVMを利用したネイティブイメージの実装方法

GraalVM JDKのインストール・設定

上記URLを参考にGraalVMのセッティングを行います。

Spring Boot プロジェクトの実装

propertiesファイルの編集

// build.gradle (batch)
plugins {
  	id 'org.graalvm.buildtools.native' version "${graalvmVersion}"
}
repositories {
	mavenCentral()
	maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
}
dependencies {
	implementation supportDependencies.mybatis_spring_native
}
graalvmNative {
  binaries {
    main {
      javaLauncher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(17)
        vendor = JvmVendorSpec.matching("Oracle Corporation")
      }
      mainClass = 'com.dressca.batch.BatchApplication'
    }
  }
}
// dependencies.gradle
ext {
    graalvmVersion = "0.9.28"
    mybatisNativeVersion = '0.1.0-SNAPSHOT'

    supportDependencies = [
        mybatis_spring_native : "org.mybatis.spring.native:mybatis-spring-native-core:$mybatisNativeVersion",
    ]
}

Mybatis Spring Native のエラー対応

[
    {
        "name" : "org.mybatis.spring.boot.autoconfigure.SpringBootVFS",
        "allDeclaredConstructors" : true,
        "allPublicConstructors" : true,
        "allDeclaredMethods" : true,
        "allPublicMethods" : true,
        "methods" : [
            { "name" : "<init>", "parameterTypes" : [] }
        ]
    },
    {
        "name" : "com.dressca.applicationcore.order.Address",
        "allDeclaredConstructors" : true,
        "allPublicConstructors" : true,
        "allDeclaredMethods" : true,
        "allPublicMethods" : true
    }
]

lombokの@valueのアノテーションはビルド時に読み込まれないようで、reflect-config.jsonでコンパイルする必要があるとの報告あり(https://stackoverflow.com/questions/74870429/spring-boot-3-native-with-lombok-immutable-value)。

ネイティブコンパイルとexeファイルの実行

  • ./gradlew clean batch:nativeCompileでネイティブイメージを作成します
    image
  • コンパイルが完了したら ./batch/build/native/nativeCompile/batch.exeで実行します
    image

@rnakagawa16
Copy link
Contributor

rnakagawa16 commented Mar 27, 2024

調査が必要な内容

ネイティブイメージのテスト方法

  • ./gradlew clean batch:nativeTestによるテストの実行

    • csvファイルを取得できないため、エラーが発生しているものと考えられる
    • image
    • データベースが更新されていることを確認する術があればネイティブイメージで正常に動作しているかが確認できる
  • h2databaseのログレベルを変更してSQLが実行されているかを確認する

    • spring.datasource.hikari.jdbc-url=jdbc:h2:mem:./data;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DEFAULT_NULL_ORDERING=HIGH;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;TRACE_LEVEL_FILE=4;
    • 画像の通り、SQLは正しく読み込まれ実行されていることがわかる
    • image
    • image

起動時間やビルド時間の高速が有効かどうか

  • ビルド時間
    image
    image
  • 起動時間

@rnakagawa16
Copy link
Contributor

rnakagawa16 commented Apr 12, 2024

ビルド時間

方法

Windows PowerShellで以下のコマンドを実行
Measure-Command {./gradlew clean batch:nativeCompile}

計測結果

回数 計測時間(分)
1 5.471898395
2 4.68909360666667
3 4.68901431
4 4.75594517333333
5 4.78200677666667

実行時間

方法

Windows PowerShellで以下のコマンドを実行
Measure-Command {./batch/build/native/nativeCompile/batch.exe }

計測結果

exeファイルの初回起動のみ時間がかかるよう、それ以降は0.2秒程度。

回数 計測時間(秒)
1 3.127847
2 0.2090029
3 0.2089103
4 0.2127338
5 0.2315858
6 0.2177371

メモリ使用量

方法

Ubuntu環境を構築し、メモリ使用量を計測する以下のコマンドを実行
/usr/bin/time -v ./gradlew batch:bootRun
/usr/bin/time -v ./batch/build/native/nativeCompile/batch.exe

計測結果

./gradlew による起動の場合

回数 最大メモリ使用量(kB)
1 109120
2 127484
3 119408
4 110148
5 116832

exeファイルによる起動の場合

回数 最大メモリ使用量(kB)
1 1604
2 1604
3 1604
4 1604
5 1604

@rnakagawa16
Copy link
Contributor

rnakagawa16 commented Apr 16, 2024

バッチ処理

バッチ処理とは

バッチ処理は、一連のタスクやプログラムを自動的に実行する処理方法です。
複数の処理をまとめて一度に実行することで、効率的な処理が可能となります。
主に大量のデータや繰り返し処理などに使用されます。

バッチ処理の種類

オンバッチ

オンバッチは、ユーザーの任意のタイミングで実行されるバッチ処理です。
ユーザーが必要に応じて実行することができます。
例えば、特定のデータを一括で処理したい場合や特定の操作を行うためにバッチ処理を実行する場合などに利用されます。

オンバッチはユーザーの操作に応じた柔軟な実行が特徴であることから、後述する定期バッチと比較して複雑なバッチ処理を実行できるメリットがあります。
しかしながら、処理が複雑になった場合にバッチ実行によるシステムの負荷が大きくなる可能性があります。
そのため、処理を高速化したり使用するリソースを削減したりといった、システムの負荷を軽減するための対処を考える必要があります。

定期バッチ

定期バッチは、あらかじめ設定された一定の間隔で自動的に実行されるバッチ処理です。
定期的にデータを更新する必要がある場合に利用されます。
例えば、毎日深夜にデータベースのバックアップを取る処理や、毎週月曜日に売上データを集計する処理などです。

定期バッチは、あらかじめ設定されたリソースを利用して実行されるため、リソースを効率的に管理することができます。
その反面、他の重要な処理やリクエストとリソースが競合する可能性があり、複雑なバッチ処理には向いていません。
また、定期バッチ処理は自動的に実行されるため、エラーや問題が発生した場合にその発見が遅れる可能性もあります。

バックエンドアプリケーションでのバッチ処理

前述したとおり、大量で反復的なデータを処理するバッチ処理を行う場合、システムの負荷が高く非効率となる場合があります。

このような場合、以下の方法で解決できます。

クラウドのリソースを利用してバッチ処理をスケジューリングする

クラウドサービスを利用してバッチ処理を自動的に実行するようスケジューリングすることで、以下のメリットがあります。

効率性
クラウドサービスのスケジューリング機能を適用することで、人による操作を最小限に抑え、反復的なタスクを定期バッチとして実行することができます。

高速化
クラウドサービスは大量のコンピューティングリソースを提供しており、これによりバッチ処理の高速化が期待できます。

リソースの削減
必要に応じてリソースをスケールアップ、スケールダウンすることで、メモリ使用量を最適化し無駄なリソースの消費を抑えることができます。

バッチ処理を行う際のシステムの負荷が問題となっている場合、バックエンドアプリケーションをクラウドのリソース上に配置して、システムの負荷低減を図ることを考慮してください。

@scheduledアノテーションを用いた実行

クラウドのサービスを利用しない場合、バッチ処理を高速に実行するために Spring Boot アプリケーションの機能のみで対処する必要があります。
しかし、 Spring Boot アプリケーションは Bean の初期化を起動時に実施する特性上、アプリケーションの起動がバッチ処理のボトルネックとなる場合があります。
そこで、バッチ処理を実行するたびに Spring Boot アプリケーションを起動する必要がないように、バッチアプリケーションを定期実行するための機能が提供されています。
@Scheduled アノテーションは、メソッドの定期的な実行を行うことができる機能です。
アノテーションを利用すれば、一度アプリケーションを起動した後は定期的にバッチ処理が自動実行されるようになり、 Bean の初期化にかかる起動時間を短縮できます。
これにより、バッチアプリケーションの高速化やリソースの削減が期待できます。
そのため、定期バッチのようにあらかじめ設定された間隔で実行する際には本機能の利用を推奨します。

実装方法については、以下を参照してください。

基本的には上記2つによる解決を推奨しますが、開発における制約上既存のアプリケーションコードは変更できないが処理の高速化や実行時のリソースを削減を実現したい場合があります。
そのような場合、以下の方法で解決できる可能性があります。

GraalVM の利用

GraalVM は、 Oracle 社が開発した高パフォーマンスの JDK です。
Spring Boot をはじめとする JVM ベースのアプリケーションを事前にネイティブイメージにコンパイルします。
これにより、ソースコードの変更を最小限に抑え、起動時間を短縮しリソース使用量を低減させることができます。

実装方法については、以下を参照してください。

??? note "GraalVMを利用する際の注意点"
JVM ベースのアプリケーションではなくGraalVMを用いたアプリケーションにコンパイルされるため、JVM でサポートされていた OSS ライブラリが GraalVM ではサポートされていない可能性があります。
サポートしている OSS ライブラリの一覧は以下をご確認ください。

- [GraalVM のサポートライブラリ一覧](https://www.graalvm.org/native-image/libraries-and-frameworks/)
また、サポートしているライブラリでも、利用するために追加で設定が必要な場合があります。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
要望 新機能や要望
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants