Open2

Cloud Functions, Cloud Runのスピンアップタイムの比較と考察

kodukikoduki

概要

Cloud Runを使ってるアプリケーションで初回アクセス時の高速化がどこまで出来そうかを知るために、Cloud RunとCloud Functionsのスピアップタイムを比較しました。

基本的にJavaで開発するのでメインの想定はGraal VMをネイティブイメージをCloud Runで実行する場合とCloud Functions with Javaですが、比較のためにGoなど他言語でのFunctionsも計測しました。

スクラップの使い方がまだ良く分かってないけど、考察で違う見解や別言語あるいは別構成でのテストの結果とかよければ追加してみてください。

比較結果

実行結果のサマリは以下の通り。

平均スピンアップ(sec) 1回目 2回目
Cloud Run: Java (JVM) 7.321 7.702 6.94
Cloud Run: Java (native-image) 1.161 1.209 1.113
Functions: Java (JVM) 2.0535 2.122 1.985
Functions: Go 0.299 0.299 0.23
Functions: JavaScript 0.295 0.308 0.282
Functions: .NET Core 1.435 2.217 0.653

比較

構成

単純なHello Worldアプリケーション。Cloud Run版はQuarkusを使用。

計測方法

スピンアップの計測のため十分な時間をおいて2回計測。

スピンアップタイムの比較

Cloud Run: Java (JVM)

time curl https://run-java-jvm-xxxx.run.app
Hello, Cloud Run!curl https://run-java-jvm-xxxx.run.app  0.02s user 0.00s system 0% cpu 7.702 total
❯ time curl https://run-java-jvm-xxxx.run.app
Hello, Cloud Run!curl https://run-java-jvm-xxxx.run.app  0.00s user 0.01s system 0% cpu 6.940 total

Cloud Run: Java (native-image)

time curl https://run-java-native-xxxx.run.app
Hello, Cloud Run!curl https://run-java-native-xxxx.run.app  0.01s user 0.00s system 0% cpu 1.209 total
❯ time curl https://run-java-native-xxxx.run.app
Hello, Cloud Run!curl https://run-java-native-xxxx.run.app  0.01s user 0.01s system 1% cpu 1.113 total

Functions: Java (JVM)

time curl https://xxxx.cloudfunctions.net/example-fn-java
Hello, Cloud Functions!curl   0.01s user 0.00s system 0% cpu 2.122 total
❯ time curl https://xxxx.cloudfunctions.net/example-fn-java
Hello, Cloud Functions!curl   0.01s user 0.00s system 0% cpu 1.985 total

Functions: Go

time curl https://xxxx.cloudfunctions.net/fn-go
Hello, Go!curl https://xxxx.cloudfunctions.net/fn-go  0.00s user 0.01s system 3% cpu 0.368 total
❯ time curl https://xxxx.cloudfunctions.net/example-fn-go
Hello, Go!curl https://xxxx.cloudfunctions.net/fn-go  0.01s user 0.00s system 4% cpu 0.230 total

Functions: JavaScript

time curl https://xxxx.cloudfunctions.net/fn-js
Hello JS!curl https://xxxx.cloudfunctions.net/fn-js  0.01s user 0.00s system 3% cpu 0.308 total
❯ time curl https://xxxx.cloudfunctions.net/example-fn-js
Hello JS!curl https://xxxx.cloudfunctions.net/fn-js 0.01s user 0.00s system 4% cpu 0.282 total

Functions: .NET Core

time curl https://xxxx.cloudfunctions.net/fn-net
Hello .NET Core!curl https://xxxx.cloudfunctions.net/fn-net  0.01s user 0.00s system 0% cpu 2.217 total
❯ time curl https://xxxx.cloudfunctions.net/fn-net
Hello .NET Core!curl https://xxxx.cloudfunctions.net/fn-net  0.01s user 0.00s system 4% cpu 0.261 total

実行時間の比較

参考までにだけどスピンアップ後の実行時間の比較。どれも0.13ms - 0.14msと同じくらい。Hello Worldなので速度は言語レベルの差異はでずネットワークとか各種オーバーヘッドで固定でかかるレイテンシがこの結果だと思われる。

time curl https://run-java-jvm-xxxx.run.app
Hello, Cloud Run!curl https://run-java-jvm-xxxx.run.app  0.01s user 0.00s system 7% cpu 0.141 total
❯ time curl https://run-java-native-xxxx.run.app
Hello, Cloud Run!curl https://run-java-native-xxxx.run.app  0.01s user 0.00s system 8% cpu 0.133 total
❯ time curl https://xxxx.cloudfunctions.net/example-fn-java
Hello, Cloud Functions!curl   0.00s user 0.01s system 7% cpu 0.152 total
❯ time curl https://xxxx.cloudfunctions.net/fn-go
Hello, Go!curl https://xxxx.cloudfunctions.net/fn-go  0.01s user 0.00s system 6% cpu 0.169 total
❯ time curl https://xxxx.cloudfunctions.net/fn-js
Hello JS!curl https://xxxx.cloudfunctions.net/fn-js  0.01s user 0.00s system 8% cpu 0.146 total
❯ time curl https://xxxx.cloudfunctions.net/fn-net
Hello .NET !curl https://xxxx.cloudfunctions.net/fn-ne  0.01s user 0.00s system 8% cpu 0.136 total

考察

分かり切ってた事だけどCloud Run: Java (JVM)が5secから7 secとダントツで遅い。これはJVMの起動が遅い事に加えてCloud Run自体のスピンアップタイムが影響していると思われる。しかしながらnative-imageであっても1 sec前後ということでローカルでの直接実行と比較すると非常に遅い。

