AWS Parameters and Secrets Lambda Extensionのパフォーマンスへの影響を確認してみた

パフォーマンスや従量課金という観点では従来どおりライブラリや自前実装でキャッシュする方式にもメリットがあります
2022.10.21

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

CX事業本部@大阪の岩田です。

先日AWS Parameters and Secrets Lambda ExtensionというLambda Extensionが公開されました。

SSM Parameter StoreやSecrets Managerから取得した値のキャッシュをよしなにやってくれるのがこのExtensionのメリットです。これまでも各言語でこういったキャッシュの機構を提供するライブラリが公開されていたため、これらのライブラリを利用してキャッシュを実現していた方も多いのではないでしょうか?

今後はこれらのライブラリを利用せずにAWS Parameters and Secrets Lambda Extensionを利用する方向に倒していくべきなのでしょうか?現状ライブラリを利用してキャッシュを実現している場合、AWS Parameters and Secrets Lambda Extensionに移行すべきなのでしょうか?この辺りのメリット・デメリットがまだ自分の中で整理できていないのですが、AWS Parameters and Secrets Lambda Extensionのデメリットとしてパフォーマンスの観点から気になった点があるので、その点について検証していきたいと思います。

環境

今回利用した環境です。

  • AWS Parameters and Secrets Lambda ExtensionのARN: arn:aws:lambda:ap-northeast-1:133490724326:layer:AWS-Parameters-and-Secrets-Lambda-Extension:2
  • Lambdaのランタイム: Python3.9
  • requests: 2.28.1
  • aws-xray-sdk: 2.10.0

気になったこと

前述のブログを見て頂けると分かるのですが、AWS Parameters and Secrets Lambda ExtensionはLambda実行環境の中でHTTPサーバーのプロセスを起動し、このHTTPサーバーに対してGETリクエストを発行することでSSMやSecrets Managerの値を取得するというアーキテクチャです。Lambda実行環境内にリバースプロキシをデプロイするExtensionと考えてもらうとイメージが湧きやすいと思います。このアーキテクチャを知った時に以下のポイントが気になりました。

  • ループバックアドレスを利用しているとはいえHTTP通信を行う以上、メモリ上のキャッシュから値を取得するライブラリと比較するとレイテンシが悪化するのではないか?
  • Lambaの設定でメモリを潤沢に割り当てないようなケースだと、CPUパワーが十分に使えないことによる処理速度低下の影響が大きくなるのではないか?
    • 例えばLambdaからSecrets Managerに直接アクセスする場合、HTTPリクエストを発行してしまえば、レスポンスが返却されるまでの待ち時間はLambdaのメモリ設定とは無関係です。待ち時間が長い/短いはSecrets Manager側の処理速度に依存します。
    • 一方AWS Parameters and Secrets Lambda Extensionを使う場合、HTTPリクエストを受け付けた後にキャッシュの有無をチェックし、必要に応じてLambdaからSecrets Managerにアクセスしてレスポンスを返却するという一連の処理もLambda実行環境内で行われることになります。Lambdaのメモリ割当が少ないとCPUパワーが十分に使えないため、これらの処理にも時間がかかることになります。関数実行の処理時間全体に対して、CPUパワー不足の影響を受ける時間の割合が増えるわけです。シーケンスにすると以下のようなイメージです。

やってみる

ということで、実際AWS Parameters and Secrets Lambda Extensionを使う場合の各種の処理時間を確認してみました。以後検証用のコードでrequestsとaws-xray-sdkを利用していますが、これらのライブラリは別途Lambda Layerにデプロイされており、このLayerを使うようにLambdaが設定済みという前提です。

AWS Parameters and Secrets Lambda Extensionのキャッシュにヒットしない場合

まずAWS Parameters and Secrets Lambda Extensionのキャッシュにヒットせず、Secrets Managerへのアクセスが発生する場合の振る舞いを確認してみましょう。

メモリ128Mの場合

まずLambdaのメモリ割り当てを最低の128Mに設定して以下のコードを実行してみます

import json
import os

import requests
from aws_xray_sdk.core import patch_all

patch_all()


