3
2

More than 1 year has passed since last update.

AWS Lambdaのコールドスタート対策:Provisioned Concurrencyの速度比較検証

Last updated at Posted at 2023-03-13

はじめに

 AWS Lambdaのコールドスタート対策として、Provisioned Concurrencyという機能があります。Provisioned Concurrencyを利用する上で、そのメリットを最大限活用するために、どのような実装をすべきかについて、実際に動かして速度を比較して検証してみました。

事前準備

 事前準備としてLambdaにサンプルプログラムをデプロイします。言語はC#で、実装としてはLambdaからDynamoDBを検索し、DynamoDBに登録されている内容を出力するだけのシンプルな内容となっています。実行結果はAWS X-rayに出力し、どの部分が遅いのかを確認できるようにしています。

using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.DataModel;
using Amazon.Lambda.Core;
using Amazon.XRay.Recorder.Handlers.AwsSdk;
using Microsoft.Extensions.DependencyInjection;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace TestColdStart;

public class Function
{
    private readonly ITestColdStartRepository _testColdStartRepository;

    public Function()
    {
        AWSSDKHandler.RegisterXRayForAllServices();

        var serviceCollection = new ServiceCollection();
        serviceCollection.AddAWSService<IAmazonDynamoDB>();
        serviceCollection.AddTransient<IDynamoDBContext, DynamoDBContext>();
        serviceCollection.AddTransient<ITestColdStartRepository, TestColdStartRepository>();

        var serviceProvider = serviceCollection.BuildServiceProvider();
        _testColdStartRepository = serviceProvider.GetService<ITestColdStartRepository>();
    }

    public async Task<string> FunctionHandler(object input, ILambdaContext context)
    {
        var data = await _testColdStartRepository.GetDataAsync("0000000001", "TEST").ConfigureAwait(false);
        return $"description:{data.Description}";
    }
}

public interface ITestColdStartRepository
{
    Task<TestColdStart> GetDataAsync(string id, string code);
}

public class TestColdStartRepository : ITestColdStartRepository
{
    private readonly IDynamoDBContext _dynamoDbContext;

    public TestColdStartRepository(IDynamoDBContext dynamoDbContext)
    {
        _dynamoDbContext = dynamoDbContext;
    }

    public async Task<TestColdStart> GetDataAsync(string id, string code)
    {
        var testColdStart = await _dynamoDbContext.LoadAsync<TestColdStart>(id, code).ConfigureAwait(false);
        return testColdStart;
    }
}

public class TestColdStart
{
    [DynamoDBHashKey]
    public string Id { get; set; }

    [DynamoDBRangeKey]
    public string Code { get; set; }

    public string Description { get; set; }
}

検証1:何も対策なし

 まずは速度対策について何も対策していない状態で速度を計測してみます。コールドスタート時とウォームスタート時のそれぞれで計測しました。

コールドスタート時の実行速度

Initialization DescribeTable GetItem Total Description
検証1 436ms 3420ms 419ms 5430ms 何も対策なし(コールドスタート)

DynamoDBのDescribeTableに3420msもかかっているのが一番のボトルネックであることがわかります。

image.png

ウォームスタート時の実行速度

Initialization DescribeTable GetItem Total Description
検証1 436ms 3420ms 419ms 5430ms 何も対策なし(コールドスタート)
ウォームスタート 0ms 0ms 19ms 31ms 何も対策なし(ウォームスタート)

コールドスタート時と比較して、InitializationとDynamoDBのDescribeTableが無くなっています。
ちなみに以降の検証で、ウォームスタート時については誤差レベルの変動しかなかったため、ウォームスタート時の記録については割愛しています。

image.png

検証2:メモリ増設

 1つ目の速度対策として、メモリ増設が挙げられます。今回は128MBのメモリを1024MBにして速度を比較してみました。

Initialization DescribeTable GetItem Total Description
検証1 436ms 3420ms 419ms 5430ms 何も対策なし(コールドスタート)
検証2 442ms 328ms 52ms 1050ms メモリ増設
ウォームスタート 0ms 0ms 19ms 31ms 何も対策なし(ウォームスタート)

