AWS Builder Center

Serverless DeepDive #02:SQSへの直接統合とLambdaを比べる

API GatewayからSQSへ直接送る構成と、Lambdaを使う構成をAWS実機で比べました。数量と本文サイズの境界、S3のルール欠落、送信後に502を返して手動再送した結果から、送信前に必要な検査と再送時の重複を確認します。

Series: Serverless DeepDive (13 articles)

  1. …
  2. 3
    Serverless DeepDive #02:SQSへの直接統合とLambdaを比べる This article
Senior Evangelist

SQSへの送信にLambdaは必要か

APIで受けた商品依頼をSQSへ送る構成を、AWS上で比べました。SQSは、後で処理するメッセージを保管するキューです。API Gatewayから直接送る構成と、Lambdaを経由する構成を使います。
転送だけなら、どちらも日本語や記号を含む本文をそのまま送れました。閉じ括弧が欠けたJSONも届きました。そこで、Lambda側に「商品ごとの数量上限を確認し、超えた依頼は送信前に拒否する」処理を加えました。
この検査を加えた場合の違いを、数量と本文サイズの境界、上限を読めない場合、送信後の再送で確かめます。
第00回 では、Serverlessを選ぶ理由を考えました。今回は、SQSへの転送をAPI Gatewayへ任せ、転送用の関数を管理せずに済む条件を調べます。第01回 で扱った呼び出しの成功と業務の完了の違いは、SQSへ送信した後の再送でも確認します。

直接統合と二つのLambdaを比べる

検証用に、商品と数量を受け付けるAPIを作りました。商品を表すSKUはBOOK、数量はquantityで指定します。商品ごとの上限はS3のrules.jsonへ保存しました。S3は、ファイルなどのオブジェクトを保存するサービスです。
経路SQSへ送る前に行うこと
directAPI Gateway HTTP APIの直接統合で、HTTP本文をSendMessageへ渡す
forwardLambdaでHTTP本文を受け、そのままSendMessageを呼ぶ
validatedLambdaで本文サイズ・JSON・数量を確認し、S3の上限を読んでからSendMessageを呼ぶ
IAM認証付きHTTP APIの三つの経路が同じSQSへ送信する構成。validatedだけがS3のルールを読み、APIとLambdaのログをCloudWatchへ残す
図は初回の構成です。青の線は依頼、橙の線はルールの取得、点線はログの流れを示します。補足測定では、同じ構成を別のstackに作り直し、CloudShellから呼びました。
三つともアクセス権を確かめるIAM認証を使い、署名付きのHTTP POSTで呼びました。API GatewayとLambdaの実行ロールで、キューへの権限は対象キューのSendMessageだけに絞りました。S3のルールを読む権限はvalidatedだけに付けています。
条件設定・観測方法
Regionap-northeast-1
LambdaPython 3.13、arm64、256 MB、timeout 10秒
キューSQS Standard、本文上限1024 byte、保持1時間、可視性timeout 30秒
入力比較する経路へ同じUTF-8本文を送信。再送試験のjobIdだけは経路ごとに変更
再試行呼出元とLambda内のSDKの自動再試行を無効化。再送は明示した呼び出しだけ
観測HTTP応答、API request ID、関数のログ、ReceiveMessageのMessageIdと本文
実行量初回47回と補足44回の計91 HTTP呼び出し。うちLambdaは58回。各測定の各条件は原則1回ずつ
再送試験では、同じ経路の初回と再送に同じ本文を使いました。
Consoleの画像は初回の測定結果です。補足測定では、各試験の経路別の応答と受信結果を一画面に並べました。そのため、同じ構成・handler・試験入力で44回を測り直しました。CloudShellの画像は、その結果です。別の送信なので、MessageIdは初回と変わります。
到着の確認には、AWS CLIのReceiveMessageを使いました。SendMessageが返したMessageIdを受信記録と突き合わせ、本文も照合しています。受信後に業務処理を行うconsumerは起動していません。関数が送信前に拒否したことは、実装とbefore_sendのログで確認しました。
CloudShellの画面では、caseが試験入力、routeが経路、HTTPが応答statusです。receivedはMessageIdの一致、body-exactは本文の一致を示します。MessageIdがない拒否応答ではFalse/Noneになります。
1024 byteは、境界を小さく試すために設定した値です。SQSのサービス上限とは区別します。MaximumMessageSizeの仕様 では、キューごとの上限を1024 byteから1 MiBまで設定できます。