def lambda_handler(event, context):

    session = requests.Session()
    headers = {
        'X-Aws-Parameters-Secrets-Token': os.environ['AWS_SESSION_TOKEN']
    }
    res = session.get('http://localhost:2773/secretsmanager/get?secretId=lambda/extension/test', headers=headers)
    print(res.text)

    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "hello world",
        }),
    }

X-Rayのトレース結果を確認してみるとlocalhostへのGET処理に698msかかっていることが分かります。

メモリ1,769 Mの場合

今度はLambdaの設定でメモリの割当を1769Mまで増やしてから先程と同じコードを実行してみます。これでvCPU1つ分のフルパワーが使えるようになり、パフォーマンスが改善されるはずです。

先程698msかかっていた処理が119msまで高速化しました

メモリ128Mで直接Secrets Managerにアクセスする場合

AWS Parameters and Secrets Lambda Extensionを利用せずに直接Secrets Managerにアクセスする場合についても処理時間を確認しておきましょう。コードを以下のように書き換えてメモリの設定を128Mに戻した上で確認してみましょう。今度はシークレット取得のリクエスト発行完了〜レスポンス取得までの間Lambda実行環境は待ち状態になるので、CPUパワー不足の影響が小さくなると予想されます。

import json
import os

import requests
from aws_xray_sdk.core import patch_all

patch_all()


def lambda_handler(event, context):
		import boto3
    client = boto3.client('secretsmanager')
    res = client.get_secret_value(SecretId='lambda/extension/test')
    print(res)

    # session = requests.Session()
    # headers = {
    #     'X-Aws-Parameters-Secrets-Token': os.environ['AWS_SESSION_TOKEN']
    # }
    # res = session.get('http://localhost:2773/secretsmanager/get?secretId=lambda/extension/test', headers=headers)
    # print(res.text)

    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "hello world",
        }),
    }

X-Rayのトレース結果です

Secrets Managerから値を取得する処理は358msで完了しており、先程のAWS Parameters and Secrets Lambda Extension経由で値を取得する場合の698msと比べて約半分の速度で完了しています

メモリ1769Mで直接Secrets Managerにアクセスする場合

メモリの割当を1769Mに増やしてから再度試してみましょう。CPUパワーが強化されますが、それが活かされるのはTCPの3ウェイハンドシェイクやTLSのネゴシエーション処理程度になります。AWS Parameters and Secrets Lambda Extensionを利用する場合のようにHTTPサーバーとしての処理が高速化するといったメリットはありません。

X-Rayのトレース結果です

Secrets Managerから値を取得する処理は83.4msで完了しています。

一旦ここまでの計測結果のまとめです。

アクセス方法 Lambdaメモリ割り当て Secret取得処理の所要時間 メモリ増による改善効果
Extension経由 128M 698ms -
Extension経由 1769M 119ms 579ms
Secrets Manager直接 128M 358ms -
Secrets Manager直接 1769M 83.4ms 274.6ms

本来はもっと計測回数を重ねるべきですが、大まかな傾向は分かって頂けたのではないでしょうか?AWS Parameters and Secrets Lambda Extensionを利用する場合は以下のように考えて良さそうです。

  • キャッシュミス時の処理速度が長くなりがち
  • CPUパワー不足の処理速度への影響が大きくなりがち

これらはAWS Parameters and Secrets Lambda Extensionを利用する場合はLambda実行環境内で稼働する別プロセスとのHTTP通信が発生するというアーキテクチャによるものでしょう。Secrets Managerから返却されたレスポンスをメモリ上にキャッシュするライブラリであればここまで影響は大きくならないと予想します。

AWS Parameters and Secrets Lambda Extensionのキャッシュにヒットする場合

キャッシュにヒットしない場合のパフォーマンスについてざっくり確認できたので、今度はキャッシュにヒットする場合のパフォーマンスを確認してみましょう。以下のコードを実行してキャッシュからSecretsを取得する処理を100回繰り返し、出力されたログを集計してみます。

import json
import os
import time

import requests
from aws_xray_sdk.core import patch_all

patch_all()


