Amazon Web Services ブログ

AWS Lambda レスポンスストリーミングの紹介

本記事は、Introducing AWS Lambda response streaming の翻訳です。

本日 (2023年4月7日)、AWS Lambda はレスポンスペイロードストリーミングのサポートを発表しました。レスポンスストリーミングは、関数がレスポンスペイロードを段階的にクライアントに返すことができる新しい呼び出しパターンです。

Lambda のレスポンスペイロードストリーミングを使用すると、レスポンスデータが利用可能になった時点で呼び出し元にデータを送信することができます。これにより、Web アプリケーションやモバイルアプリケーションのパフォーマンスを向上させることができます。また、レスポンスストリーミングによって、より大きなペイロードを返す関数や、進捗状況を段階的に報告しながら長時間の処理を実行する関数を構築することも可能です。

従来のリクエスト・レスポンスモデルでは、完全なレスポンスを生成してバッファリングしてからクライアントに返す必要がありました。これは、クライアントにレスポンスの生成を待たせ、Time to First Byte (TTFB) のパフォーマンスを悪化させる可能性があります。Web アプリケーションは、TTFB とページロードパフォーマンスに特に敏感です。レスポンスストリーミングでは、準備が整った時点で部分的なレスポンスをクライアントに送り返し、TTFB レイテンシーをミリ秒以内に改善することができます。Web アプリケーションにおいてこれは、訪問者の体験と検索エンジンのランキングを向上させることができます。

その他のアプリケーションでは、画像、動画、文書、データベースの結果など、大きなペイロードを持つことがあります。レスポンスストリーミングを使用すると、ペイロード全体をメモリにバッファリングすることなく、これらのペイロードをクライアントに転送することができます。レスポンスストリーミングでは、Lambda の 6 MB の制限より大きい、 最大 20 MB までのレスポンスペイロードを送信することができます。これはソフトリミットです。

レスポンスストリーミングは現在、Node.js 14.x 以降のマネージドランタイムをサポートしています。また、カスタムランタイムを使用してレスポンスストリーミングを実装することも可能です。レスポンスペイロードは、AWS SDK や Lambda invoke API、Amazon CloudFront オリジンとして利用する場合を含む Lambda 関数 URL などを通じて段階的ににストリーミングすることができます。Amazon API GatewayApplication Load Balancer を使用してレスポンスペイロードをストリーミングすることはできませんが、API Gateway ではより大きなペイロードを返す機能を使用することができます。

レスポンスストリーミングを有効化した関数を作成する

レスポンスストリーミングに対応した関数ハンドラーの書き方は、典型的な Node.js ハンドラーのパターンと異なります。Lambda 関数のレスポンスをストリーミングするようにランタイムに指示するには、関数ハンドラーを streamifyResponse() デコレーターでラップする必要があります。これにより、ランタイムに正しいストリームロジックパスを使用するように指示し、関数がレスポンスをストリーミングできるようになります。

以下は、レスポンスストリーミングを有効にした場合のハンドラーの例です。

exports.handler = awslambda.streamifyResponse(
    async (event, responseStream, context) => {
        responseStream.setContentType(“text/plain”);
        responseStream.write(“Hello, world!”);
        responseStream.end();
    }
);

streamifyResponse デコレーターは、デフォルトのハンドラーパラメータである event, context に加えて、追加のパラメータ responseStream を受け付けます。

新しい responseStream オブジェクトは、関数がデータを書き込むことができるストリームオブジェクトを提供します。このストリームに書き込まれたデータは、即座にクライアントに送信されます。オプションで、レスポンスの Content-Type ヘッダーを設定して、ストリームの内容に関する追加のメタデータをクライアントに渡すこともできます。

レスポンスストリームへの書き込み

responseStream オブジェクトは、Node の Writable Stream API を実装しています。これは、ストリームに情報を書き込むための write() メソッドを提供します。しかし、ストリームへの書き込みには可能な限り pipeline() を使用することをお勧めします。これにより、より流れの速い Readable ストリームが Writable ストリームに負荷をかけることがなくなり、パフォーマンスを向上させることができます。

以下は、pipeline() を使用して圧縮されたデータをストリーミングする関数のサンプルです。

