AWS Builder Center

Serverless DeepDive #03:非同期Invokeのユースケースを探る

Lambdaの同期Invokeと非同期Invokeを同じworkerで比較しました。非同期で速くなるのは202が返るまでで、完了までの時間は同じでした。再試行とtimeoutの実測もあわせて、完了を待たずに受付を返していい処理を考えます。

Series: Serverless DeepDive (13 articles)

  1. …
  2. 4
    Serverless DeepDive #03:非同期Invokeのユースケースを探る This article
Senior Evangelist
商品登録の依頼を受け、2秒以内に返答する場面を考えます。登録に2.5秒かかるなら、完了を待つ返答は間に合いません。先に受付を返し、後から結果を伝える方法が候補になります。
今回の検証では、Lambdaへの同期呼び出しと非同期呼び出しを比べました。非同期の返答は中央値0.049秒でした。一方、登録の完了確認は中央値約2.85秒後です。初回の処理を失敗させた1回の観測では、再試行後の完了確認に約55秒かかりました。
この記事で確かめるのは、どの業務なら、完了を待たずに受け付けを返せるかです。短い返答と、その後の失敗を扱う責任を一緒に考えます。
#02 では、SQSへ送る前に必要な検査を比べました。今回は受け付けた仕事の完了までを追います。Serverlessでは、実行基盤の管理をAWSへ任せます。Lambdaの関数 は、その基盤でイベントを受けてプログラムを動かします。基盤を任せた後に、受付の返答が何を約束するかを決めるのが今回のテーマです。

検証構成

架空の商品登録jobを使います。jobは、一つの処理依頼を表します。数量を検査し、指定した時間だけ待ち、S3へ登録結果を書き込みます。実際の在庫や決済には触れません。待機で所要時間を固定し、呼び出し方の違いを見ます。
Invoke API は、Lambdaを呼ぶためのAPIです。同期のRequestResponseでは、関数の返答まで待ちます。非同期のEventでは、AWSがイベントを受け付けた段階で返答します。両経路は同じworker関数を使います。
条件設定
Regionap-northeast-1(Tokyo)
Runtimenodejs26.x、実行時のNode.js v26.8.2、public Preview
worker256 MB、arm64、timeout 5秒、reserved concurrency 1
非同期の設定再試行1回、最大event age 180秒
同期のログ取得LogType Tailで、応答に実行ログの末尾を含める
通常の入力quantity 2、workMs 2500、failOnce false
数量の許可範囲整数1〜3
完了の条件result.jsonの保存後、status.jsonがSUCCEEDEDになる
状態の確認PCからS3を0.4秒間隔で確認
reserved concurrency 1は、関数の同時実行を1件に制限する設定です。最大event ageは、非同期イベントを保持する最長時間です。
Node.js 26は、検証開始時にNode.jsで利用できる最新のmanaged runtimeでした。Tokyoで関数を作成し、実行できました。ただし、runtime一覧 ではPreviewです。SLAとTechnical Supportの対象外です。本番では正式提供のruntimeを選び、同じ条件を確かめ直す必要があります。
PCからworkerを同期またはEventで呼び、S3のjob状態を確認する構成。非同期の最終失敗は別のLambdaで記録し、両関数のログをCloudWatchへ送る。
図1:検証した構成です。workerの実行基盤と非同期の配送をAWSへ任せ、完了条件と状態保存をコードで決めます。OnFailureは、非同期の最終失敗を別の関数へ渡す設定です。
job IDは呼び出し前にPCで決め、イベントへ入れます。202の応答本文は空です。PCが保持したjob IDを使い、S3の状態を探します。今回はPCで結果を読む方式です。利用者が後から結果を読む仕組みは、業務に合わせて用意します。
workerは、許可範囲の数量ならRUNNINGを書き、処理を始めます。成功時はSUCCEEDEDです。範囲外の数量は、RUNNINGを経ずにFAILEDを保存します。初回だけ例外を起こす試験では、例外の前にRETRYINGを残します。各状態にはjob ID、試行番号、時刻、RequestIdを付けます。RequestIdは、Lambdaの実行ログを追うための識別子です。

同期と非同期を18回ずつ比べる