直接統合でも日本語や記号をそのまま送れた

AWSリソースをコードで定義するAWS CDKを使いました。次は、directの統合設定の抜粋です。api、apiRole、queueは、それぞれ今回作成したHTTP API、実行ロール、キューです。
1
2
3
4
5
6
7
8
9
const direct = new apigw.CfnIntegration(stack, "DirectIntegration", {
apiId: api.ref,
integrationType: "AWS_PROXY",
integrationSubtype: "SQS-SendMessage",
credentialsArn: apiRole.roleArn,
payloadFormatVersion: "1.0",
requestParameters: { QueueUrl: queue.queueUrl, MessageBody: "$request.body" },
timeoutInMillis: 10000,
});
MessageBodyには、HTTP本文全体を表す$request.bodyを指定しています。HTTP APIのサービス統合 を使い、API GatewayからSendMessageを呼びます。
比較に使った本文の一つは、次の値です。
1
{"jobId":"forwarding-unicode","sku":"BOOK","quantity":1,"note":"受付 A&B + / \"引用\""}
日本語、アンパサンド、プラス、スラッシュ、引用符を含めました。UTF-8で91 byteです。direct、forward、validatedの三つとも、受信した本文が送信時の文字列と一致しました。
次はdirectから届いた本文です。MessageIdはe321937eで始まります。
directから届いたMessageId e321937eで始まるメッセージ。日本語と記号を含む91 byteの本文がSQSに残った
forwardから届いた本文も同じ値です。MessageIdはa3c86ad1で始まります。
forwardから届いたMessageId a3c86ad1で始まるメッセージ。directと同じ日本語と記号を含む本文がSQSに残った
HTTPの応答形式には差がありました。directは200とXML、forwardはコードで決めた200とJSON、validatedは202とJSONを返しました。validatedの202は、同期で動いた関数が返すアプリケーションの応答です。Lambda Invoke API の非同期呼び出しが返す202とは意味が異なります。
最初の測定scriptは、応答をJSONとしてだけ解析していました。そのためdirectのMessageIdを拾えず、到着との照合に失敗しました。保存した応答はXMLだったので、XML解析を足しました。
直接経路で、同種の正常・日本語・不正JSONを各1回測り直しました。正常と日本語はjobIdを変えたため、59 byteと96 byteになりました。不正JSONは同じ17 byteです。3件とも送った本文と一致しました。
掲載したSQSの画面は、初回のMessageIdです。最初の応答と受信記録も残し、別に照合し直しています。
本文の転送は、directでもできました。この比較で測ったのは送信結果と応答で、速度・料金・運用工数の優劣は測っていません。

S3の数量上限を読み、超過した依頼を拒否する