def lambda_handler(event, context):


    # 最初にSecretを取得してキャッシュに乗せておく
    # 初期処理の中でExtensionにアクセスすると not ready to serve traffic, please wait の400エラーになるのでhandler内で処理
    headers = {
        'X-Aws-Parameters-Secrets-Token': os.environ['AWS_SESSION_TOKEN']
    }
    session = requests.Session()
    res = session.get('http://localhost:2773/secretsmanager/get?secretId=lambda/extension/test', headers=headers)
    assert  res.status_code == 200
    
    for i in range(100):
        start = time.perf_counter()
        res = session.get('http://localhost:2773/secretsmanager/get?secretId=lambda/extension/test', headers=headers)
        end = time.perf_counter()
        assert  res.status_code == 200
        print((end - start) * 1000)

    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "hello world",
        }),
    }

計測結果は以下のようになりました

メモリ割り当て 最小値 最大値 平均 中央値 90%タイル
128M 19.51988 79.063083 36.67532175 39.947521 40.3995089
1769M 1.480528 5.532579 1.74424592 1.6221905 2.0196445

メモリ割当1769Mの場合は平均1.7ミリ秒でキャッシュが取得できるのに対して128Mの場合は平均36ミリ秒もかかってしまいます。ここは注意しておいた方が良さそうですね。もっとも数十ミリ秒レベルのパフォーマンスを気にするならLambdaのメモリ割り当てをガッツリ増やすなり、ECSやEC2を検討すべきなのかもしれないですが。

自前で実装したキャッシュロジックを使ってみる

AWS Parameters and Secrets Lambda Extensionでキャッシュにヒットした場合のレスポンスが確認できたので、最後に自前でキャッシュを管理するクラスを作ってレスポンスを比較してみましょう。TTL等複雑な要件は無視して、以下のようなキャッシュ付きのクラスを用意します。

class MyCacheClass:

    _secrets = {}

    @classmethod
    def get_secret(cls, secret_id):
        
        if secret_id in cls._secrets:
            return cls._secrets[secret_id]
        # 本当はboto3のクライアントクラスを使いまわした方が良いが、その辺りの実装は割愛
        client = boto3.client('secretsmanager')
        res = client.get_secret_value(SecretId=secret_id)
        cls._secrets[secret_id] = res
        return cls._secrets[secret_id]

このキャッシュクラスを使い、キャッシュにヒットした場合のレスポンスを以下のコードで計測してみます

import json
import os
import time

import requests
from aws_xray_sdk.core import patch_all
import boto3

patch_all()


class MyCacheClass:

    _secrets = {}

    @classmethod
    def get_secret(cls, secret_id):
        
        if secret_id in cls._secrets:
            return cls._secrets[secret_id]
        client = boto3.client('secretsmanager')
        res = client.get_secret_value(SecretId=secret_id)
        cls._secrets[secret_id] = res
        return cls._secrets[secret_id]

def lambda_handler(event, context):

    # キャッシュに乗せる 
    MyCacheClass.get_secret('lambda/extension/test')
    
    for i in range(100):
        start = time.perf_counter()
        MyCacheClass.get_secret('lambda/extension/test')
        end = time.perf_counter()
        print((end - start) * 1000)   

    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "hello world",
        }),
    }

計測結果です

メモリ割り当て 最小値 最大値 平均 中央値 90%タイル
128M 0.000399 0.003649 0.00046043 0.00041 0.0004265
1769M 0.000397 0.004128 0.00045728 0.0004125 0.000422

さすがにメモリにキャッシュすると早いですね。最大値でも1桁マイクロ秒レベルでSecretが取得できています。実行している処理も対した処理では無いので、メモリ割当128Mと1769Mでの差も微々たるレベルとなりました。

まとめ

パフォーマンス観点からAWS Parameters and Secrets Lambda Extensionについて確認してみました。パフォーマンスだけを気にするのであれば従来通り自前実装やライブラリを使ってメモリ上にキャッシュするのが一番良さそうですね。とはいえ前提として「シビアにパフォーマンスを求めるシステムでLambdaを使うべきなのか?」という検討から必要ですし、パフォーマンス以外の観点でのメリット・デメリットについても考慮が必要です。

思考停止的に「これからは全部AWS Parameters and Secrets Lambda Extensionを利用しよう!」と考えるのではなく、あくまでキャッシュ管理の選択肢の1つに加え、実際のキャッシュ管理方針は諸々の要件に合わせて選択していければと思います。AWS Parameters and Secrets Lambda Extensionを否定したかったわけではありません。