通常の入力を、各経路へ18回ずつ送信しました。6回ずつの比較を3組行い、経路の順序も入れ替えました。jobごとに別IDを付け、前のjobの完了を確認してから次を送っています。
経路返答までの中央値返答までの最小〜最大完了確認までの中央値完了確認までの最小〜最大
同期 RequestResponse2.751秒2.724〜3.367秒2.845秒2.814〜3.534秒
非同期 Event0.049秒0.036〜0.089秒2.847秒2.798〜3.318秒
返答時間は、PCでInvokeを始めた時点から測ります。時刻が逆行しないmonotonic clockを使いました。応答bodyの読み取りと解析、PCへの保存も含みます。同期だけは、返されたログのデコードと保存も含みます。この処理の影響を分けて測ってはいません。
完了確認時間も、同じ開始点から測ります。S3の終端状態を読み、観測記録をPCへ保存するまでです。先の応答処理、通信、pollingの待ち時間も含みます。どちらも関数内の処理時間とは区別します。
同期は18回とも、返答直後の状態取得でSUCCEEDEDを読めました。Eventは18回とも、その時点ではstatus.jsonがまだありませんでした。両経路の完了確認は、中央値で約2.85秒でした。
同期のAPI 200とSUCCEEDED、EventのAPI 202とRUNNINGを表示した実際のAWS CLI画面。Node.js v26.8.2も確認できる。
図2:撮影用に同じ入力を各経路へ1回ずつ送った結果です。通常の36回の集計とは別のjobです。先頭はNode.jsのversionを確認した別の呼び出しです。API 200とnodeVersionを表示しています。これも通常36回の集計に含めません。Eventの返答直後には、処理中のRUNNINGが読めました。状態が未作成か処理中かも、取得する時点で変わります。
通常比較のsync-g1-1とasync-g1-1が、どちらもworkMs 2500、attempt 1、SUCCEEDEDになったS3の実測状態。
図3:通常比較の両jobの終端状態です。job IDとRequestIdで、入力と実行ログを対応させています。
この条件では、Eventの返答を2秒以内に受け取れました。登録の完了確認は同期と近い値です。受付を早める設計なら使えますが、2秒以内の登録完了を約束する根拠にはできません。
通常36回のREPORTには、Init Durationの記載がありませんでした。同期の最大3.367秒は、先頭のjobの値です。そのDurationは3325.64msで、固定待機2500msの外側に約826msかかっています。残り17回の返答は、2.724〜2.823秒に収まりました。この記録では、最大値をcold startと分類する根拠は得られません。

待機時間を短くしてみる

待機時間を短くして、同じworkerを動かしました。1.0秒、1.7秒、1.9秒の待機を、両経路へ各1回ずつ送っています。
workMs同期の返答までEventの返答まで
10001.598秒0.052秒
17001.974秒0.048秒
19002.174秒0.041秒
待機1000・1700・1900msの同期jobが成功したS3の状態と、同じRequestIdのREPORT Durationを表示したAWS CLI画面。
図4:S3の入力とCloudWatch Logsの実行時間を照合しました。REPORTのDurationは順に1541.88、1935.34、2132.98msです。表のPCから見た返答時間とは、測る範囲が異なります。
待機1.7秒の同期返答は2秒以内でした。待機1.9秒では超えています。workerは、待機のほかに状態の取得と複数回の保存も行います。通信も加わるため、待機時間だけを返答期限と比べると見誤ります。
各条件1回なので、1.7秒なら常に間に合うとは言えません。それでも、短い仕事なら同期で完了を返せる候補になります。利用者がその場で登録結果を必要とするなら、実際の負荷で応答の余裕を確かめる方が要件に合います。

数量の境界を確かめる

数量の下限と上限を確かめました。0、1、3、4を両経路へ各1回送り、待機を100msに変えています。
quantity同期の応答と業務結果Eventの応答と後の状態
0API 200、ok false、FAILEDAPI 202、FAILED
1API 200、ok true、SUCCEEDEDAPI 202、SUCCEEDED
3API 200、ok true、SUCCEEDEDAPI 202、SUCCEEDED
4API 200、ok false、FAILEDAPI 202、FAILED
数量0と4は両経路でFAILED、1と3はSUCCEEDEDになった8件のS3状態。すべてattempt 1。
図5:数量の許可範囲は1〜3です。範囲外のjobにはquantity_out_of_rangeを保存しました。
範囲外の数量を、workerは通常の戻り値で拒否します。関数例外は出さず、FunctionErrorもありませんでした。8件とも試行は1回です。Lambdaにとって正常な実行でも、業務では拒否になるようにしています。
不正な数量は、待っても正しくなりません。業務の拒否と、一時的な処理失敗を分けると、直らない依頼の再試行を避けられます。この実装ではworker関数の中で数量を検査するため、Eventは先に202を返しました。受付時点で拒否を伝えたい要件なら、#02で扱った送信前の検査が必要になります。