validatedは、SQSへ送る前に本文を検査します。数量は1〜1000の整数に限定し、さらにS3から商品ごとの上限を読みます。最初のルールは次の値です。
1
{"limits":{"BOOK":3}}
最初に、UTF-8で1024 byteを超える本文を413で拒否します。通過した本文のJSON構文と、次の入力規則を確認します。いずれもS3を読む前に行う処理です。
項目受け付ける条件
本文JSON objectで、下記4項目以外のキーを含まない
jobId必須。半角英小文字・数字・ハイフンだけの1〜64文字
sku必須。半角英大文字・数字・ハイフンだけの1〜32文字
quantity必須。1〜1000の整数。文字列・小数・真偽値は拒否
note任意。指定する場合は文字列
JSONを解析できない場合は400のinvalid_json、入力規則に反する場合は400のinvalid_jobを返します。規則を満たす依頼だけがS3の確認へ進みます。取得した上限を数量が超える場合は422を返します。両方の確認に通った場合だけ、元の本文をSQSへ送ります。
入力directforwardvalidated
JSONの閉じ括弧が欠ける200・到着200・到着400・送信前に拒否
quantity = 0200・到着200・到着400・送信前に拒否
quantity = 1200・到着200・到着202・到着
quantity = 3200・到着200・到着202・到着
quantity = 4200・到着200・到着422・送信前に拒否
quantity = "3"200・到着200・到着400・送信前に拒否
quantity = true200・到着200・到着400・送信前に拒否
表の不正JSONは、閉じ括弧のない次の17 byteです。
1
{"jobId":"broken"
次はdirectで受け付けた不正JSONです。MessageIdは181ca703で始まります。
direct経路からSQSへ届いた不正JSON。閉じ括弧がない17 byteの本文も、キューは文字列として受け付けた
背景の91 bytesは、別メッセージの一覧行です。ここで扱う17 byteは、ダイアログ本文をUTF-8で数えた長さです。
補足測定では、正常なJSON、日本語を含む本文、不正JSONを三経路で比較しました。
補足測定の転送比較。正常と日本語は三経路で本文が一致し、不正JSONはdirectとforwardが到着、validatedが400を返した
directとforwardは本文をそのままSendMessageへ渡すため、不正なJSONもSQSに保存されました。二つのLambdaには、同じhandlerを配置しています。JSON解析と数量検査を行うのは、環境変数INGRESS_MODEがvalidatedのときだけです。forwardは、この検査を通らずに送信します。
次に、関数を変更せず、S3の上限だけを3から2へ変えました。数量3をdirectとvalidatedへ各1回送っています。directは200で到着し、validatedは422で拒否しました。
validatedの実行ログ。上限3で数量1と3を受け付け、数量4を拒否した。上限を2へ変えた後は数量3も送信前に拒否した
画面のvalidation-q1、q3、q4は、それぞれ数量1、3、4の入力IDです。validation-limit2-q3は、上限2への更新後に送った数量3です。limit列は、関数がS3から読んだ上限を示します。
数量と型については、補足測定で20回呼び出しました。
補足測定の20回。directとforwardは数量0・4・文字列・真偽値も到着し、validatedは400または422で拒否した。上限2で数量3はdirectだけ到着した
補足測定のstringは数量の文字列"3"、booleanはtrueです。
今回のdirectはSendMessageだけを呼びます。validatedには、S3から上限を取得し、数量と比較してから送信する処理を実装しました。上限を変えると、同じ数量3への応答が202から422へ変わりました。
JSONの構文や項目だけをSQSへの送信前に検査するなら、API Gatewayのrequest validationも候補です。機能比較 では、REST APIが対応し、HTTP APIは対応していません。この機能を使うREST APIの直接統合は、今回の測定範囲に含めていません。

1025 byteの本文は三経路とも送れなかった

キューに設定した本文上限は1024 byteです。JSONのnoteをASCII文字で埋め、ちょうど1024 byteと1025 byteになる二つの入力を作りました。数量は1に固定し、それぞれ三経路へ1回ずつ送りました。
本文のUTF-8サイズdirectforwardvalidated
1024 byte200・到着200・到着202・到着
1025 byte400・InvalidParameterValue400・InvalidParameterValue413・body_too_large
validatedだけが413を返したのは、関数内でサイズを検査してからSQSを呼ぶためです。
1024 byteは両Lambda経路で送信できた。1025 byteはforwardが送信時の400、validatedが送信前の413を記録した
同じ二つの本文で補足測定を行い、初回と同じ結果を確認しました。
補足測定のサイズ境界。1024 byteは三経路で到着し、1025 byteはdirectとforwardが400、validatedが413を返した
forwardの400は、SQSから受けたエラーを関数のJSON応答へ変換したものです。validatedは環境変数QUEUE_MAX_BYTES=1024で送信前にサイズを検査します。キューの上限を変えるときは、この値もそろえる必要があります。

S3のrules.jsonを削除すると、validatedが503を返した

S3のrules.jsonを消し、数量2の同じ本文をdirectとvalidatedへ送りました。directは200でSQSへ届きました。validatedは503のrule_unavailableを返し、送信前に止まりました。
次に上限3のrules.jsonを戻し、validatedへ同じ本文を再送しました。今度は202を返し、SQSで1件を確認できました。
同じrecovery-rule-missingの依頼に対する実行ログ。ルール欠落時は503とbefore_send、復旧後の再送では202とMessageIdを記録した
補足測定のrule-missingは欠落時、rule-restoredは復旧後の呼び出しです。
補足測定のルール欠落と復旧。directは200で到着し、validatedは503で拒否した。ルールを戻した後のvalidatedは202で到着した
validatedは、S3から上限を取得できることを送信の条件にしています。今回はrules.jsonを削除して、その条件を満たせない場合を試しました。S3全体の障害、長い遅延、キャッシュ方式は測っていません。
取得済みのルールをキャッシュする案もあります。その場合は、S3から取得できない間、古い上限で受け付けてよい時間を決める必要があります。
また、この検査で使うのはS3を読み取った時点の上限です。確認後からSQSへの送信までに、上限が変わる可能性があります。S3の読み取りとSQS送信を、一つのtransactionにはしていません。在庫の取り置きまで求める場合は、同時更新を含む別の設計が必要です。

送信後の502に対して再送すると、同じ依頼が2件届いた

次に、SQSへの送信が成功した後でエラー応答を返し、呼出元が再送した場合を試しました。forwardとvalidatedには、検証用のヘッダーが付いていると送信後に502を返す処理を加えました。
次はその抜粋です。rawはHTTP本文の文字列、modeはforwardかvalidatedです。eventにはHTTP APIのイベントが入ります。SQSは、Python用AWS SDKであるboto3のSQS clientです。doneがログの記録とHTTP応答をまとめます。
1
2
3
4
5
sent = SQS.send_message(QueueUrl=os.environ["QUEUE_URL"], MessageBody=raw)
result = {"messageId": sent["MessageId"], "phase": "sent"}
if event.get("headers", {}).get("x-lab-fail-after-send") == "1":
return done(502, {"reason": "injected_after_send", **result})
return done(202 if mode == "validated" else 200, result)
これは、送信後にエラー応答が返る条件を作るための分岐です。実際のネットワーク断や、Lambda自身の障害を発生させたわけではありません。IAM認証を付けた使い捨ての検証APIだけで使いました。
初回だけこのヘッダーを付け、次は外して送ります。経路ごとに同じjobId、同じ数量2、同じ本文を使いました。directにはこの失敗を注入しておらず、正常な依頼を単純に2回送っています。
経路初回のHTTP応答手動再送のHTTP応答受信した異なるMessageId
direct2002002件
forward502・送信後に注入2002件
validated502・送信後に注入2022件
validatedの初回502と再送202。どちらもsentを記録し、同じjobIdに異なる二つのMessageIdが付いた
SQSで三経路の計6件を受信し、それぞれ送信時の本文と一致することを確認しました。
SQSで受信した再送試験の6件。三つの経路で2件ずつ、異なるMessageIdのメッセージが残った
このConsole画面のReceive countには、測定用CLIと撮影時の受信が含まれます。ここで数えたのは、意図して2回送ったことで生じた異なるMessageIdです。SQSが自然に同じメッセージを再配送した回数としては扱いません。
Consoleで撮影した6件の対応は次のとおりです。MessageIdは先頭8桁で示します。本文には経路名を含むjobIdを入れたため、byte数が経路ごとに異なります。
経路本文のbyte数初回のMessageId再送のMessageId
direct56fbbc48c6999e315b
forward57ccd06f8ea344085c
validated59451bc05bc861d446
補足の再送試験は別の送信なので、上の表とはMessageIdが異なります。
補足測定の手動再送。directの200と200、forwardの502と200、validatedの502と202で、各経路から二つの異なるMessageIdが到着した
今回の手動再送とは別に、Standard queueは少なくとも1回の配送を保証する仕様 です。同じメッセージが複数回届く場合があるため、受信後の処理でも重複への対応が必要です。
今回、呼出元が502を受け取った時点で、本文はすでにSQSへ届いていました。同じ依頼を再送すると、別のMessageIdを持つメッセージが増えました。validatedは、過去に受け付けたjobIdとの照合を行いません。二重処理への対策は、数量などの入力検査とは別に設計します。

直接統合とLambdaを使い分ける条件

本文をそのままキューへ入れ、入力の検査を受信後に行うなら、今回の直接統合で転送用Lambdaを省けます。呼出元はXMLの成功応答とエラー本文を扱う必要があります。不正なJSONにも200を返すので、HTTP応答だけで入力が有効かどうかは判断できません。
S3の上限を確認してから送り、超過した依頼にはその場でエラーを返す場合は、validatedのLambdaで実現できました。呼出元へ返すstatusとJSON本文も、関数のコードで指定しています。HTTP API側だけで応答本文を変える構成は、今回試していません。
今回の実装では、rules.jsonを取得できないときは503で止め、キューの上限とQUEUE_MAX_BYTESを1024にそろえました。こうした拒否条件と設定値は利用者がコードで管理し、Lambdaの実行基盤の運用はAWSへ任せます。

Series: Serverless DeepDive (13 articles)

  1. …
  2. 3
    Serverless DeepDive #02:SQSへの直接統合とLambdaを比べる 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