こんにちは、ディベロップメントサービス1課の山本です。
もう12月で、1年過ぎるのがとても早いですね。
今回は、少し早いクリスマスプレゼントとして、
AWS Lambda(以下、Lambda)を1円でも安くする方法を説明します。
AWS Lambda Power Tuningというツールを使って、Lambdaの割り当てメモリ量の最適化を行います。
この記事の対象者は?
この記事は以下の方々に向けて書かれています:
- Lambdaのコストを1円でも下げたい方々
- Lambdaの割り当てメモリを何となく決めている方々
- Lambdaの基本的なコストの計算方法を知りたい方々
Lambdaのコスト
まず、おさらいです。
Lambdaのコストは下記の表に従って決まります。 ※東京リージョン x86のみ抜粋
アーキテクチャ | 期間 | リクエスト |
---|---|---|
x86 | ||
最初の60億GB秒/月 | GB-秒あたり 0.0000166667USD | リクエスト 100 万件あたり 0.20USD |
次の 90億GB秒/月 | GB-秒あたり 0.000015USD | リクエスト 100 万件あたり 0.20USD |
150億GB秒/月 以上 | GB-秒あたり 0.0000133334USD | リクエスト 100 万件あたり 0.20USD |
リクエストは、0.20USD/100万件ということもあり、コストに大きな影響は与えません。
大事なのは、期間の方です。
GB-秒あたりとは、Lambdaが、メモリ1GBで1秒間動作する単位のことを言います。
そのため、期間の価格は下記の計算式で出ます。
期間の価格 = 割り当てメモリ量(GB) × 月稼働時間(秒) × GB-秒あたりの単価
結果、Lambdaのコストが増加する要因としては、下記の二つになります。
- 割り当てメモリ量(GB)が大きくなる
- 稼働時間(秒)が長くなる
なら、コストを下げるには、割り当てメモリ量を限界まで下げればいいんだ!
それは、違います。
LambdaのメモリとCPUの関係
Lambdaでは、割り当てメモリ量が増えると、それに比例して、利用できるCPUリソースも増加する特徴があります。
先ほどの価格ページから引用します。
AWS Lambda のリソースモデルでは、お客様が関数に必要なメモリ量を指定すると、それに比例した CPU パワーとその他のリソースが割り当てられます。メモリサイズが増えると、関数で利用可能な CPU にも同等の増加が発生します。
簡単にいうと、メモリ量を半分に減らすと、処理性能も半分に減ります。
なので、メモリ量を限界まで下げると、今度は稼働時間が増加して、コストが増加する可能性があります。
逆に、メモリ量を上げ過ぎても、処理速度には限界があるので、コストは増加します。
割り当てメモリ量(GB) × 月稼働時間(秒)
が最小
となる割り当てメモリ量を見つけることがコスト最適化の鍵です。
AWS Lambda Power Tuning
概要
割り当てメモリ量(GB) × 月稼働時間(秒)
が最小となるメモリ量を探してくれるツールがAWS Lambda Power Tuningとなります。
仕組みは簡単です。
作成したLambda関数に対して、複数パターンでメモリ量を割り当てて、その稼働時間を計測するツールです。
その後、計測した時間を可視化して、一番低いコストとなるパラメータを見つけてくれます。
以下、可視化される際のサンプルです。
構成図
AWS Lambda Power Tuningの構成図は下記の通りです。
デプロイ方法
まず、AWS Lambda Power Tuning
を自身のAWSアカウントにデプロイします。
デプロイ方法は、下記Githubに格納されてます。
aws-lambda-power-tuning/README-DEPLOY.md at master · alexcasalboni/aws-lambda-power-tuning · GitHub
いくつか手段はあるのですが、今回は一番簡単なAWS Serverless Application Repository (SAR)を利用する方法でデプロイします。
Option 1: AWS Serverless Application Repositoryの Serverless Application Repository
をクリックして、SARへと遷移します。
Deploy
をクリックすると、ログインしているAWSコンソールのLambdaのアプリケーション画面へと遷移します。
アプリケーションの設定項目から、いくつかのツールの設定が可能です。
特にこだわりがなければ、全てデフォルト値で利用可能です。
項目 | デフォルト値 | 説明 |
---|---|---|
アプリケーション名 | aws-lambda-power-tuning | スタック名 |
PowerValues | 128,256,512,1024,1536,3008 | テストする割り当てメモリ量 |
lambdaResource | * | 呼び出しを許可するLambdaリソース。ARN指定やプレフィックス指定が可能。 |
layerSdkName | ツールで利用するSDKレイヤー。 | |
logGroupRetentionInDays | 7 | ログ保持日数 |
payloadS3Bucket | 大きいペイロードを利用する際の、取得先バケット。 | |
payloadS3Key | * | 上記バケットのオブジェクトキー。 |
permissionsBoundary | ||
securityGroupIds | ツールに割り当てるセキュリティグループID(VPC内のLambdaテスト時のみ必要) | |
subnetIds | ツールに割り当てるセキュリティグループID(VPC内のLambdaテスト時のみ必要) | |
totalExecutionTimeout | 300 | テスト実行時のタイムアウト時間 |
visualizationURL | https://lambda-power-tuning.show/ | 可視化する際のURL |
設定項目の変更を終えたら、デプロイ
をクリックします。
CloudFormationのスタックが生成され、各種LambdaとStep Functionsのリソースが構築されます。
テスト実行方法
Step Functionsのステートマシンから、powerTuningStateMachine
を選択します。
実行を開始
をクリックします。
設定値入力画面に遷移するので、設定値を入力後に、実行を開始
を選択します。
パラメータ | 説明 |
---|---|
lambdaARN | テスト対象のLambdaARN |
powerValues | テストするメモリ量 |
num | 並列テスト数 |
payload | eventデータの内容 |
他にも入力できる項目はありますので、詳細知りたい方は、READMEを参照ください。
結果確認
実行ステータスが成功
に変わると、テスト完了です。
実行の入力と出力
タブに切り替えると、出力に以下の内容が記載されます。
{ "power": 1024, "cost": 5.376e-7, "duration": 31.166666666666668, "stateMachine": { "executionCost": 0.0003, "lambdaCost": 0.000039679500000000005, "visualization": "https://lambda-power-tuning.show/*******" } }
パラメータ | 説明 |
---|---|
power | 最適なメモリ量 |
cost | Lambda1コールの費用 |
duration | 稼働時間 |
stateMachine.executionCost | テストにかかった費用 |
stateMachine.lambdaCost | テストにかかったLambdaの費用 |
stateMachine.visualization | 可視化用のURL |
可視化用のURLに遷移すると、メモリ量 vs 稼働時間、 メモリ量 vs コストがグラフ化されてます。
この内容を見ることで、最適なコストとなるメモリ量が1024MB
となることが分かります。
検証
せっかくなので、特性の異なる2つのLambdaを作成して、それぞれ最適となるメモリ量を検証してみます。
- サンプル1(計算負荷が高い)
- サンプル2(通信負荷が高い)
サンプル1(計算負荷が高い)
まず、サンプルとして計算負荷が高いLambdaを作成します。
とりあえず、X番目のフィボナッチ行列を導出させます。
def fibonacci(n: int): if n <= 0: return 0 elif n == 1: return 1 else: return fibonacci(n-1) + fibonacci(n-2) def lambda_handler(event, context): input_number = event.get('input_number', 30) fib_result = fibonacci(input_number) return fib_result
直感だと、CPUの価値が高まるので、ある程度高いメモリでコストが最適化されるイメージです。
結果は以下の通りです。
想像通り、パラメータ(計算数)が増えるにつれて、最適メモリ量も増加しました。
パラメータ | 最適メモリ量(MB) |
---|---|
10 | 128 |
20 | 256 |
30 | 1024 |
サンプル2(通信負荷が高い)
次に、サンプルとして通信負荷が高いLambdaを作成します。
DynamoDBにX回Get Itemを行い、通信回数を多くして、負荷を上げます。
import boto3 def get_item_dynamodb(table_name, key): dynamodb = boto3.resource('dynamodb') table = dynamodb.Table(table_name) response = table.get_item(Key=key) return response['Item'] def lambda_handler(event, context): count = event.get('count', 1) item_list = [] for i in range(count): item = get_item_dynamodb('sample_table', {'Key': 'sample'}) item_list.append(item) return { 'statusCode': 200, 'body': item_list }
直感だと、待ち時間が大半なので、低いメモリでコストが最適化されるイメージです。
結果は以下の通りです。想像通りですね。
パラメータ | 最適メモリ量(MB) |
---|---|
10 | 256 |
50 | 256 |
100 | 256 |
さいごに
少し早い、クリスマスプレゼントいかがでしたでしょうか。
Lambdaのメモリ量を適切にチューニングすることで、コスト削減/応答速度の向上が見込めます。 特に、無駄に大きいメモリを割り当てて、性能限界に達している場合は効果絶大です。
本ブログがどなかたのお役に立てれば幸いです。