初回だけ失敗させてみる

次に、数量2、待機100msのjobを使いました。両経路で、初回だけ保存前に例外を起こす試験を各1回行いました。例外の直前にはRETRYINGを保存します。
同期の初回は、API 200にFunctionErrorが付きました。Lambdaによる同期の自動再試行はありません。SDKの自動再送も無効にしています。状態を確認し、同じjob IDで手動再送すると、2回目にSUCCEEDEDになりました。
Eventは、最初の202を0.042秒で返しました。その後にworkerが失敗し、AWSが1回再試行しています。Invoke開始から55.025秒後に、2回目のSUCCEEDEDを確認できました。
同期の手動再送と非同期の自動再試行について、attempt 1のRETRYINGとattempt 2のSUCCEEDEDを表示したS3の試行履歴。
図6:初回の履歴と再試行後の履歴です。非同期では、二つの試行に同じRequestIdが記録されています。
非同期の開始時刻は、初回が06:28:52.255Z、2回目が06:29:46.744Zでした。約54.5秒離れています。今回の非同期では、RequestIdだけでは試行を区別できませんでした。job IDと試行番号、時刻も残したため、二つの実行を追えています。
AWSの仕様 では、関数errorの再試行は既定で2回です。待機は初回と2回目の間が1分、2回目と3回目の間が2分です。今回は再試行を1回、最大event ageを180秒に変えました。約55秒はこの設定での1回の観測で、固定の再試行間隔や完了期限には使えません。
#01 でも、同期の手動再送と非同期の同じRequestIdを観測しました。再試行の開始間隔は、保存前の例外で59.2秒、保存後で55.2秒です。今回、同じ測り方の値は54.5秒でした。入力とruntimeに加え、最大event ageも120秒から180秒へ変えています。条件が異なる値なので、固定の開始間隔の根拠にはしません。
今回は、受付から完了確認までの約55秒と、試行履歴を追いました。次に、関数のtimeoutを超えた場合を調べます。呼出元のread timeoutも、別に試します。#01で扱った冪等性の条件は、後でまとめます。冪等性は、同じ依頼を繰り返しても業務効果を重ねない仕組みです。

関数のtimeout

待機を6500msに延ばし、関数のtimeout 5秒を超える条件も試しました。両経路へ1回ずつ送っています。
同期は5.083秒でFunctionErrorを受け取りました。S3にはRUNNINGが残っています。実行が途中で止まり、workerがFAILEDを書けなかったためです。通常の例外処理だけでは、この停止を終端状態へ変えられません。
非同期は0.060秒で202を返しました。初回と再試行の両方がtimeoutしています。その後、OnFailureで受け取った記録を、別のfailure recorderがS3へ保存しました。Invoke開始から71.766秒後に、FAILEDを確認できました。
同期timeout後のRUNNINGと非同期のFAILED。実際の失敗通知をjqで抜粋し、approximateInvokeCountをinvokeCountという名前で表示したAWS CLI画面。
図7:非同期の通知にはRetriesExhaustedと、実行回数の概数2が入っています。画面は通知の抜粋です。requestContext.approximateInvokeCountをinvokeCount、responsePayloadをerrorという名前で表示しました。timeoutのエラーも、同じRequestIdで確認できます。
failure recorderは、workerと同じ256 MB、arm64、timeout 5秒です。再試行は0回、最大event ageは60秒にしています。workerのOnFailureは非同期呼び出し用なので、同期timeoutのRUNNINGは更新しませんでした。
失敗した呼び出しの記録を保持する設定 には、配送失敗の監視もあります。通知先へ送れない場合、LambdaはCloudWatchへ失敗回数を出します。監視に使うmetric名はDestinationDeliveryFailuresです。今回は通知が届き、状態を書けることを確認しました。
非同期配送の仕様 では、正常処理でも重複配送が起こり得ます。期限切れや全再試行の失敗では、イベントは破棄されます。処理が追い付かない場合、関数へ送られる前に削除されることもあります。
採用する際は、DestinationDeliveryFailuresを監視します。failure recorder関数のErrorsも確認します。Errorsは、関数の実行失敗を数えるmetricです。状態が未作成のjobや、長くRUNNINGのままのjobも探します。呼出元のjob IDと受付記録で、期限までに終わったかを照合します。再試行後の失敗を誰へ伝えるか、どこから再処理するかは業務側で決めます。約72秒という失敗確認の時間も、今回の再試行1回・最大event age 180秒での値です。その場で確定が必要な業務へ当てはめることはできません。

