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
    public class MyDynamoDBClientConfig {
        /**
         * Method to initialize an asynchronous DynamoDB client
         */
        public static AmazonDynamoDB createDynamoDBClient() {
            return AmazonDynamoDBAsyncClient.builder()
                            .withCredentials(new DefaultAWSCredentialsProviderChain())
                            .withClientConfiguration(createDynamoDBClientConfiguration())
                            .build();
        }
    
        /**
         * Method to overwrite the default SDK client configuration behavior
         *
         * @return ClientConfiguration with custom timeout values and retry method
         */
        private static ClientConfiguration createDynamoDBClientConfiguration() {
            ClientConfiguration clientConfiguration = new ClientConfiguration()
                   .withConnectionTimeout(MyDynamoDBClientParameters.connectionTimeout)
                    .withClientExecutionTimeout(MyDynamoDBClientParameters.clientExecutionTimeout)
                    .withRequestTimeout(MyDynamoDBClientParameters.requestTimeout)
                    .withSocketTimeout(MyDynamoDBClientParameters.socketTimeout)
                    .withRetryPolicy(PredefinedRetryPolicies
                            .getDynamoDBDefaultRetryPolicyWithCustomMaxRetries(
                                            MyDynamoDBClientParameters.maxErrorRetries));
    
            DynamoDBUtility.getClientConfiguration(clientConfiguration);
    
            return clientConfiguration;
        }
    } 

以下の項では、レイテンシーを考慮した 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 のしきい値に到達するまで再試行が長引く可能性があることも覚えておいてください。

ClientExecutionTimeoutRequestTimeout の各パラメータは、オペレーションの時間におおよその制限を設定しますが、実際のタイムアウトが生じる数秒後にでもタイマーをアクティブ化できることにも留意してください。これは、大規模な応答を返す 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 回にします。

public class MyDynamoDBClientParameters {
    /**
     * Settings for timeouts
     *
     * Ref:
     * https://github.com/aws/aws-sdk-java/blob/master/aws-java-sdk-core/src/main/java/com/amazonaws/ClientConfiguration.java
     *
     * DEFAULT_CONNECTION_TIMEOUT - 10 s
     * DEFAULT_CLIENT_EXECUTION_TIMEOUT - 0 i.e., disabled
     * DEFAULT_REQUEST_TIMEOUT - 0 i.e., disabled
     * DEFAULT_SOCKET_TIMEOUT - 50 s
     */
    public static int connectionTimeout = 1000; // 1 s
    public static int clientExecutionTimeout = 5000; // 5 s
    public static int requestTimeout = 500; // 500 ms
    public static int socketTimeout = 1000; // 1 s

    /**
     * Settings for back-off and retries
     *
     * Ref:
     * https://github.com/aws/aws-sdk-java/blob/master/aws-java-sdk-core/src/main/java/com/amazonaws/retry/PredefinedRetryPolicies.java
     * https://github.com/aws/aws-sdk-java/blob/master/aws-java-sdk-core/src/main/java/com/amazonaws/retry/PredefinedBackoffStrategies.java
     *
     * DEFAULT_RETRY_CONDITION - For 500 internal server errors,
     * 503 service unavailable errors, 400 throttling errors, and clock skew exception
     * DYNAMODB_DEFAULT_BASE_DELAY - 25 ms for 5XX
     * SDK_DEFAULT_THROTTLED_BASE_DELAY - 500 ms for 4XX errors
     * SDK_DEFAULT_MAX_BACKOFF_IN_MILLISECONDS - 20 s
     * DYNAMODB_DEFAULT_MAX_ERROR_RETRY - 10
     *
     * If these retries are not tight enough, then go further by
     * disabling retries with retry policy NO_RETRY_POLICY and writing your own
     * retry logic, or for the advanced users simply write your own retry policy by
     * extending V2CompatibleBackoffStrategyAdapter at
     * https://github.com/aws/aws-sdk-java/blob/master/aws-java-sdk-core/src/main/java/com/amazonaws/retry/V2CompatibleBackoffStrategyAdapter.java
     */

    /**
     * https://github.com/aws/aws-sdk-java/blob/master/aws-java-sdk-core/src/main/java/com/amazonaws/retry/PredefinedRetryPolicies.java#L50
     */
    public static int maxErrorRetries = 10; // Used the default
}

このセクションでは、アプリケーションレベルのレイテンシーを低減させ、サービスまたはネットワークの中断中にアプリケーションの SLA を維持するように DynamoDB を設定できるアプリケーションユースケース例のシナリオを 2 件ご紹介します。

ショッピングカートへのアイテムの追加

仮定上の e コマースウェブアプリケーションの例を検討してみましょう。このアプリケーションのマイクロサービスモジュールは、単一のアイテムをカスタマーのショッピングカートに追加する責任を担っています。ビジネス上の観点から、カスタマーのカートにアイテムを追加する機会は絶対に逃したくありません。このため、単一の DynamoDB PutItem コールが関与するこの非ストリーミングオペレーションのレイテンシー SLA は、最小で 20 ミリ秒に設定されます。

ここで、この記事で説明した ClientConfiguration パラメータを一切上書きすることなく、以下のコードにあるように、Java クラスで DynamoDB クライアントを作成したと想像してください。この場合、すべてのデフォルト値が適用されます。非同期 DynamoDB クライアントが、接続タイムアウト値 10 秒、ソケットタイムアウト値 50 秒、最大 10 回のエラー再試行で作成されます。

AmazonDynamoDB dynamoDBClient = AmazonDynamoDBClientBuilder.defaultClient();

