パフォーマンスとコスト — コールドスタートと最適化
ECサイトのLambda移行から3ヶ月が経った。全体的には快調だったが、ある日ダイチは気になるグラフを発見した。
「この P99 レイテンシが時々500msを超えてる。何だろう?」
CloudWatch のメトリクスを掘り下げると、パターンが見えてきた。アクセスが少ない深夜から朝方に、たまにリクエストが来たときだけ遅い。これはコールドスタートだ。
コールドスタートの解剖
なぜコールドスタートが発生するか
コールドスタートは以下の3フェーズで構成される。
| フェーズ | 時間 | 制御可否 |
|---|---|---|
| コンテナ起動 | 50-200ms | 不可(AWSが管理) |
| ランタイムロード | 100-500ms | 言語選択で影響 |
| 初期化コード | 任意 | あなたが制御 |
Rubyはコールドスタートが比較的長い言語の一つだ(Go/Python より長く、Java/Node.jsと同程度)。
コールドスタートの計測
# CloudWatch Logs Insights でコールドスタートを計測
fields @timestamp, @message
| filter @message like /REPORT/
| parse @message "Duration: * ms Billed Duration: * ms Memory Size: * MB Max Memory Used: * MB Init Duration: * ms"
as duration, billedDuration, memorySize, maxMemoryUsed, initDuration
| filter ispresent(initDuration) # Init Duration があるのがコールドスタート
| stats
avg(initDuration) as avgInitDuration,
max(initDuration) as maxInitDuration,
count(*) as coldStartCountコールドスタート対策
1. 初期化コードの最適化
ハンドラ関数の外にある「初期化コード」はコールドスタート時のみ実行される。これを最小限にする。
# 悪い例: ハンドラ内で毎回接続を作る
def lambda_handler(event:, context:)
# 毎リクエストでクライアント生成(ウォームスタートでも毎回実行される)
dynamodb = Aws::DynamoDB::Client.new
s3 = Aws::S3::Client.new
result = dynamodb.get_item(...)
end
# 良い例: グローバルスコープで一度だけ初期化
# コールドスタート時のみ実行され、ウォームスタートでは再利用される
require 'aws-sdk-dynamodb'
require 'aws-sdk-s3'
$dynamodb = Aws::DynamoDB::Client.new
$s3 = Aws::S3::Client.new
def lambda_handler(event:, context:)
# $dynamodb, $s3 は既に初期化済み
result = $dynamodb.get_item(...)
end# 遅延初期化パターン(重い依存関係に有効)
$payment_client = nil
def payment_client
$payment_client ||= begin
secret = get_secret(ENV['PAYMENT_SECRET_ARN'])
PaymentSDK::Client.new(api_key: secret['api_key'])
end
end
def lambda_handler(event:, context:)
# payment_client が必要になった時点で初めて初期化
result = payment_client.charge(...)
end2. require の最適化
# 悪い例: 不要な gem を読み込む
require 'rails' # 絶対NG: Rails全体
require 'active_record' # DBが必要な関数のみ
require 'nokogiri' # HTMLパースが必要な関数のみ
# 良い例: 必要なものだけ require する
require 'json' # 標準ライブラリ
require 'logger' # 標準ライブラリ
require 'aws-sdk-dynamodb' # 使うサービスのみ(aws-sdk-allは使わない)# gem のサイズ確認
gem list --details | grep -E "^(aws-sdk-|rails|nokogiri)"
# Lambda デプロイパッケージのサイズ確認(50MB以下が目標)
sam build && du -sh .aws-sam/build/*/3. Provisioned Concurrency
コールドスタートを完全に排除したい場合は Provisioned Concurrency を使う。指定した数のコンテナを常時ウォーム状態で維持する。
Resources:
ProductsFunction:
Type: AWS::Serverless::Function
Properties:
AutoPublishAlias: live
ProvisionedConcurrencyConfig:
ProvisionedConcurrentExecutions: 5 # 5コンテナを常時ウォーム状態に
# スケジュールでプロビジョニング数を変動させる(コスト最適化)
ScaleUpSchedule:
Type: AWS::ApplicationAutoScaling::ScalableTarget
Properties:
MaxCapacity: 20
MinCapacity: 5
ResourceId: !Sub "function:${ProductsFunction}:live"
ScalableDimension: lambda:function:ProvisionedConcurrency
ServiceNamespace: lambda
ScheduledActions:
- ScheduledActionName: scale-up-for-sale
Schedule: "cron(0 9 * * ? *)" # 毎朝9時にスケールアップ
ScalableTargetAction:
MinCapacity: 20
MaxCapacity: 20
- ScheduledActionName: scale-down-after-sale
Schedule: "cron(0 22 * * ? *)" # 夜10時にスケールダウン
ScalableTargetAction:
MinCapacity: 5
MaxCapacity: 5INFO
Provisioned Concurrency は追加費用がかかる(GB-秒あたり$0.0000041667)。セール期間中だけ有効にするスケジューリングが費用対効果に優れる。通常時はウォームスタートでもP95が100ms以下なら不要。
メモリとコストのトレードオフ
LambdaのCPUはメモリに比例して割り当てられる。メモリを増やすと実行速度が上がり、結果的にコストが下がることがある。
コスト = (リクエスト数 × $0.0000002) + (実行時間 × メモリ(GB) × $0.0000166667)
例1: 128MB, 実行時間 500ms
コスト = (1,000,000 × $0.0000002) + (500/1000 × 0.125 × $0.0000166667 × 1,000,000)
= $0.20 + $1.04 = $1.24/月(100万リクエスト)
例2: 512MB, 実行時間 200ms(メモリ4倍でCPU増、実行速度2.5倍向上)
コスト = (1,000,000 × $0.0000002) + (200/1000 × 0.5 × $0.0000166667 × 1,000,000)
= $0.20 + $1.67 = $1.87/月
この例では128MBのほうが安いが、画像処理のような重い計算では逆転する。AWS Lambda Power Tuning というツールで最適なメモリサイズを自動探索できる。
# Lambda Power Tuning のデプロイと実行
# https://github.com/alexcasalboni/aws-lambda-power-tuning
npm install -g serverless
serverless deploy
# 自動チューニング実行(10回実行して最適値を探索)
aws stepfunctions start-execution \
--state-machine-arn "arn:aws:states:ap-northeast-1:xxx:stateMachine:powerTuningMachine" \
--input '{
"lambdaARN": "arn:aws:lambda:ap-northeast-1:xxx:function:ProductsFunction",
"powerValues": [128, 256, 512, 1024, 2048, 3008],
"num": 10,
"payload": {"path": "/products", "httpMethod": "GET"},
"parallelInvocation": true,
"strategy": "cost"
}'X-Ray によるトレーシング
パフォーマンスのボトルネックを特定するには、AWS X-Ray の分散トレーシングが有効だ。
Globals:
Function:
Tracing: Active # 全関数でX-Rayトレースを有効化# X-Ray SDK for Ruby
require 'aws-xray-sdk'
XRay.configure do |c|
c.daemon_address = '127.0.0.1:2000'
end
def lambda_handler(event:, context:)
# カスタムサブセグメントで詳細なトレース
XRay.recorder.capture('DynamoDB:GetProduct') do |seg|
seg.put_annotation('product_id', event.dig('pathParameters', 'id'))
result = $dynamodb.get_item(...)
seg.put_metadata('item_size', result.item&.to_json&.bytesize)
result
end
endX-Ray コンソールで確認できるサービスマップ:
この例では、S3 へのアクセスがボトルネック(120ms)だと分かる。
CloudWatch メトリクスとアラート
# 重要なメトリクスへのアラート設定
LambdaErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "${AWS::StackName}-lambda-errors"
MetricName: Errors
Namespace: AWS/Lambda
Dimensions:
- Name: FunctionName
Value: !Ref ProductsFunction
Statistic: Sum
Period: 60
EvaluationPeriods: 1
Threshold: 10
ComparisonOperator: GreaterThanThreshold
AlarmActions:
- !Ref AlertSnsTopic
LambdaDurationAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "${AWS::StackName}-lambda-duration"
MetricName: Duration
Namespace: AWS/Lambda
Dimensions:
- Name: FunctionName
Value: !Ref ProductsFunction
ExtendedStatistic: p99 # P99レイテンシ
Period: 300
EvaluationPeriods: 2
Threshold: 3000 # 3秒を超えたらアラート
ComparisonOperator: GreaterThanThresholdコスト管理の実践
AWS Cost Explorer での可視化
# CLIでLambdaのコストを確認
aws ce get-cost-and-usage \
--time-period Start=2024-01-01,End=2024-01-31 \
--granularity MONTHLY \
--filter '{"Dimensions":{"Key":"SERVICE","Values":["AWS Lambda"]}}' \
--metrics "UnblendedCost"コスト最適化のチェックリスト
| 最適化項目 | 期待効果 |
|---|---|
| 不要な CloudWatch Logs 保持期間を短縮 | ストレージコスト削減 |
| Lambda メモリを Power Tuning で最適化 | 実行コスト削減 |
| DynamoDB を PAY_PER_REQUEST から Provisioned に変更(高スループット時) | DynamoDB コスト削減 |
| S3 Intelligent-Tiering 有効化 | ストレージコスト削減 |
| CloudFront キャッシュで Lambda 呼び出し削減 | Lambda コスト削減 |
ダイチのECサイトでの最適化結果:
Lambda 最適化後の月次コスト(100万リクエスト想定):
- Lambda 実行費: ¥180
- API Gateway: ¥350
- DynamoDB: ¥800
- CloudFront: ¥500
- その他(SQS, SES等): ¥200
合計: 約 ¥2,030/月
Before(EC2ベース): 約 ¥30,000/月
削減率: 93%
「コストが93%削減されたのは驚きだったけど、それ以上に深夜の呼び出しがなくなったことが大きい」とダイチはチームへの報告でそう述べた。
WARNING
コスト削減の罠: トラフィックが一定量を超えると、Lambdaのコストはオンデマンドインスタンスを上回る場合がある。月1億リクエストを超えるような高トラフィックでは、Reserved Instancesを使ったECSやEC2のほうが安くなることも。常にコスト試算を行うこと。