呼出元のtimeout

関数の実行期限とは別に、呼出元の通信期限も試しました。通常と同じ2500msのjobを同期で1回送り、SDKのread timeoutを1秒にします。自動再送は無効です。後の撮影用のCLI試験も1回です。
PCでは1.053秒後にReadTimeoutErrorが出ました。この値はInvoke開始から、例外を受けるまでの時間です。Invokeの応答を読めていないので、その時点で登録の成否は分かりません。同じjob IDの状態を追うと、3.939秒後にSUCCEEDEDを確認できました。
AWS CLIがread timeoutを返した後、同じshot-callerのS3状態がSUCCEEDEDになった実画面。待機2500ms、attempt 1。
図8:撮影用にCLIでも1回確かめた結果です。CLIのread timeoutは1秒です。AWS_MAX_ATTEMPTS=1で自動再送を無効にしています。3秒待ってから、同じjob IDの状態を取得しました。上のSDK測定とは別のjobです。
この条件では、呼出元が待つのをやめても、workerは登録結果を保存しました。timeoutを「登録に失敗した」と決めて再送すると、完了済みの仕事へ新たな依頼を重ねるおそれがあります。まず既知のjob IDで状態を照合する設計が必要になります。
今回の比較に含めた負荷は、別IDのjobを一つずつ送る条件です。backlog、同時実行数を増やした場合、p99、意図的なcold startは未測定です。既定の再試行設定での完了時間も、集計の対象外です。通知先の障害・権限不足、未配送、期限切れを起こす試験は省いています。運用工数と費用の差も、今回の判断の根拠には含めません。

まとめ

今回の2.5秒の仕事では、Eventの返答が2秒以内に収まりました。再試行をAWSへ任せ、timeout後のjobの失敗を別の関数で残せました。受付を早く返し、約72秒後の最終失敗まで追えたことが、今回の条件でLambdaを選ぶ理由になります。
そのためには、利用者が後から結果を確認できることが必要です。商品登録の一括依頼なら、「受け付けました」と「登録できました」を分け、失敗した行を後から直す、という要件を設計候補にできます。正常時の中央値約2.85秒だけでは、期限を決められません。最大約3.5秒まで観測しました。この最大値も、期限の上限を保証するものではありません。利用者に伝える期限は、正常時の幅に加え、再試行や最終失敗までの待ち時間を考えて決めます。この業務での適合性は、今回の架空のjobから導く設計上の判断です。
同時に、job IDと状態の保持、結果の通知、停滞したjobの照合、再処理の判断を用意します。
今回のworkerは、既存の状態がSUCCEEDEDでも同じjobを再実行します。受付から完了確認までを見る実験用のコードとして、冪等化を省いています。S3の状態更新も、readとwriteを順に行うだけです。同じjobの同時更新を排除するtransactionはありません。reserved concurrency 1にしても、重複配送や業務効果の重複は防げません。本番の在庫確保や決済に使うなら、#01の条件付き保存と既存結果の照合を出発点にします。外部処理後の再送や、同じjobの同時更新も、対象の業務で確かめます。
その場で在庫の確保や支払いの確定が必要なら、202だけを成功の返答にはできません。同期で期限内に終える条件を探すか、受付待ちを許せる業務の流れへ変える必要があります。完了を待たずに返せる業務は、後の失敗まで含めて結果を伝えられる業務です。その約束を作れるかどうかが、非同期のLambdaを選ぶ条件になります。

Series: Serverless DeepDive (13 articles)

  1. …
  2. 4
    Serverless DeepDive #03:非同期Invokeのユースケースを探る This article
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article