開発環境でプロトタイプをテストして評価する場合はこの設定で十分ですが、本番環境では、短時間のサービス停止、ネットワークフリップが発生した、またはこの API を呼び出すアプリケーションモジュールが反応しない場合に、マイクロサービスモジュールとそれに関連するダウンストリームアプリケーションを保護する方法が必要です。クライアントの実行タイムアウトとリクエストタイムアウトがデフォルトで無効化されていることを考えると、デフォルトのクライアント設定は、全体的なアプリケーションレイテンシーを急速に増加させることによって事態を悪化させる可能性があります。これは、10 回のデフォルト再試行を完了するまでの時間が経過する間、連鎖的に一時的な障害を伝播する場合があります。

では、より堅牢な設定を検討しましょう。この例では、以下のコード例にあるように DynamoDB クライアントを設定します。各 PutItem コールは 20 秒以内に完了しなければなりません。コールがスロットリングされる場合は、5 回再試行します。クライアントは接続の確立を最大 50 ミリ秒待ち、ソケットが 25 ミリ秒たってもデータの転送を行っている場合はタイムアウトします。最後に、最悪の事態では、クライアントが 100 ミリ秒後にこのオペレーションの実行を終了し、その発信元に戻します。

public class MyDynamoDBClientParameters {
    public static int connectionTimeout = 50; // 50 ms
    public static int clientExecutionTimeout = 100; // 100 ms
    public static int requestTimeout = 20; // 20 ms
    public static int socketTimeout = 25; // 25 ms
    public static int maxErrorRetries = 5;
}

以下のコード例は、前のセクションで作成した MyDynamoDBClientConfig Java クラスを使って、Java で DynamoDB クライアントオブジェクト (dynamoDBClient) を作成します。これは、低レベルクライアントと、高レベルの API インタラクションのための DynamoDB ドキュメント API クライアントオブジェクト (dynamoDB) を作成します。クライアントの設定パラメータは、MyDynamoDBClientParameters クラスを通じて投入されます。

public class DynamoDBClientConfigTest {
    // Logger initialization
    public final static Logger LOG = LogManager.getLogger(DynamoDBClientConfigTest.class);

    public static AmazonDynamoDB dynamoDBClient;
    public static DynamoDB dynamoDB;

    /**
     * Initializes the DynamoDB client
     */
    public static void init() {
        try {
            dynamoDBClient = MyDynamoDBClientConfig.createDynamoDBClient();
            dynamoDB = new DynamoDB(dynamoDBClient);

        } catch(Exception e) {
            DynamoDBClientConfigTest.LOG.error(DynamoDBExceptionHandler.handleException(e));
        }
    }
}

BatchWriteItem の使用によるゲームステートの保存

もうひとつの非ストリーミングオペレーションシナリオについて検討してみましょう。ここでは、MMOG アプリケーションの状態をリアルタイムで保存します。これには、DynamoDB テーブルへの BatchWriteItem という形での GetItem コールと PutItem コールの組み合わせが必要です。このシナリオでは、デフォルトクライアントの作成が厄介になる可能性があります。リアルタイムの MMOG アプリケーションでは、ゲームステートの保存が他のいくつかのステート移行をトリガーし、その変更を他のオンラインユーザーに伝播します。その結果、アプリケーションモジュールは、一時的なサービスまたはネットワークの問題がある場合、またはアプリケーション自体を使用する他のカスタマーが引き起した遅延でさえも、そのダウンストリームモジュールのすべてに深刻な影響を与えかねません。このような状況に対処するには、クライアント実行タイムアウトを 1 秒未満にし、エラー時における内部再試行の回数を少なくしてクライアントを設定する必要があります。最悪の事態では、アプリケーションモジュールがすべての再試行に失敗する場合、アプリケーションが一時的な障害を乗り切る間に、ゲーマーがダウンストリームのアプリケーション内モジュールに影響を及ぼすことなく再試行を実行できます。

AmazonDynamoDB dynamoDBClient = AmazonDynamoDBClientBuilder.defaultClient();

このユースケースには、クライアントが BatchWriteItem コールのための応答の送信と受信に 500 ミリ秒以上の時間を費やす場合、クライアントを以下のコード例にあるように設定できます。さらに、失敗したリクエストはいずれも、クライアントが 1 秒後にタイムアウトする前に、3 回再試行されます。ネットワークパケットの送信または受信が 550 ミリ秒以内に行われなかった場合は、基盤となるソケットもタイムアウトします。いつものように、ネットワーク関連の問題がある場合は、50 ミリ秒後に接続の確立を中止します。

public class MyDynamoDBClientParameters {  
    public static int connectionTimeout = 50; // 50 ms
    public static int clientExecutionTimeout = 1000; // 1 s
    public static int requestTimeout = 500; // 500 ms
    public static int socketTimeout = 550; // 550 ms
    public static int maxErrorRetries = 3;
}

まとめ

この記事では、ユースケースとアプリケーション定義の SLA の両方に基づいて、AWS Java SDK を使いながら DynamoDB クライアント設定を調整する方法について説明しました。DynamoDB のために基盤となる HTTP クライアントの SDK パラメータを調整するには、アプリケーションの平均的なレイテンシー要件とレコード特性 (アイテムの数とそれらの平均サイズなど)、異なるアプリケーションモジュールまたはマイクロサービス間における依存関係、およびデプロイメントプラットフォームに関する知識が必要です。慎重なアプリケーション API 設計、適切なタイムアウト値、および再試行戦略は、避けることができないネットワーク側とサーバー側の問題に対してアプリケーションを備えることができます。


著者について

Joarder の写真Joarder Kamal は AWS クラウドサポートエンジニアです。Joarder は、分散型通信、リアルタイムデータ、および集団的知性を組み合わせたシステムを構築して自動化することが好きです。
 

 

   Sean Shriver はダラスを拠点とし、DynamoDB に焦点を当てるシニア NoSQL スペシャリストソリューションアーキテクトです。