こんにちは。
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

----------2023-11-21-1.34.38

遅くなる要因は何か?

元記事によれば、デプロイするパッケージの容量に依存する、という仮説がありましたので、 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 は避けるのが無難かもしれません。
  • リージョンもちゃんと設定しないと、もちろんかなりの遅延になる。