これはおそらくDocker系コンテナのオーバーヘッドが大きそう。実際ローカルで動かしても直接jarコマンドないしはネイティブ実行した場合は、Jarは一瞬ひっかかるのでおそらく0.5secから0.8sec程度、ネイティブの場合は瞬時に起動するので0.3sec以下くらいなのだが、dokcerで実行すると1 secから2 sec程度かかる。これと同等以上のオーバーヘッドがCloud Runには存在している。ただし、jarとネイティブの起動速度の差がローカルで実行するよりDocker上で実行する方が圧倒的に大きいので工夫の余地はあるかもしれない。

また、BorgベースのためかFunctionsの場合はJVMであっても2 sec程度とそれなりに速い。ただしNode.jsやGo言語などに至っては0.3 sec前後と爆速なのでこの差は大きい。今回は試してないがGo言語をCloud Runで実行した場合はGraalVMのネイティブイメージ同様に1秒程度はかかるだろう。

このあたりのCloud RunとFunctionsのオーバーヘッドの差異は将来的なGCP側のチューニングで改善する可能性もある。
特に直接Dockerfileを使わずにBuildpacksで作られたイメージの場合は特別な実行のされ方でスピンアップを短縮してくるという将来的な可能性も無くはない。
が、現状としてこのオーバーヘッドを取り除きたければCloud RunをやめてFunctionsを使うか、無料枠をあきらめてmin-instances を使うしかない。

GAEは今回検証してないがおそらくFunctionsに準じた結果になるはず。

まとめ

とりあず自分が細々とやってる個人プロダクトの速度を改善出来ないかとというモチベーションで調べてみたけど、やはりDockerのオーバーヘッドは無視出来なさそう。マイクロサービス化はしてるので良く使うというか最初に使うAPIをFunctions + Goで実装してその裏側で次に使いそうなAPIをスピンアップすることで見かけ上の速度を上げるとかは出来そうかなー。まあ、そこまでやる? って話はあるけれど。Functionsにしなくてもnative-imageで1秒くらいになるなら言語やFWを変える手間を考えたら妥当な対応かも。

業務で使うようなものなら素直にmin-instancesの料金を払うのが一番良さそうかな。ざっくり月に1000円ちょっとだし。

kodukikoduki

Java, Ruby, Goのスピンアップタイムの比較

GraalVM

❯ time curl https://app-graal-xxx.run.app/healthcheck
{"message":"Hello World, Java","date":"2021-05-23T21:18:50"}curl https://app-graal-xxx.run.app/healthcheck 0.00s user 0.01s system 1% cpu 1.189 total

❯ time curl https://app-graal-xxx.run.app/healthcheck
{"message":"Hello World, Java","date":"2021-05-24T00:58:10"}curl https://app-graal-xxx.run.app/healthcheck 0.01s user 0.01s system 0% cpu 1.389 total

❯ time curl https://app-graal-xxx.run.app/healthcheck
{"message":"Hello World, Java","date":"2021-05-24T01:34:54"}curl https://app-graal-xxx.run.app/healthcheck 0.00s user 0.01s system 1% cpu 1.058 total

JVM

❯ time curl https://app-jvm-xxx.run.app/healthcheck
{"message":"Hello World, Java","date":"2021-05-23T21:19:56"}curl https://app-jvm-xxx.run.app/healthcheck 0.01s user 0.00s system 0% cpu 5.851 total

❯ time curl https://app-jvm-xxx.run.app/healthcheck
{"date":"2021-05-24T00:57:50","message":"Hello World, Java"}curl https://app-jvm-xxx.run.app/healthcheck 0.01s user 0.00s system 0% cpu 5.912 total

❯ time curl https://app-jvm-xxx.run.app/healthcheck
{"date":"2021-05-24T01:33:01","message":"Hello World, Java"}curl https://app-jvm-xxx.run.app/healthcheck 0.01s user 0.00s system 0% cpu 5.547 total

Ruby

❯ time curl https://app-ruby-xxx.run.app/healthcheck
{"message":"Hello World, Ruby","date":"2021-05-24T00:57:24"}curl https://app-ruby-xxx.run.app/healthcheck 0.01s user 0.00s system 0% cpu 1.612 total

❯ time curl https://app-ruby-xxx.run.app/healthcheck
{"message":"Hello World, Ruby","date":"2021-05-24T01:32:24"}curl https://app-ruby-xxx.run.app/healthcheck 0.01s user 0.01s system 0% cpu 1.516 total

❯ time curl https://app-ruby-xxx.run.app/healthcheck
{"message":"Hello World, Ruby","date":"2021-05-24T02:00:39"}curl https://app-ruby-xxx.run.app/healthcheck 0.01s user 0.00s system 0% cpu 1.297 total

❯ time curl https://app-ruby-xxx.run.app/healthcheck
{"message":"Hello World, Ruby","date":"2021-05-24T03:23:06"}curl https://app-ruby-xxx.run.app/healthcheck 0.01s user 0.00s system 0% cpu 1.497 total

Go

❯ time curl https://app-go-xxx.run.app/healthcheck
{"message":"Hello World, Go","date":"2021-05-24T00:00:00"}curl https://app-go-xxx.run.app/healthcheck 0.01s user 0.00s system 2% cpu 0.457 total

❯ time curl https://app-go-xxx.run.app/healthcheck
{"message":"Hello World, Go","date":"2021-05-24T00:00:00"}curl https://app-go-xxx.run.app/healthcheck 0.01s user 0.00s system 1% cpu 0.622 total

❯ time curl https://app-go-xxx.run.app/healthcheck
{"message":"Hello World, Go","date":"2021-05-24T00:00:00"}curl https://app-go-xxx.run.app/healthcheck 0.01s user 0.00s system 1% cpu 0.631 total