HotSpot
JVMを用いた一般的なJavaは、起動直後はインタプリタのように動作するため、パフォーマンスが出ません。
サービスインからすぐにパフォーマンスを出すためには、ウォームアップ(暖機運転)が必須です。
本記事ではSpring
Boot+Kubernetesという環境という前提で、その対応方法を紹介します。
サンプルコード
こちらに置いております。
abekoh/spring-warmup-on-k8s
アプリケーションについて
ユーザ登録を行うだけのWebAPIを用意しています。実際にはDB書き込みなどは行わず、標準出力ログとして流れるだけです。
$ cat /tmp/req.json
{
"firstName": "Taro",
"lastName": "Yamada",
"birthYear": 1990,
"birthMonth": 5,
"birthDate": 3
}
$ curl -s -X POST -H "Content-Type: application/json" http://localhost:30080/api/users -d @/tmp/req.json | jq .
{
"isSucceeded": true,
"userAddResponse": {
"user": {
"userId": {
"id": "59036d2f-55e5-4977-bbe7-caaa515ba030"
},
"name": {
"firstName": "Taro",
"lastName": "Yamada"
},
"birthday": {
"date": "1990-05-03"
},
"isDummy": false
}
}
}
ウォームアップ処理について
本題です。
Liveness Probe / Readiness Probe の設定
まず、ウォームアップが終わってサービスインができるか否かを判別できるように設定を施します。
Kubenetes(以下k8s)では「アプリケーションが生きているか」を確認するLiveness
Probe, 「アプリケーションがリクエストを受け付けて良いか」を確認するRediness
Probeという仕組みがあります。
Configure Liveness, Readiness and Startup Probes | Kubernetes
この仕様に簡単に対応できる設定が、Spring Boot 2.3にて追加されました。
Liveness and Readiness Probes with Spring Boot
有効にするには
Spring Boot Actuator
を導入、 management.endpoint.health.probes.enabled=true
または
spring.main.cloud-platform=kubernetes
と設定を追記すればOKです。
後者だと他にもk8sに関する設定がなされるようなので、サンプルでは後者を選んでいます。
k8sのdeployment.yamlは次のように設定します。
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
ここではデフォルトのままですが、起動ループに陥る場合などはperiodSeconds
,
initialDelaySeconds
,
failureThreshold
の設定を見直しましょう。
ウォームアップの実行
ウォームアップとして行うことは単純で、事前にリクエストをたくさん投げておくことです。
ここでは Spring Bootの
ApplicationRunner
を実装したクラスにその処理を書きます。
ApplicationRunnerはアプリケーション起動後、サービスイン前という状態で実行されます。
このとき、LivenessProbe=OK、ReadinessProbe=NGという状態になります。
Spring Boot Reference Documentation / 4.1.6. Application Availability
ウォームアップ処理のために事前リクエストを行うにはちょうど良いタイミングです。
以下はそのサンプルです。
HTTPクライアントは何でも良いですが、Spring WebFluxのWebClientがMVCでも使えるとのことで、それで実装してみました。
@Slf4j
@Component
public class WarmupRunner implements ApplicationRunner {
private final WebClient webClient;
private final WarmupProperty warmupProperty;
public WarmupRunner(
WebClient.Builder webClientBuilder,
WarmupProperty warmupProperty,
@Value("${server.port}") Integer port) {
this.webClient =
webClientBuilder.baseUrl(String.format("http://localhost:%d/api/users", port)).build();
this.warmupProperty = warmupProperty;
}
@Override
public void run(ApplicationArguments args) throws Exception {
if (warmupProperty.getRequestCount() == null || warmupProperty.getRequestCount() <= 0) {
log.info("skip warmup");
return;
}
var request =
WebApiUserAddRequest.builder()
.firstName("Taro")
.lastName("Yamada")
.birthYear(1970)
.birthMonth(1)
.birthDate(1)
.isDummy(true)
.build();
log.info("start warmup");
webClient
.post()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(request)
.retrieve()
.bodyToMono(Object.class) // 結果は使わないので適当なところにマッピング
.repeat(warmupProperty.getRequestCount())
.blockLast();
log.info("finish warmup");
}
}
リクエストを実行する回数はWarmupPropertyというクラスに設定値が入るようにしております。
ベンチマーク
試しにどれくらいウォームアップによる効果があるのか、ベンチマークを実施してみました。
scripts/generate_request.pyにランダムなリクエストを生成できるPythonスクリプトを置き、vegetaを使ってテストしました。
python3 scripts/generate_requests.py | vegeta attack -rate=1000/s -lazy -format=json -duration=60s > /tmp/result.bin
対象はreplicas=2として2podsで動くアプリケーションで、1000RPSで60秒間投げてみました。
ウォームアップのリクエスト回数別の結果は次のとおりです。
min | mean | 50 | 90 | 95 | 99 | max | |
---|---|---|---|---|---|---|---|
0 | 0.499 | 11.074 | 0.660 | 0.988 | 1.338 | 529.671 | 1396.000 |
100 | 0.507 | 1.608 | 0.703 | 1.052 | 1.287 | 2.65 | 286.235 |
1,000 | 0.518 | 1.688 | 0.720 | 1.042 | 1.267 | 2.314 | 312.099 |
10,000 | 0.512 | 1.309 | 0.652 | 0.823 | 0.918 | 1.384 | 242.643 |
行はそれぞれウォームアップのリクエスト数が0回、100回…となります。各セルの単位はミリ秒です。
50,90,95,99はそれぞれパーセンタイル値で、例えば「99%のリクエストはこのレスポンスタイムに抑えられる」という値となります。
0回とその他では平均値、最大値が大きく異なります。100回と1,000回ではあまり変わらない結果となりました。(むしろ悪化してるところも)
10,000回では特に最適化されているように見えます。
さらに細かく調整すればより良くなると思いますが、ウォームアップの有用性が確認できました。
参考資料
以下の資料が非常に参考になりました。
-
2020-06-16 JSUG勉強会 2020年その5 Spring Boot 2.3 徹底解説
#jsug|諏訪真一|note
- こちらに貼ってある"Deep dive into Spring Boot 2.3"のスライドが非常に参考になります。
-
KubernetesでJVMアプリを動かすための実践的ノウハウ集 / JVM on
Kubernetes - Speaker Deck
- こちらではサイドカーでウォームアップを行う手法が紹介されています。その他の設定についても勉強になります。