メモリ増設前と比較して、DescribeTableの速度が3420ms→328msと大幅に改善しています。GetItemについても419ms→52msと全体的に高速に処理されていることがわかりました。

image.png

検証3:Provisioned Concurrency+メモリ増設

 続いて、Provisioned Concurrencyを有効にして検証してみます。メモリは1024MBのままです。

Initialization DescribeTable GetItem Total Description
検証1 436ms 3420ms 419ms 5430ms 何も対策なし(コールドスタート)
検証2 442ms 328ms 52ms 1050ms メモリ増設
検証3 0ms 323ms 47ms 459ms Provisioned Concurrency+メモリ増設
ウォームスタート 0ms 0ms 19ms 31ms 何も対策なし(ウォームスタート)

Provisioned Concurrency適用前と比較して、Initializationが無くなっているのがわかります。ですが、DescribeTableの処理は実施されているため、ウォームスタート時ほどは速くなりませんでした。

image.png

検証4:Provisioned Concurrency+メモリ増設+DescribeTableキャッシュ

 DescribeTableを無くすために調査した結果、コンストラクタでのキャッシュ作成が必要という結論に至りました。以下のリンクに記載がありますが、DynamoDB検索処理実行時にDescribeTableが呼ばれ、テーブルメタデータのキャッシュが作成され、次回以降の検索処理時にはキャッシュが利用されるようです。
 このため、コンストラクタでキャッシュを作成するために、検索処理を一度実行し、テーブルメタデータのキャッシュを作成するように修正して速度検証してみました。

Initialization DescribeTable GetItem Total Description
検証1 436ms 3420ms 419ms 5430ms 何も対策なし(コールドスタート)
検証2 442ms 328ms 52ms 1050ms メモリ増設
検証3 0ms 323ms 47ms 459ms Provisioned Concurrency+メモリ増設
検証4 0ms 0ms 37ms 90ms Provisioned Concurrency+メモリ増設+DescribeTableキャッシュ
ウォームスタート 0ms 0ms 19ms 31ms 何も対策なし(ウォームスタート)

狙い通り、DynamoDBのDescribeTable処理が無くなり、高速に処理できるようになりました。

image.png

検証5:Provisioned Concurrency+メモリ増設+DescribeTableキャッシュ+GetItemキャッシュ

 最後の検証はGetItemで取得するデータもキャッシュするパターンです。これはデータ次第だと思いますがある程度静的なデータであれば実現可能だと思います。DescribeTableでキャッシュされるテーブルメタデータはSDK内部でキャッシュされるようですが、取得したデータ自体は独自でキャッシュする必要があります。
 Provisioned Concurrencyで常駐しているインスタンスもCloudWatchLogsのログを見る限り、定期的(大体1~2時間おき)に再起動しているように見えます。※このあたりの情報ソースは見つかりませんでした。

Initialization DescribeTable GetItem Total Description
検証1 436ms 3420ms 419ms 5430ms 何も対策なし(コールドスタート)
検証2 442ms 328ms 52ms 1050ms メモリ増設
検証3 0ms 323ms 47ms 459ms Provisioned Concurrency+メモリ増設
検証4 0ms 0ms 37ms 90ms Provisioned Concurrency+メモリ増設+DescribeTableキャッシュ
検証5 0ms 0ms 0ms 50ms Provisioned Concurrency+メモリ増設+DescribeTableキャッシュ+GetItemキャッシュ
ウォームスタート 0ms 0ms 19ms 31ms 何も対策なし(ウォームスタート)

GetItem処理も無くなりました。ここまでくると当初のウォームスタートと殆ど変わらない速度となりました。

image.png

おわりに

 今回は、Lambdaのコールドスタートをどこまで対策できるかを実際に速度比較して検証してみました。言語がC#だったため、snap startは利用できませんでしたが、速度としては十分な結果を得ることができました。Provisioned Concurrencyは事前に初期処理をして待機しておく機能のため、利用する場合は、キャッシュ作成などを初期処理に含めておくと、メリットが得られるように思います。ただし、Provisioned Concurrencyはサーバーレスの大きなメリットであるコスト面を犠牲にするため、適用は十分検討をした上で行うのが良いかと思います。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2