Slide 1

Slide 1 text

Java x Spring Boot製アプリケーションの コールドスタートに立ち向かう! 〜暖機運転のアプローチいろいろやってみた〜 2024/10/27 Presentation for JJUG CCC 2024 Fall @kazu_kichi_67

Slide 2

Slide 2 text

#jjug_ccc #jjug_ccc_j

Slide 3

Slide 3 text

自己紹介 Name - @kazu_kichi_67 Company - 中堅独立系SIer Skills - Java / Spring Boot / Scrum Master / DDD / Clean Architecture / etc… Hobby - フットサル / 爬虫類 / キャンプ / サウナ / ウイスキー / クラフトビール / etc…

Slide 4

Slide 4 text

本題に入る前に準備

Slide 5

Slide 5 text

背景 ECサイトにおけるカート決済システムのリプレイス案件 Java 21 Spring Boot 3系 Amazon EKS(Elastic Kubernetes Service) Domain-Driven Design Onion Architecture バックエンドのAPIアプリケーション

Slide 6

Slide 6 text

なんか、最初のリクエストだけ 異常に遅くない・・??

Slide 7

Slide 7 text

地味に困ってる人多い気がするけど、 情報少ないな・・

Slide 8

Slide 8 text

「情報は発信する人に集まる」 よし、登壇しよう!!

Slide 9

Slide 9 text

アジェンダ 話すこと コールドスタートと暖機運転について 自力で頑張る暖機運転のアプローチ ランタイムが提供するアプローチ 話さないこと 各アプローチの詳細な説明 コールドスタート問題に対する銀の弾丸 背景によって最適解は異なるため、各々で計測・検証をお願いします 🙏

Slide 10

Slide 10 text

アジェンダ 話すこと コールドスタートと暖機運転について 自力で頑張る暖機運転のアプローチ ランタイムが提供するアプローチ 話さないこと 各アプローチの詳細な説明 コールドスタート問題に対する銀の弾丸 背景によって最適解は異なるため、各々で計測・検証をお願いします 🙏

Slide 11

Slide 11 text

コールドスタートとは何か? スタートアップ:アプリケーションが起動するまでの時間 ウォームアップ:ピークパフォーマンスに達するまでの時間 スタートアップ + ウォームアップ => コールドスタート

Slide 12

Slide 12 text

コールドスタートとは何か? スタートアップ:アプリケーションが起動するまでの時間 ウォームアップ:ピークパフォーマンスに達するまでの時間 スタートアップ + ウォームアップ => コールドスタート

Slide 13

Slide 13 text

ウォームアップの段階 初回リクエスト特有の遅延: 1回のリクエストでそこそこ効く JITコンパイラ(Just In Time Compile)による最適化: 一般的にC1で数千、C2で3万回 程度のリクエストが必要 レイテンシーの遷移イメージ Request Time

Slide 14

Slide 14 text

ウォームアップの段階 初回リクエスト特有の遅延: 1回のリクエストでそこそこ効く JITコンパイラ(Just In Time Compile)による最適化: 一般的にC1で数千、C2で3万回 程度のリクエストが必要 レイテンシーの遷移イメージ Request Time

Slide 15

Slide 15 text

ウォームアップの重要性 アプリケーションの生存時間は短くなっている コンテナやサーバレスの普及 Podの再起動、オートスケールアップ アジャイル開発による、高速なリリースサイクル 恩恵よりもデメリットが目立つようになってきた

Slide 16

Slide 16 text

ウォームアップの重要性 アプリケーションの生存時間は短くなっている コンテナやサーバレスの普及 Podの再起動、オートスケールアップ アジャイル開発による、高速なリリースサイクル 恩恵よりもデメリットが目立つようになってきた

Slide 17

Slide 17 text

どうやって暖機するのか? ユーザーからのリクエストを受け付ける前に、実際にAPIを呼び出してあげる 一例として、 Spring Boot Actuatorでアプリケーションの状態を可視化 KubernetesのStartup Probeでリクエストを投げる 参照系は比較的容易、では更新系は? 参考 Liveness and Readiness Probes with Spring Boot Liveness, Readiness, and Startup Probes

Slide 18

Slide 18 text

