APIGateway + Lambdaにserverless-expressを採用することでコールドスタートヒットが減るかもしれない話
APIGateway + Lambdaにserverless-expressを採用すると、複数のURLパス+HTTPメソッドの組み合わせを1つのLambda関数にまとめることができます。
これによりAPIコール時のLambdaコールドスタートのヒット率が減るのではないかと思い、簡単にではありますが検証してみました。
サンプルとして次のようなラーメンの具を取得するAPIを作って試していきます。
GET /ramen-gu/jiro # 二郎系ラーメンの具を返す GET /ramen-gu/iekei # 家系ラーメンの具を返す
環境
- macOS Monterey
- node 16.15.0
- typescript 4.7.4
- aws-cdk-lib 2.46.0
- constructs 10.1.43
- source-map-support 0.5.21
- moment 2.29.4
- lodash 4.17.21
- @vendia/serverless-express 4.10.1
- express 4.18.2
serverless-expressを使わない場合
比較のためにserverless-expressを使う場合、使わない場合両方のAPIを作ります。
まず使わない場合で、次のCDKコードを用意します:
// lib/apigateway-stack.ts import { Construct } from "constructs" import * as cdk from "aws-cdk-lib" export class ApigatewayStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props) // APIGW Lambda関数 const jiroFn = new cdk.aws_lambda_nodejs.NodejsFunction(this, "jiroFn", { runtime: cdk.aws_lambda.Runtime.NODEJS_16_X, entry: "src/lambda/jiro-handler.ts", }) const iekeiFn = new cdk.aws_lambda_nodejs.NodejsFunction(this, "iekeiFn", { runtime: cdk.aws_lambda.Runtime.NODEJS_16_X, entry: "src/lambda/iekei-handler.ts", }) // APIGW const api = new cdk.aws_apigateway.RestApi(this, "api", { deployOptions: { tracingEnabled: true, stageName: "api", }, }) const ramenGuResource = api.root.addResource("ramen-gu") ramenGuResource .addResource("jiro") .addMethod("GET", new cdk.aws_apigateway.LambdaIntegration(jiroFn)) ramenGuResource .addResource("iekei") .addMethod("GET", new cdk.aws_apigateway.LambdaIntegration(iekeiFn)) } }
Lambda関数のコード:
// src/lambda/jiro-handler.ts import { APIGatewayProxyResult } from "aws-lambda" import "source-map-support/register" import "lodash" import "moment" export const handler = async ( event: APIGatewayProxyResult ): Promise<APIGatewayProxyResult> => { return { statusCode: 200, body: JSON.stringify({ message: "もやしキャベツ" }), } }
// src/lambda/iekei-handler.ts import { APIGatewayProxyResult } from "aws-lambda" import "source-map-support/register" import "lodash" import "moment" export const handler = async ( event: APIGatewayProxyResult ): Promise<APIGatewayProxyResult> => { return { statusCode: 200, body: JSON.stringify({ message: "ほうれん草" }), } }
実戦的なバンドルサイズに近づけるため、著名かつサイズが大きいlodashとmomentをimportしています。
serverless-expressを使う場合
続いてserverless-expressを使う場合です。CDKコード:
import { Construct } from "constructs" import * as cdk from "aws-cdk-lib" export class ApigatewayStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props) // APIGW Lambda関数 const ramenGuFn = new cdk.aws_lambda_nodejs.NodejsFunction( this, "ramenGuFn", { runtime: cdk.aws_lambda.Runtime.NODEJS_16_X, entry: "src/lambda/ramen-gu-handler.ts", } ) // APIGW const api = new cdk.aws_apigateway.RestApi(this, "api", { deployOptions: { tracingEnabled: true, stageName: "api", }, }) api.root.addProxy({ defaultIntegration: new cdk.aws_apigateway.LambdaIntegration(ramenGuFn), }) } }
Lambda関数コード:
// src/lambda/ramen-gu-handler.ts import "source-map-support/register" import "lodash" import "moment" import serverlessExpress from "@vendia/serverless-express" import express from "express" const app = express() app.use(express.json()) app.get("/ramen-gu/jiro", (_, res) => { res.status(200).send({ message: "もやしキャベツ", }) }) app.get("/ramen-gu/iekei", (_, res) => { res.status(200).send({ message: "ほうれん草", }) }) export const handler = serverlessExpress({ app })
確認手順
手元で以下のcurlコマンドを連続で実行し、要した時間を計測します。
/usr/bin/time -l curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/ramen-gu/jiro /usr/bin/time -l curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/ramen-gu/jiro /usr/bin/time -l curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/ramen-gu/jiro /usr/bin/time -l curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/ramen-gu/iekei /usr/bin/time -l curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/ramen-gu/iekei /usr/bin/time -l curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/ramen-gu/iekei
結果
レスポンス完了までに要した時間と、出力されたJSコードのバンドルサイズが以下です。
serverless-expressを使わなかったAPI
API | リクエスト回数 | 時間 |
---|---|---|
/api/ramen-gu/jiro | 1回目 | 380ms |
/api/ramen-gu/jiro | 2回目 | 25.0ms |
/api/ramen-gu/jiro | 3回目 | 14.0ms |
/api/ramen-gu/iekei | 1回目 | 392ms |
/api/ramen-gu/iekei | 2回目 | 26.0ms |
/api/ramen-gu/iekei | 3回目 | 36.0ms |
serverless-expressを使ったAPI
API | リクエスト回数 | 時間 |
---|---|---|
/api/ramen-gu/jiro | 1回目 | 598ms |
/api/ramen-gu/jiro | 2回目 | 29.0ms |
/api/ramen-gu/jiro | 3回目 | 30.0ms |
/api/ramen-gu/iekei | 1回目 | 34.0ms |
/api/ramen-gu/iekei | 2回目 | 21.0ms |
/api/ramen-gu/iekei | 3回目 | 31.0ms |
バンドルサイズ比較
ケース | サイズ |
---|---|
serverless-expressを使わなかった場合 | 437.2kb |
serverless-expressを使った場合 | 2.1mb |
まとめ
serverless-expressを使わなかったAPIでは、初回リクエストとコールするURLパスが変わったタイミングで約380ms要し、コールドスタートを引いていると思われます。
一方serverless-expressを使ったAPIでは、初回リクエストのみコールドスタートを引いたと思われる598msですが、URLパスが変わった際は34msであり、ウォームスタートを維持できているように見えます。したがって、serverless-expressを使うことでコールドスタートを引く回数が減少する可能性はあると思われます。
ただ、コールドスタート発生時は、バンドルサイズの増大が響いたのか後者のほうが時間がかかる結果となりました。そのため、手放しで後者の方がパフォーマンスが良いかというと微妙かもしれません。実戦ではコード量も多く依存ライブラリの数も増えるわけですが、それによりこの差が縮まるかもしれませんし、逆に広がるかもしれません。
以上参考になれば幸いです。