Amazon Web Services ブログ
レイテンシーを考慮した Amazon DynamoDB アプリケーションのための AWS Java SDK HTTP リクエスト設定の調整
Amazon DynamoDB は、大規模に実行されるアプリケーションとサービスに低レイテンシーと高スループットのパフォーマンスを提供するために設計された NoSQL クラウドデータベースサービスです。ユースケースの例には以下が含まれます。
- 大規模多人数参加型オンラインゲーム (MMOG)
- バーチャルリアリティとオーグメントリアリティ
- e コマースでのチェックアウトと注文処理
- リアルタイムの株価情報と売買
そのようなシステムをグローバルで運営していると、時折レイテンシースパイクが発生することがあります。これらのスパイクは、一時的なネットワーク中断による再試行、サービス側およびネットワーク側の問題、または過剰な負荷がかかった応答が遅いクライアントが原因で発生する場合があります。
根本的な原因にかかわらず、DynamoDB サービスとやり取りするアプリケーションは、レイテンシースパイクを回避するために役立つ再試行戦略に従うよう調整しておく必要があります。使用している AWS SDK に応じて、基盤となる HTTP クライアントの動作は、HTTP を経由する低レベルのクライアント対サーバー通信がアプリケーションのレイテンシー要件に従うことができるように、デフォルト設定を設定し直すことが可能です。このブログ記事では、レイテンシーを考慮した DynamoDB クライアントのための、HTTP リクエストのタイムアウトと再試行動作の微調整に利用できる AWS Java SDK 設定オプションについて説明します。また、適切な設定のメリットを実証するために、2 つの仮定上のアプリケーションシナリオについても説明します。
DynamoDB クライアントのための AWS Java SDK HTTP 設定
AWS Java SDK は、HTTP クライアントの動作と再試行戦略に対する完全な制御を提供します。標準 HTTP 設定の情報については、「クライアント側の設定」を参照してください。一方で、レイテンシーを考慮した DynamoDB アプリケーションクライアントを構築するために必要なより詳しい設定は、ClientConfiguration (JavaDocs) コード実装にあります。
このブログ記事では、非同期 DynamoDB クライアント を Java でゼロから構築し、AWS SDK からの ClientConfiguration
実装を使ってアプリケーション固有のレイテンシー要件を定義する方法をご紹介します。この例では、非同期 DynamoDB クライアントを作成します。このクライアントは、サービスエンドポイントに対して複数の順次 DynamoDB API コールを行うことができ、応答が返されるのを待たずに次の API コールを発行します。DynamoDB アプリケーションをレイテンシーに敏感なものにしたいため、非同期クライアントアプリケーションは適切な選択です。これは、異なるモジュールまたはマイクロサービスからバックエンドへの API コールの増加に備え、それらを処理することができるので、個々の実行プロセスが切り離されます。
まず、関数 createDynamoDBClient()
および createDynamoDBClientConfiguration()
で MyDynamoDBClientConfig
という名前の Java クラスを作成します。
createDynamoDBClient()
関数は、createDynamoDBClientConfiguration()
プライベート API のオペレーションによって返された ClientConfiguration
オブジェクトからの低レベル HTTP クライアント設定を使用する、非同期 DynamoDB クライアントオブジェクトを返します。以下のコード例からわかるように、ClientConfiguration
オブジェクトの作成中に 5 つの HTTP クライアント設定パラメータが設定されます。
- ConnectionTimeout
- ClientExecutionTimeout
- RequestTimeout
- SocketTimeout
- the DynamoDB default retry policy for HTTP API calls with a custom maximum error retry count
以下の項では、レイテンシーを考慮した DynamoDB クライアントアプリケーションの作成時におけるこれらのクライアント設定パラメータの重要性を説明するために、これらのパラメータの詳細を挙げていきます。
ConnectionTimeout
ConnectionTimeout は、この SDK で、DynamoDB エンドポイントとの TCP 接続を確立するためにクライアントが基盤となる HTTP クライアントを待つ最大時間です。この接続は、クライアントとサーバー間におけるエンドツーエンドの双方向通信リンクで、API コールを実行し、応答を受け取るために何度も使用されます。この設定のデフォルト値は 10 秒です。TCP および TLS でのソケットの確立時間が 10 秒を超える場合は、ネットワークパス、パケット損失、またはその他制御不能な詳細不明の問題に関連するより重大な問題が存在する可能性があります。
ClientExecutionTimeout
ClientExecutionTimeout は、エンドツーエンドオペレーションを実行し、希望する応答を受け取るために費やすことができる最大合計時間で、これには発生する可能性がある再試行も含まれます。実質上、これは DynamoDB オペレーションの SLA、つまり再試行を含めたすべての HTTP リクエストを完了するための時間枠です。
ClientExecutionTimeout
は、アプリケーションレベルのオペレーションの全体的な実行時間を制御します。(個々の HTTP リクエストの動作を制御したい場合は、次に説明する RequestTimeout
オプションを使用できます。) デフォルトの HTTP クライアント設定では、ClientExecutionTimeout
はデフォルトで無効化されています。ただし、オペレーションのアプリケーション SLA を定義し、制御する上でのこの設定の重要性に基づくと、これを適切な値に設定して、DynamoDB からの応答待ちにおける最悪の事態をコントロールするために役立てるべきです。例えば、アプリケーション固有の非ストリーミングオペレーションについて考えられる最長のブロック時間を推定して使用することができます。
RequestTimeout
RequestTimeout は、クライアントが単一の HTTP リクエストを実行するためにかかる時間です。RequestTimeout
は DynamoDB API コール (PutItem
または GetItem
など) が実行された瞬間から、サービスからの応答を受け取るまでの時間から測定されます。論理的に、このタイムアウトの値は ClientExecutionTimeout
よりも短くなるはずです。ClientExecutionTimeout
と同様に、この設定はデフォルトで無効化されています。合理的なリクエストタイムアウト値を推定するときは、過剰な値に設定しないように注意してください。例えば、設定する値が低すぎると、トランスポートレイヤーにおける TCP パケットの損失と、それに続く再送信が関わる、ささいで一時的なネットワーク障害でさえも、リクエストが失敗する原因になり得ます。また、RequestTimeout
をデフォルトのまま (無効) にしておく場合は、ClientExecutionTimeout
(設定されている場合) または SocketTimeout
のしきい値に到達するまで再試行が長引く可能性があることも覚えておいてください。
ClientExecutionTimeout
と RequestTimeout
の各パラメータは、オペレーションの時間におおよその制限を設定しますが、実際のタイムアウトが生じる数秒後にでもタイマーをアクティブ化できることにも留意してください。これは、大規模な応答を返す API コールの中止が、タイムアウト発生後数秒かかる可能性があることを意味します。SDK レベルでは、これら 2 つの設定のいずれかが有効化されるときにスレッドプールが作成され、リクエストコンテキストおよびクライアントコンテキストでタイマーを監視するために、すべてのリクエストスレッド全体で最大 5 つのスレッドが使用されます。RequestTimeout
設定とともに ClientExecutionTimeout
も設定して ClientExecutionTimeout
を上位レベルの保護対策として機能させ、実際のシナリオに基づいて単一の HTTP リクエストの個々の再試行にかかる時間を概算できるようにすることをお勧めします。
SocketTimeout
SocketTimeout は、すでに確立された TCP 接続からデータを読み取るために HTTP クライアントが待つ最大時間です。これは、HTTP POST が終了してから、リクエストの全応答が受け取られるまでの時間で、サービスとネットワークの往復時間が含まれます。ソケットがハングアップする (例えば、I/O 例外によるもの) というような特定のケースにおいて、この設定はクライアントが長時間ブロックすることを防ぎます。これを RequestTimeout
と併用する場合は、それよりもこの値を少し高く設定することが一般的な推奨です。BatchWriteItem
および BatchGetItem
などのオペレーションのベストプラクティスは、SocketTimeout
を 5,500 ミリ秒などの高い値に設定することです。高い値は、サービス側でひとつ、または複数のアイテムに問題がある場合に、レコードが UnprocessedItems として返されることを確実にするために役立つからです。DynamoDB は 5 秒後に個々のアイテムの読み取りまたは書き込みオペレーションをタイムアウトしますが、サービスの応答を待つ時間がそれより短いと、どのアイテムに問題があるかがわからないことから、バッチオペレーションには高い値が推奨されます。DynamoDB にアイテムを未処理アイテムとして返させることのメリットは、その後でクライアントが、バッチ全体ではなく、失敗したアイテムを再試行できることです。
カスタマイズされた最大再試行設定を使用する DynamoDB のデフォルト再試行ポリシー
DynamoDB 向けの AWS Java SDK で利用できるデフォルト再試行ポリシーは、基盤となる HTTP クライアント用にクライアント側の再試行戦略を定義するために格好の出発点です。デフォルトのポリシーは、5XX 系のサーバー側例外(「HTTP status code – 500 Internal Server Error」または「HTTP status code – 503 Service Unavailable」など) に対する 25 ミリ秒、および 4XX 系のサーバー側例外 (「HTTP status code 400 – ProvisionedThroughputExceededException」など) に対する 500 ミリ秒の、事前定義されたベース遅延での再試行最大 10 回から開始されます。
PredefinedBackoffStrategies
(JavaDocs) には、これらの再試行で使用される 2 つの事前定義されたバックオフ戦略が含まれています。スロットリングされていない 5XX リクエストには FullJitterBackoffStrategy
が選択されます。これは 25 ミリ秒のベース遅延を使用し、最大遅延は 20 秒になります。スロットリングされた 4XX リクエストには EqualJitterBackoffStrategy
が使用され、500 ミリ秒のベース遅延から開始されます。これは最大 20 秒にすることができ、20,000 ミリ秒に達するまで、500 ミリ秒、1,000 ミリ秒、2,000 ミリ秒というように指数関数的に増加します。(これら 2 つの戦略は、AWS Architecture ブログの Exponential Backoff and Jitter で詳しく説明されています。)
アプリケーションで DynamoDBMapper クラスを使用する場合、先ほど説明したデフォルトの ClientConfiguration
オプションを使って、内部でクライアントが開始されます。さらにこのクラスは、BatchWriteItem
API コールからの未処理アイテムに、独自の再試行メカニズムである DefaultBatchWriteRetryStrategy
も使用します。これには、1 秒の最小遅延と 3 秒の最大遅延、およびエクスポネンシャルバックオフ戦略が含まれます。このため、デフォルト設定での DynamoDBMapper
クラスの使用は、リクエストに予期しない余分な遅延を追加する可能性があります。つまり、最大限のコントロールを維持するためにも、可能な場合は常に低レベル DynamoDB API オペレーションを使用し、その後 SDK レベルの設定を微調整して本番におけるアプリケーションの動作を定義する必要があります。
最後に、デフォルトの戦略がユースケースに対応しない場合は、NO_RETRY_POLICY
でデフォルトの再試行オプションを無効化します。これは、ClientConfiguration
の作成時にそれを RetryPolicy
(PredefinedRetryPolicies.NO_RETRY_POLICY
) で指定することによって行います。また、V2CompatibleBackoffStrategyAdapter クラスを拡張することによって、独自の再試行ロジックを実装することも可能です。ベストプラクティスとして、5XX 系のサーバー側例外はより速い速度で再試行するようにしてください。これらの種類の問題は、通常一時的だからです。例えば、ベース遅延 25 ミリ秒、最大 1 秒までの直線的な増加から始めるとよいでしょう。同様に、4XX 系のクライアント側例外も、ベース遅延 100 ミリ秒、最大遅延 500 ミリ秒で始めます。4XX 系のクライアント側例外では、DynamoDB テーブルのキャパシティーを十分に活用するために、常にスロットルされたリクエストを後の 1 秒にまわしてみるようにしてください。どちらの場合も、実行する再試行の回数は、実際のユースケースと独自の判断次第です。
再試行スロットリング
ThrottledRetries は、フェイルファストする (つまり、必要に応じてフェイルオーバーすることが可能な、長い間続いているサーバー側の障害を検知する) ために使用できるもうひとつの有用な ClientConfiguration
パラメータです。この機能は、ClientConfiguration で、デフォルトで有効化されています。この SDK では、AmazonHttpClient クラスに有限サイズの再試行プールが維持されており、各再試行リクエストがこのプールから特定のキャパシティーを消費し、最終的にはそのすべてが消費されます。このプールのデフォルトサイズは再試行 100 回です。スロットリングが開始され、クライアントが正常な再試行リクエストを行えなくなるまでの実際の再試行の回数は、RetryPolicy
で定義された戦略に応じて異なります。サーバー側の問題が解決されると、プールは再度満杯になり、再試行リクエストが実行されます。再試行スロットリングは、5XX HTTP レスポンスコードでの再試行の試みの失敗回数が増加している場合にのみ開始されます。これは、一時的な再試行が再試行スロットリングの影響を受けないことを意味します。ThrottledRetries
はサーキットブレーカーではないため、サービスエンドポイントに対する新しいリクエストは停止されません。一般的な推奨は、このパラメータをデフォルトのままにしておくことです。maxErrorRetries
(前述) の値を低くする場合は (例えば、1 回または 2 回の再試行)、ThrottledRetries
を無効化して再試行を正しく処理する、またはカスタム再試行ロジックを利用するようにしてください。
以下のコード例では、HTTP クライアント設定パラメータをそれぞれの値で定義するために MyDynamoDBClientParameters
という名前の Java クラスを作成しました。このクラスはその後、先ほど作成した MyDynamoDBClientConfig
クラスの createDynamoDBClientConfiguration()
関数に投入できます。
この例では、アプリケーションオペレーションに複数の BatchWriteItem
コールが関与し、HTTP クライアントが 5 秒後にタイムアウトする (clientExecutionTimeout
を使用) と仮定します。また、クライアントが 1 秒以内に接続を確立できない場合にタイムアウト (connectionTimeout
を使用)、または確立された接続が 3 秒以上アイドル状態である場合にタイムアウト (socketTimeout
を使用) できるとも仮定します。最後に、単一の BatchWriteItem
リクエストが、処理するアイテムの数に基づいて完了までに最大 500 ミリ秒かかる (requestTimeout
を使用) と仮定し、デフォルトの再試行回数は 10 回にします。
このセクションでは、アプリケーションレベルのレイテンシーを低減させ、サービスまたはネットワークの中断中にアプリケーションの SLA を維持するように DynamoDB を設定できるアプリケーションユースケース例のシナリオを 2 件ご紹介します。
ショッピングカートへのアイテムの追加
仮定上の e コマースウェブアプリケーションの例を検討してみましょう。このアプリケーションのマイクロサービスモジュールは、単一のアイテムをカスタマーのショッピングカートに追加する責任を担っています。ビジネス上の観点から、カスタマーのカートにアイテムを追加する機会は絶対に逃したくありません。このため、単一の DynamoDB PutItem
コールが関与するこの非ストリーミングオペレーションのレイテンシー SLA は、最小で 20 ミリ秒に設定されます。
ここで、この記事で説明した ClientConfiguration
パラメータを一切上書きすることなく、以下のコードにあるように、Java クラスで DynamoDB クライアントを作成したと想像してください。この場合、すべてのデフォルト値が適用されます。非同期 DynamoDB クライアントが、接続タイムアウト値 10 秒、ソケットタイムアウト値 50 秒、最大 10 回のエラー再試行で作成されます。
開発環境でプロトタイプをテストして評価する場合はこの設定で十分ですが、本番環境では、短時間のサービス停止、ネットワークフリップが発生した、またはこの API を呼び出すアプリケーションモジュールが反応しない場合に、マイクロサービスモジュールとそれに関連するダウンストリームアプリケーションを保護する方法が必要です。クライアントの実行タイムアウトとリクエストタイムアウトがデフォルトで無効化されていることを考えると、デフォルトのクライアント設定は、全体的なアプリケーションレイテンシーを急速に増加させることによって事態を悪化させる可能性があります。これは、10 回のデフォルト再試行を完了するまでの時間が経過する間、連鎖的に一時的な障害を伝播する場合があります。
では、より堅牢な設定を検討しましょう。この例では、以下のコード例にあるように DynamoDB クライアントを設定します。各 PutItem
コールは 20 秒以内に完了しなければなりません。コールがスロットリングされる場合は、5 回再試行します。クライアントは接続の確立を最大 50 ミリ秒待ち、ソケットが 25 ミリ秒たってもデータの転送を行っている場合はタイムアウトします。最後に、最悪の事態では、クライアントが 100 ミリ秒後にこのオペレーションの実行を終了し、その発信元に戻します。
以下のコード例は、前のセクションで作成した MyDynamoDBClientConfig
Java クラスを使って、Java で DynamoDB クライアントオブジェクト (dynamoDBClient
) を作成します。これは、低レベルクライアントと、高レベルの API インタラクションのための DynamoDB ドキュメント API クライアントオブジェクト (dynamoDB
) を作成します。クライアントの設定パラメータは、MyDynamoDBClientParameters
クラスを通じて投入されます。
BatchWriteItem の使用によるゲームステートの保存
もうひとつの非ストリーミングオペレーションシナリオについて検討してみましょう。ここでは、MMOG アプリケーションの状態をリアルタイムで保存します。これには、DynamoDB テーブルへの BatchWriteItem
という形での GetItem
コールと PutItem
コールの組み合わせが必要です。このシナリオでは、デフォルトクライアントの作成が厄介になる可能性があります。リアルタイムの MMOG アプリケーションでは、ゲームステートの保存が他のいくつかのステート移行をトリガーし、その変更を他のオンラインユーザーに伝播します。その結果、アプリケーションモジュールは、一時的なサービスまたはネットワークの問題がある場合、またはアプリケーション自体を使用する他のカスタマーが引き起した遅延でさえも、そのダウンストリームモジュールのすべてに深刻な影響を与えかねません。このような状況に対処するには、クライアント実行タイムアウトを 1 秒未満にし、エラー時における内部再試行の回数を少なくしてクライアントを設定する必要があります。最悪の事態では、アプリケーションモジュールがすべての再試行に失敗する場合、アプリケーションが一時的な障害を乗り切る間に、ゲーマーがダウンストリームのアプリケーション内モジュールに影響を及ぼすことなく再試行を実行できます。
このユースケースには、クライアントが BatchWriteItem
コールのための応答の送信と受信に 500 ミリ秒以上の時間を費やす場合、クライアントを以下のコード例にあるように設定できます。さらに、失敗したリクエストはいずれも、クライアントが 1 秒後にタイムアウトする前に、3 回再試行されます。ネットワークパケットの送信または受信が 550 ミリ秒以内に行われなかった場合は、基盤となるソケットもタイムアウトします。いつものように、ネットワーク関連の問題がある場合は、50 ミリ秒後に接続の確立を中止します。
まとめ
この記事では、ユースケースとアプリケーション定義の SLA の両方に基づいて、AWS Java SDK を使いながら DynamoDB クライアント設定を調整する方法について説明しました。DynamoDB のために基盤となる HTTP クライアントの SDK パラメータを調整するには、アプリケーションの平均的なレイテンシー要件とレコード特性 (アイテムの数とそれらの平均サイズなど)、異なるアプリケーションモジュールまたはマイクロサービス間における依存関係、およびデプロイメントプラットフォームに関する知識が必要です。慎重なアプリケーション API 設計、適切なタイムアウト値、および再試行戦略は、避けることができないネットワーク側とサーバー側の問題に対してアプリケーションを備えることができます。
著者について
Joarder Kamal は AWS クラウドサポートエンジニアです。Joarder は、分散型通信、リアルタイムデータ、および集団的知性を組み合わせたシステムを構築して自動化することが好きです。
Sean Shriver はダラスを拠点とし、DynamoDB に焦点を当てるシニア NoSQL スペシャリストソリューションアーキテクトです。