const pipeline = require("util").promisify(require("stream").pipeline);
const zlib = require('zlib');
const { Readable } = require('stream');

exports.gzip = awslambda.streamifyResponse(async (event, responseStream, _context) => {
    // As an example, convert event to a readable stream.
    const requestStream = Readable.from(Buffer.from(JSON.stringify(event)));
    
    await pipeline(requestStream, zlib.createGzip(), responseStream);
});

レスポンスストリームの終了

write() メソッドを使用する場合、ハンドラーが return する前にストリームを終了させる必要があります。responseStream.end() を使用して、ストリームにこれ以上データを書き込まないことを通知してください。pipeline() を使用してストリームに書き込む場合は、これは不要です。

ストリームで送信されたレスポンスを読み取る

レスポンスストリーミングでは、新たに InvokeWithResponseStream API が導入されています。ストリームによって送信されたレスポンスは、Lambda 関数 URL 経由か、AWS SDK を使って新しい API を直接呼び出すことで読み取ることができます。

API Gateway も Application Load Balancer の Lambda ターゲット統合も、チャンク転送エンコーディングをサポートしていません。そのため、ストリーミングレスポンスでも高速な TTFB を達成することはできません。しかし、API Gateway でレスポンスストリーミングを使用することで、API Gateway の上限である 10 MB まで返せるレスポンスのペイロードサイズを拡張することはできます。これを実装するには、LAMBDA_PROXY 統合を使用する代わりに、API Gateway と Lambda 関数 URL の間を HTTP_PROXY 統合によって構成する必要があります。

また、関数 URL をオリジンとした CloudFront を設定することも可能です。関数 URL と CloudFront を経由してレスポンスをストリーミングする場合、より高速な TTFB パフォーマンスを得られるだけでなく、より大きなペイロードサイズを返すことができます。

関数 URL で Lambda レスポンスストリーミングを使用する

関数 URL を設定して関数を起動し、チャンク転送エンコーディングによって HTTP クライアントに生データをストリームで返すことができます。関数 URL の呼び出しモードをデフォルトの BUFFERED から RESPONSE_STREAM に変更することで、新しい InvokeWithResponseStream API を使用するように関数 URL を設定することができます。

RESPONSE_STREAM にすることで、streamifyResponse() デコレーターで関数をラップすると、ペイロードの結果が利用可能になった段階でデータをストリーミングします。Lambda は、InvokeWithResponseStream API を使用して関数を呼び出します。streamifyResponse() でラップされていない関数を InvokeWithResponseStream によって呼び出した場合、Lambda はレスポンスをストリーミングせずに、バッファされたレスポンスを返します。これは、最大 6 MB のサイズ制限を受けます。

AWS Serverless Application Model (AWS SAM) または AWS CloudFormation を使用して、InvokeMode プロパティを設定します。

MyFunctionUrl:
  Type: AWS::Lambda::Url
  Properties:
    TargetFunctionArn: !Ref StreamingFunction
    AuthType: AWS_IAM
    InvokeMode: RESPONSE_STREAM

汎用 HTTP クライアントライブラリで 関数 URL を使用する場合

HTTP リクエストの形成やストリームレスポンスのパースには、言語やフレームワークによってさまざまなメソッドが使用されます。HTTP クライアントライブラリの中には、サーバーが接続を閉じた後にのみレスポンスボディを返すものもあります。これらのクライアントは、レスポンスストリームを返す関数では動作しません。レスポンスストリームの利点を得るためには、レスポンスデータをインクリメンタルに返す HTTP クライアントを使用してください。Java の Apache HttpClient、Node の組み込み http クライアント、Python の requests および urllib3 パッケージなどをはじめ、多くの HTTP クライアントライブラリがストリーミングレスポンスをサポート済みです。皆さんが使用している HTTP ライブラリのドキュメントも確認してみてください。

サンプルアプリケーション

Serverless Patterns Collection には、多くの Lambda ストリーミングアプリケーションのサンプルが掲載されています。これらは AWS アカウント内のリソースの構築およびデプロイに AWS SAM を使用しています。

