こんにちは。
serverless framework 芸人の遠藤です。
はじめに
先日、ネットを徘徊していたら、以下のような記事を見かけました。
https://www.gomomento.com/blog/how-we-turned-up-the-heat-on-node-js-lambda-cold-starts
ざっくり要約すると...
- serverless framework のプラグインである、 serverless-esbuild を使用すると、コールドスタートの実行時間が 1000ms から 100ms まで短縮できた。
- 同じことを、 serverless framework 使わずに、 esbuild などを駆使して再現できないか試してみるよ。
- 結果、 600ms まで 短縮できたよ。(40%)
という内容です。
個人的に驚いたのは、短縮前のコールドスタートが 1000ms って早くない???
という点でした。
体感ですが、 Lambda のコールドスタートには、 5秒近くかかるイメージがあります。
記事では、 node.js を使用していますが、私が普段実行している環境が Python なので、その違いによるものでは?と疑問が湧きました。
もしかすると、言語によりかなり差があるのでは、ということで、今回は serverless framework がテンプレートとして用意している言語の、ほぼ初期状態で、各 function を実行し、その実行時間などをまとめてみたいと思います。
Ruby
特に深い理由はないですが、まずは ruby から実行してみます。
$ sls create -t aws-ruby
作成されるテンプレートはいたってシンプルですね。
handler.rb
のみです。
require 'json'
def hello(event:, context:)
{
statusCode: 200,
body: {
message: 'Go Serverless v1.0! Your function executed successfully!',
input: event
}.to_json
}
end
テンプレートのままだと、トリガーが存在しないため、 httpApi
を有効にしておきます。(コメントの削除と、コメントアウトを外すだけ)
これ以降、 GO を除く全てのプロジェクトで、 httpApi を有効にしています。
(GOのテンプレートは最初から有効でした)
handler: handler.hello
events:
- httpApi:
path: /users/create
method: get
では、デプロイして実行してみます。
$ curl -s -o /dev/null -w '%{json}' https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/users/create | jq | grep time
"time_appconnect": 0.619892,
"time_connect": 0.254073,
"time_namelookup": 0.078891,
"time_pretransfer": 0.620351,
"time_redirect": 0,
"time_starttransfer": 1.195944,
"time_total": 1.196165,
確かに1秒ちょっとですね。
$ curl -s -o /dev/null -w '%{json}' https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/users/create | jq | grep time
"time_appconnect": 0.582121,
"time_connect": 0.194722,
"time_namelookup": 0.006669,
"time_pretransfer": 0.582391,
"time_redirect": 0,
"time_starttransfer": 0.818774,
"time_total": 0.819033,
ただし、2回目以降でも 0.7秒を切ることはありませんでした。
私の通信環境にもよりそうです。
試しに、 region を東京にしてみます。
curl -s -o /dev/null -w '%{json}' https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/users/create | jq | grep time
"time_appconnect": 0.099594,
"time_connect": 0.065416,
"time_namelookup": 0.032703,
"time_pretransfer": 0.099695,
"time_redirect": 0,
"time_starttransfer": 0.552264,
"time_total": 0.552621,
初回は 0.55秒
curl -s -o /dev/null -w '%{json}' https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/users/create | jq | grep time
"time_appconnect": 0.050314,
"time_connect": 0.018364,
"time_namelookup": 0.005023,
"time_pretransfer": 0.050423,
"time_redirect": 0,
"time_starttransfer": 0.096338,
"time_total": 0.09646,
二回目は 0.09秒
Python
続いて、Python です。
プロジェクトの作成は以下のコマンドです。
$ sls create -t aws-python3
こちらも httpApi
を有効にして実行してみましょう。
% curl -s -o /dev/null -w '%{json}' https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/users/create | jq | grep time
"time_appconnect": 0.641517,
"time_connect": 0.246016,
"time_namelookup": 0.064964,
"time_pretransfer": 0.641973,
"time_redirect": 0,
"time_starttransfer": 1.173754,
"time_total": 1.174153,
やはり 1秒ちょっとですね。
curl -s -o /dev/null -w '%{json}' https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/users/create | jq | grep time
"time_appconnect": 0.539468,
"time_connect": 0.180288,
"time_namelookup": 0.004705,
"time_pretransfer": 0.539839,
"time_redirect": 0,
"time_starttransfer": 0.773942,
"time_total": 0.774261,
二回目でも 0.77 秒。
東京リージョンも試してみます。
$ curl -s -o /dev/null -w '%{json}' https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/users/create | jq | grep time
"time_appconnect": 0.093419,
"time_connect": 0.06388,
"time_namelookup": 0.033465,
"time_pretransfer": 0.093567,
"time_redirect": 0,
"time_starttransfer": 0.37505,
"time_total": 0.3757,
初回は 0.3 秒。早い。
$ curl -s -o /dev/null -w '%{json}' https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/users/create | jq | grep time
"time_appconnect": 0.047814,
"time_connect": 0.017771,
"time_namelookup": 0.005278,
"time_pretransfer": 0.047917,
"time_redirect": 0,
"time_starttransfer": 0.093568,
"time_total": 0.09372,
二回目で 0.09 秒。
node.js
引用した記事にもあった node.js を試します。
プロジェクトの作成は以下のコマンドです。
$ sls create -t aws-nodejs
httpApi
を有効にして実行してみます。
$ curl -s -o /dev/null -w '%{json}' https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/users/create | jq | grep time
"time_appconnect": 0.546401,
"time_connect": 0.197569,
"time_namelookup": 0.029535,
"time_pretransfer": 0.546648,
"time_redirect": 0,
"time_starttransfer": 1.131231,
"time_total": 1.131571,
初回 1.13 秒。
$ curl -s -o /dev/null -w '%{json}' https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/users/create | jq | grep time
"time_appconnect": 0.574078,
"time_connect": 0.190488,
"time_namelookup": 0.008435,
"time_pretransfer": 0.574598,
"time_redirect": 0,
"time_starttransfer": 0.789526,
"time_total": 0.78992,
二回目は 0.78 秒。
東京リージョンではどうでしょうか。
curl -s -o /dev/null -w '%{json}' https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/users/create | jq | grep time
"time_appconnect": 0.072397,
"time_connect": 0.039462,
"time_namelookup": 0.026409,
"time_pretransfer": 0.072512,
"time_redirect": 0,
"time_starttransfer": 0.339076,
"time_total": 0.33939,
初回 0.33 秒。
$ curl -s -o /dev/null -w '%{json}' https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/users/create | jq | grep time
"time_appconnect": 0.086989,
"time_connect": 0.029709,
"time_namelookup": 0.006374,
"time_pretransfer": 0.087115,
"time_redirect": 0,
"time_starttransfer": 0.156008,
"time_total": 0.156189,
二回目 0.15 秒。
C#
続いて C# です。
個人的には C# 使うなら Azure function の方が良いのでは?という気もしますが...
プロジェクトは以下のコマンドで作成します。
$ sls create -t aws-csharp
C# の場合は、そのままではデプロイできませんので、まずはプロジェクトのビルドをします。
dotnet がインストールされていない場合は、事前にインストールしておきましょう。
https://dotnet.microsoft.com/ja-jp/download/dotnet/thank-you/sdk-6.0.417-macos-arm64-installer
$ sh build.sh
成功すると、 serverless.yml に記載された package が生成されます。
ビルドしないと、ファイルが存在しないエラーが発生します。
package:
artifact: bin/Release/net6.0/hello.zip
ビルドしたらデプロイします。
$ curl -s -o /dev/null -w '%{json}' https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/users/create | jq | grep time
"time_appconnect": 0.578687,
"time_connect": 0.200703,
"time_namelookup": 0.026354,
"time_pretransfer": 0.579506,
"time_redirect": 0,
"time_starttransfer": 1.271791,
"time_total": 1.272151,
初回 1.2 秒
$ curl -s -o /dev/null -w '%{json}' https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/users/create | jq | grep time
"time_appconnect": 0.604584,
"time_connect": 0.193366,
"time_namelookup": 0.011806,
"time_pretransfer": 0.60504,
"time_redirect": 0,
"time_starttransfer": 0.830186,
"time_total": 0.830354,
二回目 0.83 秒。
そして東京リージョン。
$ curl -s -o /dev/null -w '%{json}' https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/users/create | jq | grep time
"time_appconnect": 0.059029,
"time_connect": 0.032678,
"time_namelookup": 0.023719,
"time_pretransfer": 0.059141,
"time_redirect": 0,
"time_starttransfer": 0.547818,
"time_total": 0.548133,
初回 0.54 秒。
$ curl -s -o /dev/null -w '%{json}' https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/users/create | jq | grep time
"time_appconnect": 0.040793,
"time_connect": 0.016125,
"time_namelookup": 0.006695,
"time_pretransfer": 0.040884,
"time_redirect": 0,
"time_starttransfer": 0.083323,
"time_total": 0.083668,
二回目 0.08 秒。
Java
Java も C# と同様に package の内容を手元でビルドしておく必要があります。
事前に mvn をインストールしておきましょう。
私は brew でインストールしました。
$ which mvn
/opt/homebrew/bin/mvn
$ mvn --version
Apache Maven 3.9.4 (dfbb324ad4a7c8fb0bf182e6d91b0ae20e3d2dd9)
Maven home: /opt/homebrew/Cellar/maven/3.9.4/libexec
Java version: 20.0.1, vendor: Homebrew, runtime: /opt/homebrew/Cellar/openjdk/20.0.1/libexec/openjdk.jdk/Contents/Home
Default locale: ja_JP, platform encoding: UTF-8
OS name: "mac os x", version: "14.1.1", arch: "aarch64", family: "mac"
プロジェクトの作成は以下です。
$ sls create -t aws-java-maven
ちなみに、テンプレートには aws-java-gradle
もあるので、 gradle build もできそうです。
プロジェクトを作成したら、ビルドします。
$ mvn clean package
ビルドが成功したら、デプロイしましょう。
$ curl -s -o /dev/null -w '%{json}' https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/users/create | jq | grep time
"time_appconnect": 0.570913,
"time_connect": 0.205865,
"time_namelookup": 0.029634,
"time_pretransfer": 0.571532,
"time_redirect": 0,
"time_starttransfer": 3.494312,
"time_total": 3.494494,
初回は 3.49 秒。
ここにきて、初めてちょっと遅いなと思いました。
$ curl -s -o /dev/null -w '%{json}' https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/users/create | jq | grep time
"time_appconnect": 0.531742,
"time_connect": 0.178458,
"time_namelookup": 0.004685,
"time_pretransfer": 0.531956,
"time_redirect": 0,
"time_starttransfer": 0.750374,
"time_total": 0.750526,
二回目は 0.75 秒。
これは他と大差ないですね。
続いて東京リージョン。
$ curl -s -o /dev/null -w '%{json}' https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/users/create | jq | grep time
"time_appconnect": 0.10044,
"time_connect": 0.061328,
"time_namelookup": 0.036287,
"time_pretransfer": 0.100537,
"time_redirect": 0,
"time_starttransfer": 2.870047,
"time_total": 2.870318,
初回が 2.87 秒。
$ curl -s -o /dev/null -w '%{json}' https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/users/create | jq | grep time
"time_appconnect": 0.048652,
"time_connect": 0.017969,
"time_namelookup": 0.005023,
"time_pretransfer": 0.048753,
"time_redirect": 0,
"time_starttransfer": 0.0993,
"time_total": 0.099881,
二回目が 0.09 秒。
GO
最後に GO を試します。
GO もビルドが必要になるので、事前にインストールしておきましょう。
$ which go
/opt/homebrew/bin/go
$ go version
go version go1.21.4 darwin/arm64
プロジェクトの作成は以下です。
$ sls create -t aws-go-mod
aws-go
というテンプレートもありますが、 go の Makefile などがないため、 aws-go-mod
の方が扱いやすそうでした。
必要なライブラリをインストールします。
$ go mod download github.com/aws/aws-lambda-go
$ make build
build に成功したらデプロイしてみましょう。
$ curl -s -o /dev/null -w '%{json}' https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/hello | jq | grep time
"time_appconnect": 0.591873,
"time_connect": 0.226685,
"time_namelookup": 0.046781,
"time_pretransfer": 0.592241,
"time_redirect": 0,
"time_starttransfer": 1.039621,
"time_total": 1.039899,
初回は 1.0 秒
$ curl -s -o /dev/null -w '%{json}' https://xxxxxxxx.execute-api.us-east-1.amazonaws.com/hello | jq | grep time
"time_appconnect": 0.574277,
"time_connect": 0.194989,
"time_namelookup": 0.008318,
"time_pretransfer": 0.575616,
"time_redirect": 0,
"time_starttransfer": 0.802837,
"time_total": 0.802973,
二回目は 0.80 秒。
そして東京リージョン。
$ curl -s -o /dev/null -w '%{json}' https://r6472bohg6.execute-api.ap-northeast-1.amazonaws.com/hello | jq | grep time
"time_appconnect": 0.082636,
"time_connect": 0.047037,
"time_namelookup": 0.034499,
"time_pretransfer": 0.082748,
"time_redirect": 0,
"time_starttransfer": 0.635991,
"time_total": 0.636371,
初回が 0.63 秒。
$ curl -s -o /dev/null -w '%{json}' https://r6472bohg6.execute-api.ap-northeast-1.amazonaws.com/hello | jq | grep time
"time_appconnect": 0.052491,
"time_connect": 0.022134,
"time_namelookup": 0.00557,
"time_pretransfer": 0.052592,
"time_redirect": 0,
"time_starttransfer": 0.102562,
"time_total": 0.102659,
二回目は 0.1 秒。
集計
ざっくりですが、まとめると以下のようになりました。
コールドスタートのテストということもあり、短時間に負荷をかけて平均を取れていないので、誤差はかなりありそうですが、参考にはなるかと思います。
- Java はちょっと遅い。
- 東京からのアクセスなので、東京リージョンで実行すると早い。
言語 | region | 初回 | 二回目 |
---|---|---|---|
Ruby | us-east-1 | 1.196165 | 0.819033 |
ap-northeast-1 | 0.552621 | 0.09646 | |
Python | us-east-1 | 1.174153 | 0.774261 |
ap-northeast-1 | 0.3757 | 0.09372 | |
node.js | us-east-1 | 1.131571 | 0.78992 |
ap-northeast-1 | 0.33939 | 0.156189 | |
C# | us-east-1 | 1.272151 | 0.830354 |
ap-northeast-1 | 0.548133 | 0.083668 | |
Java | us-east-1 | 3.494494 | 0.750526 |
ap-northeast-1 | 2.870318 | 0.099881 | |
Go | us-east-1 | 1.039899 | 0.802973 |
ap-northeast-1 | 0.636371 | 0.102659 |
遅くなる要因は何か?
元記事によれば、デプロイするパッケージの容量に依存する、という仮説がありましたので、 serverless がデプロイした各パッケージのファイルサイズを見てみます。
- ruby 411.0 B
- python 516.0 B
- nodejs 1.4 KB
- C# 30.2 KB
- Java 3.7 MB
- GO 3.8 MB
確かに、 Java のファイルサイズがでかいのは間違いないですが、よりファイルサイズの大きな GO はレスポンスが早かったので、一概にパッケージサイズのみが影響するとも言えなさそうですね。
さて、ここで当初の疑問に戻り、私はなぜ初回の実行に5秒もかかると考えていたのでしょうか。
というわけで、あの頃のあの API は遅かったよなーというものをチラッと叩いてみました。
既存の API を叩いてみました。
curl -s -o /dev/null -w '%{json}' https://{existing-project}.execute-api.ap-northeast-1.amazonaws.com/hoge/fuga | jq | grep time
"time_appconnect": 0.060997,
"time_connect": 0.042919,
"time_namelookup": 0.030044,
"time_pretransfer": 0.061088,
"time_redirect": 0,
"time_starttransfer": 2.049164,
"time_total": 2.049397,
初回で 2秒前後。
$ curl -s -o /dev/null -w '%{json}' https://{existing-project}.execute-api.ap-northeast-1.amazonaws.com/hoge/fuga | jq | grep time
"time_appconnect": 0.054843,
"time_connect": 0.026479,
"time_namelookup": 0.007485,
"time_pretransfer": 0.054934,
"time_redirect": 0,
"time_starttransfer": 0.182834,
"time_total": 0.183792,
二回目で 0.2秒前後。
5秒とまではいかないですが、まぁまぁ遅いですね。
二回目のレスポンスが早くなっていることから、内部処理が異常に遅いわけではないことが分かります。
Python だと、初期状態では 0.3 秒のものが、2秒程度まで遅くなる原因について、色々と検証してきたいと思いますが、ここから先は Python 固有の話がメインとなりますので、また別の記事にしたいと思います。
まとめ
- コールドスタートは確かに遅いが、初期状態であればさほど遅くはない。
- ただし、何もしていなくても 0.3 ~ 0.5 秒かかるので、応答速度を気にするアプリケーションは注意。
- コールドスタートに限り maven ビルドの Java はそれなりに気になる遅延がある。
- 既存の Java リソースがあるとか、Javaエンジニアしかいない、といったケースを除けば、 Lambda + Java は避けるのが無難かもしれません。
- リージョンもちゃんと設定しないと、もちろんかなりの遅延になる。