サーバーレスアプリケーション開発におけるエラーハンドリング
~ Web API パターン ~
Author : 大磯 直人
今回は Web API パターンにおけるサーバーレスのエラーハンドリングを AWS で実現する際に抑えておくべきポイントについて紹介します。Web API パターンでは、リトライを利用したエラーハンドリングをご説明します。
リトライの概要については、前回の「サーバーレスアプリケーション開発におけるエラーハンドリング」でも、サーバーレスエラーハンドリングの基礎と絡めてご説明しておりますので、またご覧になられていない方は、そちらからご覧いただけると理解が深まると思います !
この連載記事のその他の記事はこちら
- 選択
- 第 1 回 オープニング
- 第 2 回 Web API パターン
- 第 3 回 イベント駆動のデータ加工、連携処理パターン
- 第 4 回 マイクロバッチ・ストリーミングパターン
- 第 5 回 ワークフローパターン 前編
- 第 6 回 ワークフローパターン 後編
ユースケース
皆さんがサーバーレスアプリケーションに一番最初に触れたユースケースで最も多いのが、この Web API としてのパターンではないでしょうか ? HTTP リクエストをトリガーにして処理が行われる Web API は、サーバーレスと相性がいいです。
Web API は常時リクエストを受け付ける必要があるものの、実際にコンピューティングリソースが必要になるのは、HTTPリクエストがあったときのみです。そのタイミングについてはクライアント次第であることから、リクエストがなく全くリソースが必要ない時もあれば、同時に多数のリクエストが発生し大量のリソースが必要になる時もあり、その時々で変動します。このようなユースケースに求められるサーバーリソースの制御は、サーバーレスのイベント駆動のリソースプロビジョニングやそれに応じたコスト効率の良い課金体系にマッチします。
AWSがまとめているサーバーレスパターン集である「形で考えるサーバーレスパターン 」における代表的な適用シーン/ユースケースと実装形の中で、 Web API としてのサーバーレスアプリケーションは、以下のパターンに適応できます。
- 動的 Web / モバイルバックエンド
- リアルタイムモバイル / オフライン対応
- 業務系 API、グループ企業間 API
パターン特性
Web API パターンのサーバーレスアプリケーションは起動特性として、Push 形式のメッセージ処理かつ同期呼び出しの性質を持ちます。
Push 形式の処理とは、サーバーレスアプリケーションを起動させるトリガーが存在し、そのトリガーの発火によってアプリケーションが起動される形式です。Web API パターンのサーバーレスアプリケーションでは、サーバーレスアプリケーションを起動させるトリガーとして外部のアプリケーションからの HTTP リクエストが利用されます。
このような Push 形式の処理の特徴として、メッセージを送る責任がクライアントにあるため、サーバーサイドにあるサーバーレスアプリケーションは純粋な処理ロジックのみに注力できる点が挙げられます。
[Tips]
Push 形式の起動に対して、Pull 形式の処理の場合は、メッセージを取得する責任がその際のエラーハンドリングを含めてサーバー側にあるため、コードの実装によってカバーする責任範囲が増える傾向にあります。
このような Pull 型の難しい点をサーバーレスサービスを使うと実装者は意識しなくて済む機構もあります。
具体的には AWS ではイベントソースマッピングという機構で Pull 形式の実行方式のアーキテクチャパターンにおいて、その責務をサービス側で吸収する仕組みを提供しており、詳細についてはシリーズの後の章でご紹介します。
またもう一つの特徴として、同期型の通信であるため、リクエストに対するレスポンスが存在します。レスポンスは正常時にはシステムとしてクライアントで利用されるデータが入っておりますが、エラー時にはエラーに関わる情報を含むことができます。クライアントがエラーハンドリングを行う際には、レスポンスに含まれるエラーに関わる情報を活用してエラーハンドリングを行います。
AWS サービスとアーキテクチャ
Web API パターンのサーバーレスアプリケーションを AWS 上で構築するためには、HTTP リクエストをハンドリングするWebサーバーとしての役割を果たすサービスと、HTTP リクエストをトリガーに、渡ってきたメッセージを処理するアプリケーションサーバーとしての役割を果たすサービスが利用されます。
Web サーバーとしての役割を果たすサービスとして、最も代表的なサービスは Amazon API Gateway (API Gateway) です。API Gateway は フルマネージドの API の作成、公開、保守、モニタリング、保護を実現するサービスです。一般的なバックエンドサービスを実行する REST API と呼ばれる形式の API を構築する際に利用されます。
「形で考えるサーバーレスパターン」における代表的な適用シーン/ユースケースと実装形のうち、「動的 Web / モバイルバックエンド」、「業務系 API、グループ企業間 API」 で記載されているアーキテクチャで利用されます。
また従来の REST API とは異なる API 呼び出しの手法として、昨今利用されてきている GraphQL を利用した新たな API を実装する際には、 AWS AppSync (AppSync) が利用されます。AppSync はサーバーレス な GraphQL API を構築するサービスです。リアルタイムのチャットアプリケーションのバックエンドとしての GraphQL サーバーを構築する際に利用されます。
「形で考えるサーバーレスパターン」における代表的な適用シーン / ユースケースと実装形のうち、「リアルタイムモバイル / オフライン対応」 で記載されているアーキテクチャで利用されます。
アプリケーションサーバーとしての役割を果たすサービスとして最も代表的なサービスは AWS Lambda です。AWS Lambda はイベント駆動で実行されるサーバーレスなアプリケーション実行環境です。様々な種類のプログラミング言語の任意のコードを実行できるため、多くのユースケースで利用されます。このシリーズで取り扱うほとんどのユースケースにて、AWS Lambda がバックエンドのサービスを実行する環境として利用されます。
これらのサービスを組み合わせてサーバーレスな Web API を構築することが出来ます。
エラーハンドリング
Web API パターンのサーバーレスアプリケーションのエラーハンドリング
Web API パターンのサーバーレスアプリケーションのエラーハンドリングの特徴として、ユーザーの責任でクライアント側でエラーハンドリングを行う点にあります。
クライアントは、自身のアプリケーションロジックによるハンドリングで、リカバリ可能なエラーなのかを区別する必要があります。クライアントのハンドリングでリカバリ可能なエラーの例としては、アプリケーション外で発生するエラーである呼び出しエラーとしての一時的な障害、スロットリングがあります。またアプリケーション内で発生するエラーとしては、不正なペイロードが送られてきた場合があります。これらはクライアントのハンドリングでリカバリ可能なエラーです。
一方クライアントのハンドリングでリカバリが出来ないエラーの例としては、バックエンドのサービスが依存している他のサービスへのアクセス権がなかったり、プログラムのランタイムエラーなどがあります。これらはシステムの管理者による対応が必要なエラーになります。
クライアントのハンドリングでリカバリ可能なエラーの対応策の中でも、特にウェブページからのリクエストや、サーバーロジックからの API 呼び出しのリクエストで発生した一時的な障害、スロットリングなどについては、「サーバーレスアプリケーション開発におけるエラーハンドリング」のサーバーレスエラーハンドリングの手法で紹介した「リトライ」を行う事によって、エラーを正常な処理にすることができる可能性があります。ここではそのリトライにフォーカスをあててご説明します。
クライアントのリトライによるエラーハンドリング
Web API パターンのクライアントで行うべきエラーハンドリングの 1 つとしてリトライがあります。
リトライでは対応出来ない種類のエラーはリクエスト内容やアプリケーションやクラウドの設定の修正が必要なものになるので、ログによる検知・原因の分析後の運用・保守で対応する必要があるのですが、逆に言えばそれ以外のリクエスト頻度の制限 (=スロットリング) や、一時的な障害、ネットワーク起因によるエラーなど一時的なエラーにはリトライで対応します。
リトライにおいて検討すべきポイントは、回数と間隔です。特にリトライ間隔については短時間に何度も繰り返しリクエストを行うと、サーバーに負荷をかけてしまい、正常な処理への復帰を妨害する可能性もあるため、重要です。
リトライ間隔の制御の好ましいプラクティスとしてエクスポネンシャルバックオフ、ジッターがあります。
【エクスポネンシャルバックオフ】
エクスポネンシャルバックオフは指数バックオフとも呼ばれるリトライ手法で、エラー直後にリクエストをリトライする代わりに、指数関数的にスリープ時間を増やしてリトライを行う手法です。
等間隔でリトライを実行すると、ダウン中のサービスにトラフィックが集中していき、復帰に影響を与えてしまう可能性があります。そのため、エラーが返ってくるごとに、初回は 2 秒後、次は 4 秒後、次は 8 秒後と、2 の n 乗ごとにスリープ時間を増やしていくことでリトライの集中を防ぐことができます。
さらなる工夫としてリトライが繰り返され過ぎて、再試行の間隔が長くなりすぎてしまことを避けるために、リトライの待ち時間の間隔に最大値を設定します。これは、上限付きエクスポネンシャルバックオフと呼ばれます。
この図は、リトライの回数が増えるごとに、待ち時間の間隔を上限まで指数関数的に増やしていることを表している図になります。
【ジッター】
エクスポネンシャルバックオフによって、リトライ起因のリクエスト数の増加によるシステム復旧への影響は避けられましたが、同時リクエストによる過負荷や競合がエラーの原因である場合、バックオフでは何度繰り返してもエラーとなってしまいます。その場合同時に複数のリクエストが来ていることが原因でエラーが発生していた場合、何度リトライを行っても同じ原因でエラーが繰り返されてしまいます。
このような同じタイミングのリクエスト及びそのリトライで起きたエラーに対するソリューションとしてジッターがあります。ジッター は直訳すると 「時間のずれ」であり、リトライにある程度のランダム性を追加して、リクエストのタイミングを分散させる手法になります。これにより、同時接続によって起きるエラー及びそのリトライによってエラーが繰り返されるリスクを解消することが出来ます。
エクスポネンシャルバックオフとジッターについては、ジッターを伴うタイムアウト、再試行、およびバックオフのブログ もご覧いただけると理解が深まるかと思います。
リトライによるクライアントのエラーハンドリングの実装方法
エラーハンドリングとして備えるべき機能として、エクスポネンシャルバックオフとジッターを含んだリトライが必要であることについてはご理解いただけたかと思いますが、実際に自前で実装するとなるとハードルを感じてしまう方もいらっしゃるかと思います。
ご安心いただければと思うのですが、クライアントライブラリである AWS SDK を利用することで、エクスポネンシャルバックオフとジッターを含んだリトライを含む API 呼び出しをシンプルに実現することができます。
以下はブラウザ上で動くフロントエンドアプリケーションから API Gateway に対して HTTP リクエストを実行する AWS SDK のサンプルコード になります。
const apigClient = apigClientFactory.newClient({
apiKey: 'API_KEY'
});
apigClient.methodName(params, body, additionalParams)
.then(function(result){
// Add success callback code here.
}).catch( function(result){
// Add error callback code here.
});
API 呼び出しのための HTTP リクエストを実行している箇所は、apigClient.methodName のメソッド呼び出しの箇所となりますが、ここにジッターとエクスポネンシャルバックオフが備わったリトライが実装がされています。
参考までに、AWS SDK の実装であるジッターやエクスポネンシャルバックオフのロジックを含んだ API の呼び出しのコードをご覧いただければと思います。
aws-sdk-js/lib/util.js L880
AWS SDK を利用することで非常にシンプルにサーバーレスエラーハンドリングとしてのベストプラクティスを実装できることがお分かりいただければと思います。
サーバーのエラーハンドリングの実装方法
アプリケーションに到達しない呼び出しエラーであれば、クライアントのみでリトライによるエラーハンドリングの可否について判断できますが、サーバーサイドのアプリケーション内で起きたリトライでハンドリング可能なエラーかどうかは、アプリケーションの中でしか判断することが出来ません。このようなエラーの例として、サーバーサイドのアプリケーションの後続処理として依存しているサービスのスロットリングなどが挙げられます。
サーバーサイドで起きたエラーの中でリトライによって対応できるものは、アプリケーションロジックでそのその判別を行い、判別の結果をレスポンスのなかにそのフラグを含めることで、クライアント側でのリトライ対応の要否について判断させることが出来ます。その際に活用できる情報として Retry-After ヘッダーがあります。これはサーバーサイドからクライアントに対して、「いつリトライしれくれたら正常にレスポンスを返します」というような補足情報を提示する HTTP レスポンスヘッダー情報になります。
以下はサーバーレス環境上で動くサーバーサイドのアプリケーションで、エラーハンドリングを行うサンプルコードになります。このサンプルコードでは、リトライでハンドリング可能なエラーのレスポンスにて、レスポンスヘッダーに Retry-After ヘッダー を付与して、クライアント側でのリトライによるエラーハンドリングの可否の判断に活用できるようなデータを返しています。
const AWS = require('aws-sdk');
const dynamo = new AWS.DynamoDB();
exports.handler = async (event, context) => {
try {
// 正常処理としてイベントを処理する
} catch (error) {
// 異常処理としてエラーの種類に応じてハンドリングする
if (error instanceof TypeError) {
// 不正なエラーペイロード
console.log('Invalid error payload');
return {
statusCode: 400,
body: JSON.stringify({
error: error.message
}),
};
} else if (error instanceof DynamoDB.Errors.ProvisionedThroughputExceededException) {
// DynamoDBのスロットリングエラー
console.log('DynamoDB is throttled');
return {
statusCode: 503,
headers: { "Retry-After": 300 }, // リトライ可否とそのタイミングについての情報を付与するヘッダ
body: JSON.stringify({
error: error.message,
}),
};
} else if (error instanceof DynamoDB.Errors.UnauthorizedException) {
// DynamoDBへのアクセス権がありません
console.log('Unauthorized to access DynamoDB');
return {
statusCode: 500,
body: JSON.stringify({
error: error.message,
}),
};
} else {
// その他のサニタイズしない不明なエラー
console.log('Unhandled Rejection');
throw new Error("Unhandled Rejection")
}
}
};
サンプルコード内の「異常処理としてエラーの種類に応じてハンドリング」を行っている箇所について説明すると、最初の不正なペイロードと権限不足のエラーは、同じリクエストを何度送ってもエラーとなるため、リトライすべきではないエラーとして、Retry-After ヘッダー を付与せずレスポンスを返しています。次に、Amazon DynamoDB のスロットリングによるエラーの場合は、同じリクエストを時間を置くことで成功するため、リトライすべきエラーとしてレスポンスヘッダーに Retry-After ヘッダー を付与してレスポンスを返しています。それ以外のハンドリング不可の関数の失敗であれば、再度例外を投げることで関数自体を失敗させるとともに、開発者がデバッグできるよう問題をログに記録します。
このように、クライアントがリトライによるエラーハンドリングの可否を判断できるようなレスポンスを行うことが、Web API パターンのサーバーレスアプリケーションのエラーハンドリングに求められる手法になります。
リトライとべき等性 (idempotency)
リトライを行うにあたって、大前提として担保されるべき性質があります。それはべき等性 (idempotency) です。べき等性について一言で説明すると、「ある操作を 1 回行っても複数回行っても結果が同じである」ことをいう性質です。
リトライを実装するためには、サーバー側のアプリケーションロジックは何度同じリクエストを処理しても、同じ結果を返す必要があります。このような同一のメッセージを何度処理しても同一の結果になる特性をべき等性といいます。リトライを行う際には、再度実行されるアプリケーションロジックに、べき等性が備わっていることが必須でになります。
べき等性についてはこちらの「サーバーレスが気になる開発者に捧ぐ『べき等性』ことはじめ」シリーズをご覧いただけるとさらに理解が深まると思います。
まとめ
今回は、Web API のユースケースでサーバーレスアプリケーションを利用する際のエラーハンドリングについてご紹介しました。
Web API のユースケースでのエラーハンドリングは、ユーザーの責任でクライアント側でエラーハンドリングを行います。その際に、クライアントはアプリケーションコードや設定の改修が必要なエラーなのか、不要なエラーなのかを区別し、その結果に応じたエラーハンドリングを行うことが求められます。
修正が不要なエラーにおいてアプリケーションコードで吸収できる有効なエラーハンドリングの 1 つがリトライになります。リトライ時の好ましいプラクティスとしてエクスポネンシャルバックオフとジッターがあります。クライアント側でこれらの機能の実装を行い、安全なエラーハンドリングを行う必要がありますが、AWS SDK を利用すると、これらの機能が事前に実装されているため、自前で実装する労力が不要になります。
またサーバー側のエラーでも、それがリトライによってによって解決するエラーかどうかの情報を、エラーハンドリングの一環でレスポンスに含めることで、クライアント側でリトライによるエラーハンドリングの可否を判断することが出来ます。
加えてリトライの実行時に呼ばれるロジックには、「ある操作を 1 回行っても複数回行っても結果が同じである」べき等性が必須であることも押さえていただきたいポイントです。
以上が Web API パターンにおけるエラーハンドリングでした。
次回は「イベント駆動のデータ加工、連携処理」のユースケースにおけるサーバーレスアプリケーションのエラーハンドリングについてご紹介します。
サーバーレス学習のための関連資料
この連載記事のその他の記事はこちら
- 選択
- 第 1 回 オープニング
- 第 2 回 Web API パターン
- 第 3 回 イベント駆動のデータ加工、連携処理パターン
- 第 4 回 マイクロバッチ・ストリーミングパターン
- 第 5 回 ワークフローパターン 前編
- 第 6 回 ワークフローパターン 後編
筆者プロフィール
大磯 直人
アマゾン ウェブ サービス ジャパン合同会社
ソリューションアーキテクト
インターネット・Web サービスを提供されるお客様に対して技術支援を行っています。好きな食べ物は 肉・寿司・ラーメン です。空き時間は永遠に Youtube 見てます。好きな AWS サービスは AWS StepFunctions です。
AWS を無料でお試しいただけます