どうやって暖機するのか? ユーザーからのリクエストを受け付ける前に、実際にAPIを呼び出してあげる 一例として、 Spring Boot Actuatorでアプリケーションの状態を可視化 KubernetesのStartup Probeでリクエストを投げる 参照系は比較的容易、では更新系は? 参考 Liveness and Readiness Probes with Spring Boot Liveness, Readiness, and Startup Probes

Slide 19

Slide 19 text

どうやって暖機するのか? ユーザーからのリクエストを受け付ける前に、実際にAPIを呼び出してあげる 一例として、 Spring Boot Actuatorでアプリケーションの状態を可視化 KubernetesのStartup Probeでリクエストを投げる 参照系は比較的容易、では更新系は? 参考 Liveness and Readiness Probes with Spring Boot Liveness, Readiness, and Startup Probes

Slide 20

Slide 20 text

自力で頑張るアプローチ

Slide 21

Slide 21 text

繰り返し呼べるように環境を整える 注文APIについて考えてみる 暖機用のユーザ、商品を準備する 自動で注文をキャンセルする仕組みが必要 キャンセルを繰り返すユーザに対して罰則があればその対象外とする 検索に載せないなど、一般ユーザには買えない仕組みが欲しい 計測や分析側への影響も考慮する必要あり などなど・・・

Slide 22

Slide 22 text

❌ めっちゃつらい 🤮

Slide 23

Slide 23 text