リポジトリをクローンして、サンプルを探してみましょう。各パターンフォルダ内の README ファイルには、追加の情報が含まれています。

git clone https://github.com/aws-samples/serverless-patterns/ 
cd serverless-patterns

write() を使用した場合の TTFB

  1. ストリーミングによって最初のバイトまでの時間がどのように改善されるかを確かめるために、lambda-streaming-ttfb-write-sam パターンをデプロイしてみます。
    cd lambda-streaming-ttfb-write-sam
  2. AWS SAM を使用して、リソースを AWS アカウントにデプロイします。最初のデプロイでは、ガイド付きのデプロイを実行して、デフォルトパラメータをセットします。
    sam deploy -g --stack-name lambda-streaming-ttfb-write-sam

    これ以降のデプロイには、sam deploy を使用します。

  3. Stack Name を入力し、他のパラメータはデフォルトのままにします。
    AWS SAM は、ストリーミングをサポートした Lambda 関数と関数 URL をデプロイします。

    AWS SAM deploy -g

    デプロイが完了すると、AWS SAM からリソースの詳細が提供されます。

    AWS SAM リソース

    AWS SAM の出力では、Lambda 関数 URL が返されます。

  4. URL は認証に AWS Identity and Access Management (IAM) を使用しているため、ストリーミングレスポンスを確認するには AWS のクレデンシャルを使用して curl を実行します。URL と Region のパラメータは、自分の環境に合わせて置き換えてください。
    curl --request GET https://<url>.lambda-url.<Region>.on.aws/ --user AKIAIOSFODNN7EXAMPLE:wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY --aws-sigv4 'aws:amz:<Region>:lambda' -N

    ストリームで送信されたレスポンスが徐々に表示される様子が見えるかと思います。

    write() からストリームレスポンスを取得するために curl を使用

pipeline() を使用した場合の TTFB

  1. pipeline() を使用したサンプルを試すには、lambda-streaming-ttfb-pipeline-sam パターンをデプロイします。
    cd ..
    cd lambda-streaming-ttfb-pipeline-sam
  2. AWS SAM を使用して、リソースを AWS アカウントにデプロイします。初回デプロイでは、ガイド付きデプロイを実行して、デフォルトパラメータをセットします。
    sam deploy -g --stack-name lambda-streaming-ttfb-pipeline-sam
  3. Stack Name を入力し、他のパラメータはデフォルトのままにします。
  4. ストリーミングレスポンスを確認するには AWS のクレデンシャルを使用して curl を実行します。URL と Region のパラメータは、自分の環境に合わせて置き換えてください。
    curl --request GET https://<url>.lambda-url.<Region>.on.aws/ --user AKIAIOSFODNN7EXAMPLE:wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY --aws-sigv4 'aws:amz:<Region>:lambda' -N

    パイプライン化されたレスポンスストリームが返されるのが分かるかと思います。

    関数からストリームレスポンスを取得するために curl を使用

巨大なペイロード

  1. ストリーミングによってより大きなペイロードを返すことができることを示すために、lambda-streaming-large-sam アプリケーションをデプロイします。AWS SAM は、非ストリームの場合の 6 MB というレスポンスペイロードのサイズ制限よりも大きな 7 MB の PDF ファイルを返す Lambda 関数をデプロイします。
    cd ..
    cd lambda-streaming-large-sam 
    sam deploy -g —stack-name lambda-streaming-large-sam
  2. AWS SAM の出力は、Lambda 関数 URL を返します。AWS の クレデンシャルを使用して curlを実行し、ストリーミングレスポンスを確認してみます。
    curl --request GET https://<url>.lambda-url.<Region>.on.aws/ --user AKIAIOSFODNN7EXAMPLE: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY --aws-sigv4 'aws:amz:<Region>:lambda' -o SVS401-ri22.pdf -w '%{content_type}'

    PDFファイル SVS401-ri22.pdf をカレントディレクトリにダウンロードし、コンテンツの種類を application/pdf と表示します。

同様に、API Gateway を利用する場合も、Lambda 関数 URL と HTTP_PROXY 統合によって巨大なペイロードを返すことができます。

AWS SDK を使用してレスポンスストリーミングをサポートした関数を呼び出す

