Serverless DeepDive #02:SQSへの直接統合とLambdaを比べる
API GatewayからSQSへ直接送る構成と、Lambdaを使う構成をAWS実機で比べました。数量と本文サイズの境界、S3のルール欠落、送信後に502を返して手動再送した結果から、送信前に必要な検査と再送時の重複を確認します。
Series: Serverless DeepDive (13 articles)
- …
- 3Serverless DeepDive #02:SQSへの直接統合とLambdaを比べる This article
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へ送る前に行うこと |
|---|---|
| direct | API Gateway HTTP APIの直接統合で、HTTP本文をSendMessageへ渡す |
| forward | LambdaでHTTP本文を受け、そのままSendMessageを呼ぶ |
| validated | Lambdaで本文サイズ・JSON・数量を確認し、S3の上限を読んでからSendMessageを呼ぶ |

図は初回の構成です。青の線は依頼、橙の線はルールの取得、点線はログの流れを示します。補足測定では、同じ構成を別のstackに作り直し、CloudShellから呼びました。
三つともアクセス権を確かめるIAM認証を使い、署名付きのHTTP POSTで呼びました。API GatewayとLambdaの実行ロールで、キューへの権限は対象キューのSendMessageだけに絞りました。S3のルールを読む権限はvalidatedだけに付けています。
| 条件 | 設定・観測方法 |
|---|---|
| Region | ap-northeast-1 |
| Lambda | Python 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で始まります。

forwardから届いた本文も同じ値です。MessageIdはa3c86ad1で始まります。

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へ送ります。
| 入力 | direct | forward | validated |
|---|---|---|---|
| JSONの閉じ括弧が欠ける | 200・到着 | 200・到着 | 400・送信前に拒否 |
| quantity = 0 | 200・到着 | 200・到着 | 400・送信前に拒否 |
| quantity = 1 | 200・到着 | 200・到着 | 202・到着 |
| quantity = 3 | 200・到着 | 200・到着 | 202・到着 |
| quantity = 4 | 200・到着 | 200・到着 | 422・送信前に拒否 |
| quantity = "3" | 200・到着 | 200・到着 | 400・送信前に拒否 |
| quantity = true | 200・到着 | 200・到着 | 400・送信前に拒否 |
表の不正JSONは、閉じ括弧のない次の17 byteです。
1
{"jobId":"broken"次はdirectで受け付けた不正JSONです。MessageIdは181ca703で始まります。

背景の91 bytesは、別メッセージの一覧行です。ここで扱う17 byteは、ダイアログ本文をUTF-8で数えた長さです。
補足測定では、正常なJSON、日本語を含む本文、不正JSONを三経路で比較しました。

directとforwardは本文をそのままSendMessageへ渡すため、不正なJSONもSQSに保存されました。二つのLambdaには、同じhandlerを配置しています。JSON解析と数量検査を行うのは、環境変数INGRESS_MODEがvalidatedのときだけです。forwardは、この検査を通らずに送信します。
次に、関数を変更せず、S3の上限だけを3から2へ変えました。数量3をdirectとvalidatedへ各1回送っています。directは200で到着し、validatedは422で拒否しました。

画面のvalidation-q1、q3、q4は、それぞれ数量1、3、4の入力IDです。validation-limit2-q3は、上限2への更新後に送った数量3です。limit列は、関数がS3から読んだ上限を示します。
数量と型については、補足測定で20回呼び出しました。

補足測定の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サイズ | direct | forward | validated |
|---|---|---|---|
| 1024 byte | 200・到着 | 200・到着 | 202・到着 |
| 1025 byte | 400・InvalidParameterValue | 400・InvalidParameterValue | 413・body_too_large |
validatedだけが413を返したのは、関数内でサイズを検査してからSQSを呼ぶためです。

同じ二つの本文で補足測定を行い、初回と同じ結果を確認しました。

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件を確認できました。

補足測定のrule-missingは欠落時、rule-restoredは復旧後の呼び出しです。

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 |
|---|---|---|---|
| direct | 200 | 200 | 2件 |
| forward | 502・送信後に注入 | 200 | 2件 |
| validated | 502・送信後に注入 | 202 | 2件 |

SQSで三経路の計6件を受信し、それぞれ送信時の本文と一致することを確認しました。

このConsole画面のReceive countには、測定用CLIと撮影時の受信が含まれます。ここで数えたのは、意図して2回送ったことで生じた異なるMessageIdです。SQSが自然に同じメッセージを再配送した回数としては扱いません。
Consoleで撮影した6件の対応は次のとおりです。MessageIdは先頭8桁で示します。本文には経路名を含むjobIdを入れたため、byte数が経路ごとに異なります。
| 経路 | 本文のbyte数 | 初回のMessageId | 再送のMessageId |
|---|---|---|---|
| direct | 56 | fbbc48c6 | 999e315b |
| forward | 57 | ccd06f8e | a344085c |
| validated | 59 | 451bc05b | c861d446 |
補足の再送試験は別の送信なので、上の表とは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)
- …
- 3Serverless DeepDive #02:SQSへの直接統合とLambdaを比べる This article
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article