特殊ルートの実装 コードサンプル 1 @Repository 2 public class OrderRepositoryImpl implements OrderRepository { 3 public OrderId create(Order order) { 4 if (order.userId().isWarmup()) { 5 // 暖機運転用の特殊ルート 6 return OrderId.ofWarmup(); 7 } else { 8 // 注文作成 9 } 10 } 11 }

Slide 24

Slide 24 text

特殊ルートの実装 コードサンプル 4 if (order.userId().isWarmup()) { 5 // 暖機運転用の特殊ルート 6 return OrderId.ofWarmup(); 1 @Repository 2 public class OrderRepositoryImpl implements OrderRepository { 3 public OrderId create(Order order) { 7 } else { 8 // 注文作成 9 } 10 } 11 }

Slide 25

Slide 25 text

特殊ルートの実装 メリ/デメ ⭕️ 最も手軽ですぐに始められる 🔺 特殊ルートから先は暖機されないため、その分効果が小さい ❌ 実装・メンテナンスコスト大 ビジネスロジック(ドメインモデル)に余計な関心ごとが入り込む

Slide 26

Slide 26 text

特殊ルートの実装 メリ/デメ ⭕️ 最も手軽ですぐに始められる 🔺 特殊ルートから先は暖機されないため、その分効果が小さい ❌ 実装・メンテナンスコスト大 ビジネスロジック(ドメインモデル)に余計な関心ごとが入り込む

Slide 27

Slide 27 text

特殊ルートの実装 メリ/デメ ⭕️ 最も手軽ですぐに始められる 🔺 特殊ルートから先は暖機されないため、その分効果が小さい ❌ 実装・メンテナンスコスト大 ビジネスロジック(ドメインモデル)に余計な関心ごとが入り込む 一言メモ 本当にクリティカルな時にだけ、小規模かつ暫定的に導入しましょう

Slide 28

Slide 28 text

Dynamic Dependency Injection コードサンプル Interface BeanFactory Bean Scopes 1 @Configuration 2 @RequiredArgsConstructor 3 public class WarmupConfiguration { 4 private final BeanFactory beanFactory; 5 private final WarmupService warmupService; 6 7 @Bean 8 @Primary 9 @Scope(value = "prototype", proxyMode = ScopedProxyMode.INTERFACES) 10 public OrderRepository orderRepository() { 11 return beanFactory.getBean(warmupService.getMode() + "OrderRepository", OrderRepository.class); 12 } 13 }

Slide 29

Slide 29 text

Dynamic Dependency Injection コードサンプル Interface BeanFactory Bean Scopes 4 private final BeanFactory beanFactory; 7 @Bean 8 @Primary 9 @Scope(value = "prototype", proxyMode = ScopedProxyMode.INTERFACES) 10 public OrderRepository orderRepository() { 11 return beanFactory.getBean(warmupService.getMode() + "OrderRepository", OrderRepository.class); 12 } 1 @Configuration 2 @RequiredArgsConstructor 3 public class WarmupConfiguration { 5 private final WarmupService warmupService; 6 13 }

Slide 30

Slide 30 text

Dynamic Dependency Injection メリ/デメ ⭕️ 暖機の関心ごとをクラス毎に切り離せる 🔺 切り替えたクラスから先のコードが異なるため、その分効果が小さい 🔺 実装・メンテナンスコスト中 ❌ 該当クラスが呼び出される度にDIされる

Slide 31

Slide 31 text

Dynamic Dependency Injection メリ/デメ ⭕️ 暖機の関心ごとをクラス毎に切り離せる 🔺 切り替えたクラスから先のコードが異なるため、その分効果が小さい 🔺 実装・メンテナンスコスト中 ❌ 該当クラスが呼び出される度にDIされる

Slide 32

Slide 32 text

Dynamic Dependency Injection メリ/デメ ⭕️ 暖機の関心ごとをクラス毎に切り離せる 🔺 切り替えたクラスから先のコードが異なるため、その分効果が小さい 🔺 実装・メンテナンスコスト中 ❌ 該当クラスが呼び出される度にDIされる 一言メモ 特殊ルートだらけになりそうな時に選択肢になりうる

Slide 33

Slide 33 text

Dynamic Data Source コードサンプル ① Class AbstractRoutingDataSource 1 @Component 2 @RequiredArgsConstructor 3 public class DynamicRoutingDataSource extends AbstractRoutingDataSource { 4 private final WarmupService warmupService; 5 6 @Override 7 protected Object determineCurrentLookupKey() { 8 return warmupService.getMode(); 9 } 10 }

Slide 34

Slide 34 text

Dynamic Data Source コードサンプル ① Class AbstractRoutingDataSource 3 public class DynamicRoutingDataSource extends AbstractRoutingDataSource { 6 @Override 7 protected Object determineCurrentLookupKey() { 1 @Component 2 @RequiredArgsConstructor 4 private final WarmupService warmupService; 5 8 return warmupService.getMode(); 9 } 10 }

Slide 35

Slide 35 text

Dynamic Data Source コードサンプル ② 1 @Configuration 2 @RequiredArgsConstructor 3 public class DynamicDataSourceConfiguration { 4 private final DynamicRoutingDataSource dynamicRoutingDataSource; 5 6 ・・・ 7 8 @Bean 9 @Primary 10 public DataSource dataSource() { 11 Map dataSources = new LinkedHashMap<>(); 12 dataSources.put(WarmupMode.PROD, prodDataSource()); 13 dataSources.put(WarmupMode.WARMUP, warmupDataSource()); 14 15 dynamicRoutingDataSource.setTargetDataSources(dataSources); 16 dynamicRoutingDataSource.setDefaultTargetDataSource(prodDataSource()); 17 18 return dynamicRoutingDataSource; 19 } 20 }

Slide 36

Slide 36 text

Dynamic Data Source コードサンプル ② 12 dataSources.put(WarmupMode.PROD, prodDataSource()); 13 dataSources.put(WarmupMode.WARMUP, warmupDataSource()); 14 15 dynamicRoutingDataSource.setTargetDataSources(dataSources); 1 @Configuration 2 @RequiredArgsConstructor 3 public class DynamicDataSourceConfiguration { 4 private final DynamicRoutingDataSource dynamicRoutingDataSource; 5 6 ・・・ 7 8 @Bean 9 @Primary 10 public DataSource dataSource() { 11 Map dataSources = new LinkedHashMap<>(); 16 dynamicRoutingDataSource.setDefaultTargetDataSource(prodDataSource()); 17 18 return dynamicRoutingDataSource; 19 } 20 }

Slide 37

Slide 37 text

Dynamic Data Source メリ/デメ ⭕️ データを自由に準備できるため、暖機の自由度が高い ⭕️ 暖機効果が大きい ⭕️ 実装・メンテナンスコスト小 ❌ インフラの整備が必要

Slide 38

Slide 38 text

Dynamic Data Source メリ/デメ ⭕️ データを自由に準備できるため、暖機の自由度が高い ⭕️ 暖機効果が大きい ⭕️ 実装・メンテナンスコスト小 ❌ インフラの整備が必要

Slide 39

Slide 39 text

Dynamic Data Source メリ/デメ ⭕️ データを自由に準備できるため、暖機の自由度が高い ⭕️ 暖機効果が大きい ⭕️ 実装・メンテナンスコスト小 ❌ インフラの整備が必要 一言メモ ややトリッキーではあるが、インフラ構築さえできれば色々なユースケースに対応でき そう

Slide 40

Slide 40 text

前半のまとめ 特殊ルート Dynamic Dependency Injection Dynamic Data Source 暖機効果 🔺 中 🔺 中 ⭕️ 大 実装・メンテナンスコスト ❌ 大 🔺 中 ⭕️ 小 その他 ⭕️ 手軽 ⭕️ クラス毎に切り替え可能 ⭕️ 暖機の自由度が高い ❌ 都度DIあり ❌ インフラ整備の必要あり

Slide 41

Slide 41 text

前半のまとめ 特殊ルート Dynamic Dependency Injection Dynamic Data Source 暖機効果 🔺 中 🔺 中 ⭕️ 大 実装・メンテナンスコスト ❌ 大 🔺 中 ⭕️ 小 その他 ⭕️ 手軽 ⭕️ クラス毎に切り替え可能 ⭕️ 暖機の自由度が高い ❌ 都度DIあり ❌ インフラ整備の必要あり 結論 どのアプローチもデメリット(痛み)を伴う

Slide 42

Slide 42 text

ランタイムが提供する アプローチ

Slide 43

Slide 43 text

Class Data Sharing(CDS) Java JEP 310: Application Class-Data Sharing Java 10で導入された、手動でアーカイブを作成する仕組み JEP 341: Default CDS Archives Java 12で導入された、ユーザの操作なしにデフォルトでCDSが有効になる機能 JEP 350: Dynamic CDS Archives Java 13で導入された、アプリケーション終了時にアーカイブを作成してくれる仕組み Spring Boot Class Data Sharing spring.context.exit=onRefresh は3.2以降、 jarmode=tools extract は3.3以降で利用可能

Slide 44

Slide 44 text

Class Data Sharing(CDS) 出典元: https://shipilev.net/talks/j1-Oct2011-21682-benchmarking.pdf

Slide 45

Slide 45 text

Class Data Sharing(CDS) メリ/デメ ⭕️ 起動時間の削減 ⭕️ 初回リクエストの遅延緩和 ⭕️ 制限事項が少なく、導入コストは低め 🔺 ピークパフォーマンスに達するまでの時間へのアプローチではない 🔺 後述のアプローチに比べると効果は控えめ 実体験で約30%ほどの削減効果がありました

Slide 46

Slide 46 text

Class Data Sharing(CDS) メリ/デメ ⭕️ 起動時間の削減 ⭕️ 初回リクエストの遅延緩和 ⭕️ 制限事項が少なく、導入コストは低め 🔺 ピークパフォーマンスに達するまでの時間へのアプローチではない 🔺 後述のアプローチに比べると効果は控えめ 実体験で約30%ほどの削減効果がありました

Slide 47

Slide 47 text

Native化(GraalVM) GraalVM AOTコンパイル(Ahead of Time Compile): 事前にネイティブコードにコンパイルする Spring Boot Introducing GraalVM Native Images 3.0以降で利用可能

Slide 48

Slide 48 text

Native化(GraalVM) 出典元: https://shipilev.net/talks/j1-Oct2011-21682-benchmarking.pdf

Slide 49

Slide 49 text

Native化(GraalVM) メリ/デメ ⭕️ 起動時間の大幅な削減 ⭕️ 起動直後からピークパフォーマンス 🔺 利用しているライブラリがちゃんと動くかは要検証 Libraries and Frameworks Tested with Native Image 🔺 コンパイルに時間がかかり、開発体験が変わる ❌ アプリケーションの規模などにもよるが、移行のハードルは高め リフレクションのように実行時に決まる要素については、コンパイル時に明示的に指 定する必要がある

Slide 50

Slide 50 text

Native化(GraalVM) メリ/デメ ⭕️ 起動時間の大幅な削減 ⭕️ 起動直後からピークパフォーマンス 🔺 利用しているライブラリがちゃんと動くかは要検証 Libraries and Frameworks Tested with Native Image 🔺 コンパイルに時間がかかり、開発体験が変わる ❌ アプリケーションの規模などにもよるが、移行のハードルは高め リフレクションのように実行時に決まる要素については、コンパイル時に明示的に指 定する必要がある

Slide 51

Slide 51 text

CRaC(Coordinated Restore at Checkpoint) Java CRaC Project、CRaC/docs アプリケーションのチェックポイントを作成し、復元する形で起動できる 現時点だと、Azul Zulu、Liberica JDKで利用可能 Spring Boot Checkpoint and Restore With the JVM spring.context.checkpoint=onRefresh は3.2以降で利用可能 活用事例 SnapStart で AWS Lambda 関数の Java コールドスタートを削減する

Slide 52

Slide 52 text

CRaC(Coordinated Restore at Checkpoint) Java CRaC Project、CRaC/docs アプリケーションのチェックポイントを作成し、復元する形で起動できる 現時点だと、Azul Zulu、Liberica JDKで利用可能 Spring Boot Checkpoint and Restore With the JVM spring.context.checkpoint=onRefresh は3.2以降で利用可能 活用事例 SnapStart で AWS Lambda 関数の Java コールドスタートを削減する

Slide 53

Slide 53 text

CRaC(Coordinated Restore at Checkpoint) 出典元: https://shipilev.net/talks/j1-Oct2011-21682-benchmarking.pdf

Slide 54

Slide 54 text

CRaC(Coordinated Restore at Checkpoint) メリ/デメ ⭕️ 起動時間の大幅な削減 ⭕️ ピークパフォーマンスに達するまでの時間 チェックポイントの作成タイミングに依存する 🔺 チェックポイント作成時にDB接続やファイルハンドルを閉じる必要がある 🔺 シークレットな情報がスナップショットに含まれるリスクがある ❌ Linux KernelのCheckpoint/Restore in Userspace(CRIU)を利用するため、実行環境 に依存する 特権操作が必要になるため、プラットフォームによっては利用できない

Slide 55

Slide 55 text

CRaC(Coordinated Restore at Checkpoint) メリ/デメ ⭕️ 起動時間の大幅な削減 ⭕️ ピークパフォーマンスに達するまでの時間 チェックポイントの作成タイミングに依存する 🔺 チェックポイント作成時にDB接続やファイルハンドルを閉じる必要がある 🔺 シークレットな情報がスナップショットに含まれるリスクがある ❌ Linux KernelのCheckpoint/Restore in Userspace(CRIU)を利用するため、実行環境 に依存する 特権操作が必要になるため、プラットフォームによっては利用できない

Slide 56

Slide 56 text

Project Leyden Java Project Leyden CDSと事前最適化 (AOT optimization) を活用したアプローチ 起動時間、ピークパフォーマンスまでの時間、メモリの改善を目指している Leyden Early Access Release

Slide 57

Slide 57 text

Project Leyden Java Project Leyden CDSと事前最適化 (AOT optimization) を活用したアプローチ 起動時間、ピークパフォーマンスまでの時間、メモリの改善を目指している Leyden Early Access Release → 有力な選択肢になりそう。  今後の動向に注目!!

Slide 58

Slide 58 text

まとめ 暖機運転は可能な限り行うのが望ましい。 ただし、自力で頑張るのは痛みが伴う場合があるので、 コストやリスクのトレードオフを考慮し、本当に必要なところだけ導入する コールドスタートに対するランタイムのアプローチは複数存在するが、 こちらもそれぞれ制約があるため、しっかりと見極めた上で選択する Javaの今後の進化に期待しましょう!!

Slide 59

Slide 59 text

まとめ 暖機運転は可能な限り行うのが望ましい。 ただし、自力で頑張るのは痛みが伴う場合があるので、 コストやリスクのトレードオフを考慮し、本当に必要なところだけ導入する コールドスタートに対するランタイムのアプローチは複数存在するが、 こちらもそれぞれ制約があるため、しっかりと見極めた上で選択する Javaの今後の進化に期待しましょう!!

Slide 60

Slide 60 text

まとめ 暖機運転は可能な限り行うのが望ましい。 ただし、自力で頑張るのは痛みが伴う場合があるので、 コストやリスクのトレードオフを考慮し、本当に必要なところだけ導入する コールドスタートに対するランタイムのアプローチは複数存在するが、 こちらもそれぞれ制約があるため、しっかりと見極めた上で選択する Javaの今後の進化に期待しましょう!!

Slide 61

Slide 61 text

End 良いJava Lifeを! Thank you for listening!