AWS SDK を使用して、新しい Lambda InvokeWithResponseStream API から直接レスポンスをストリームすることも可能です。これにより、ストリームの途中で発生したエラーの処理などの追加機能が提供されます。これは、例えば内部向けのマイクロサービスを構築する際に役立ちます。レスポンスストリーミングは、AWS SDK for Java 2.x, AWS SDK for JavaScript v3, AWS SDK for Go version 1 および version 2 でサポートされています。

SDK のレスポンスは、読み取り可能なイベントストリームを返します。イベントストリームには、2 つのイベントタイプが含まれます。PayloadChunk には、クライアントが受信したレスポンスデータの一部を含む生のバイナリバッファが含まれます。InvokeComplete は、関数がデータの送信を完了したことを知らせます。また、ストリームの途中で関数内でエラーが発生したかどうかなど、追加のメタデータも含まれます。エラーには、関数コードが投げる未処理の例外や、関数のタイムアウトが含まれます。

AWS SDK for Javascript v3 の使用

  1. AWS SDK を使用して関数からレスポンスをストリームする方法を確認するには、lambda-streaming-sdk-sam パターンをデプロイしてください。
    cd ..
    cd lambda-streaming-sdk-sam 
    sam deploy -g —stack-name lambda-streaming-sdk-sam
  2. Stack Name を入力し、他のパラメータはデフォルトのままにします。
    AWS SAM は、ストリーミングをサポートした 3 つの Lambda 関数をデプロイします。
  • HappyPathFunction: 完全なストリームを返します。
  • MidstreamErrorFunction: ストリームの途中でエラーが発生することをシミュレートします。
  • TimeoutFunction: ストリームが完了する前に関数がタイムアウトします。

SDK のサンプルアプリケーションを実行し、各 Lambda 関数を呼び出した結果を出力します。
(*訳註: 記事執筆時点では、index.mjs の書き換えが必要です。const REGION を SAM をデプロイしたリージョンに、const EXAMPLE_FUNCTION_NAME_PREFIX をデプロイしたスタック名に置き換えてください)

npm install @aws-sdk/client-lambda 
node index.mjs

各関数と、ストリームの途中で発生したエラーやタイムアウトエラーがどのように SDK クライアントに返されるかを確認できます。

ストリームの途中でエラー

タイムアウトのエラー

クォータと料金

ストリーミングレスポンスは、レスポンスペイロードのネットワーク転送にかかる追加コストが発生します。最初の 6 MB を超えて Lambda 関数から生成およびストリーミングされたバイト数に基づいて課金されます。詳細については、Lambdaの料金設定を参照してください。
(*訳注: 日本語ページで表示されない場合は英語ページに切り替えてご覧ください)

レスポンスの最大サイズは初期値で 20 MB ですが、これは上限緩和可能なソフトリミットです。また、ストリーミング機能には、最大 16 Mbps (2MB/s) の帯域スループット制限があります。

まとめ

本日 (2023年4月7日)、AWS Lambda は、レスポンスが利用可能になった段階で呼び出し側に部分的なレスポンスを送信するレスポンスペイロードストリーミングのサポートを発表しました。これにより、Web アプリケーションやモバイルアプリケーションのパフォーマンスを向上させることができます。また、レスポンスストリーミングを使用することで、より大きなペイロードを返す関数や、進捗状況を段階的に報告しながら長時間処理を行う関数を構築することも可能です。部分的なレスポンスのストリーミングは、Lambda 関数 URL または AWS SDK 経由で利用できます。レスポンスストリーミングは現在、Node.js 14.x 以降のランタイムと、カスタムランタイムをサポートしています。

Serverless Patterns Collection には、Lambda ストリーミングアプリケーションのサンプルが多数用意されており、この機能を試してみることができます。

また、Lambda レスポンスストリーミングのサポートは、Datadog, Dynatrace, New Relic, Pulumi, Lumigo など多くの AWS Lambda パートナーからも提供されています。

その他のサーバーレス学習リソースは、Serverless Land をご覧ください。

翻訳はパートナーソリューションアーキテクトの櫻谷が担当しました。原文はこちらです。