クラスタークライアント検出とエクスポネンシャルバックオフ (Valkey と Redis OSS) - Amazon ElastiCache

翻訳は機械翻訳により提供されています。提供された翻訳内容と英語版の間で齟齬、不一致または矛盾がある場合、英語版が優先します。

クラスタークライアント検出とエクスポネンシャルバックオフ (Valkey と Redis OSS)

クラスターモードで ElastiCache Valkey または Redis OSSクラスターに接続する場合、対応するクライアントライブラリはクラスターを認識している必要があります。クライアントは、適切なノードにリクエストを送信し、クラスターのリダイレクト処理によるパフォーマンスのオーバーヘッドを回避できるように、ハッシュスロットとクラスター内のノードを対応付けたマップを取得する必要があります。そのため、クライアントは以下の 2 つの異なる状況下で、スロットとマッピング先のノードを網羅したリストを検出する必要があります。

  • クライアントが初期化され、初期スロット構成を読み込む必要がある。

  • レプリカによって以前のプライマリノードによって処理されたすべてのスロットが引き継がれるフェイルオーバーの状況や、スロットがソースプライマリからターゲットプライマリノードに移動されるときに再シャーディングなど、サーバーからMOVEDリダイレクトを受信します。

クライアント検出は通常、Valkey または Redis OSSサーバーに CLUSTERSLOTまたは CLUSTER NODE コマンドを発行することで行われます。スロット範囲のセットと、関連付けられたプライマリノードとレプリカノードをクライアントに返すため、 CLUSTERSLOTメソッドをお勧めします。その場合、クライアント側で別途解析を行う必要がなく、効率が向上します。

クラスタートポロジによっては、CLUSTERSLOTコマンドのレスポンスのサイズがクラスターのサイズによって異なる場合があります。ノード数が多い大きなクラスターは、応答も大きくなります。したがって、クラスタートポロジー検出を行うクライアントの数が、際限なく増えないようにすることが重要です。例えば、クライアントアプリケーションの起動時やサーバーとの接続の切断時にクラスター検出を実行しなければならない場合に、クライアントアプリケーションで再接続や検出のリクエストを複数回行い、再試行時のエクスポネンシャルバックオフが実装されていないという間違いがよく見受けられます。これにより、Valkey または Redis OSSサーバーが長期間応答しなくなり、CPU使用率が 100% になる可能性があります。各CLUSTERSLOTコマンドがクラスターバス内の多数のノードを処理する必要がある場合、停止は延長されます。この動作により、Python (redis-py-cluster) や Java (Lettuce や Redisson) など、さまざまな言語にわたって、過去に複数のクライアント停止が発生しています。

サーバーレスキャッシュでは、アドバタイズされるクラスタートポロジが静的であり、書き込みエンドポイントと読み取りエンドポイントの 2 つのエントリで構成されるため、こうした問題の多くは自動的に軽減されます。また、キャッシュエンドポイントを使用する場合、クラスター検出が自動的に複数のノードに分散されます。ただし、以下の推奨事項は引き続き有効です。

接続リクエストや検出リクエストが殺到した場合の影響を軽減するために、以下の対応を推奨します。

  • クライアントアプリケーションからの同時着信接続数を制限するために、有限サイズのクライアント接続プールを実装する。

  • タイムアウトによりクライアントがサーバーから切断された場合は、エクスポネンシャルバックオフとジッター(揺らぎ) を加えて再試行する。これにより、複数のクライアントが同時にサーバーに負荷をかける事態を阻止できます。

  • での接続エンドポイントの検索 ElastiCache」のガイドを参考にして、クラスターエンドポイントを検索し、クラスター検出を実行する。これにより、クラスター内のハードコーディングされたいくつかのシードノードにアクセスする代わりに、検出の負荷をクラスター内のすべてのノード (最大 90 個) 間で分散できます。

以下は、redis-py、、PHPRedisおよび Lettuce の指数バックオフ再試行ロジックのコード例です。

バックオフロジックのサンプル 1: redis-py

redis-py には、障害の発生直後に 1 回再試行する再試行メカニズムが組み込まれています。このメカニズムは、Redis OSS オブジェクトの作成時に指定されたretry_on_timeout引数を使用して有効にできます。ここでは、エクスポネンシャルバックオフとジッターを加えたカスタムの再試行メカニズムを紹介します。redis-py (#1494) で、エクスポネンシャルバックオフをネイティブに実装するプルリクエストを送信しました。今後は、手動で実装する必要がなくなる可能性があります。

def run_with_backoff(function, retries=5): base_backoff = 0.1 # base 100ms backoff max_backoff = 10 # sleep for maximum 10 seconds tries = 0 while True: try: return function() except (ConnectionError, TimeoutError): if tries >= retries: raise backoff = min(max_backoff, base_backoff * (pow(2, tries) + random.random())) print(f"sleeping for {backoff:.2f}s") sleep(backoff) tries += 1

その後、以下のコードを使用して値を設定できます。

client = redis.Redis(connection_pool=redis.BlockingConnectionPool(host=HOST, max_connections=10)) res = run_with_backoff(lambda: client.set("key", "value")) print(res)

ワークロードに応じて、例えば、レイテンシーの影響を受けやすいワークロードについては、バックオフの基準値を 1 秒から数十ミリ秒または数百ミリ秒に変更することができます。

バックオフロジックサンプル 2: PHPRedis

PHPRedis には、最大 10 回 (設定不可) 再試行する再試行メカニズムが組み込まれています。試行間隔の遅延を設定できます (2 回目以降にジッターを加えます)。詳細については、こちらのサンプルコードを参照してください。エクスポネンシャルバックオフを PHPredis (#1986) にネイティブに実装するプルリクエストを送信しました。このリクエストは、 がマージされて文書化されています。の最新リリースの ではPHPRedis、手動で を実装する必要はありませんが、以前のバージョンの のリファレンスをここに含めました。差し当たり、再試行メカニズムの遅延を設定するコード例を以下に紹介します。

$timeout = 0.1; // 100 millisecond connection timeout $retry_interval = 100; // 100 millisecond retry interval $client = new Redis(); if($client->pconnect($HOST, $PORT, $timeout, NULL, $retry_interval) != TRUE) { return; // ERROR: connection failed } $client->set($key, $value);

バックオフロジックのサンプル 3: Lettuce

Lettuce には、エクスポネンシャルバックオフとジッターの投稿で説明したエクスポネンシャルバックオフ戦略に基づく再試行メカニズムが組み込まれています。以下は、フルジッターのアプローチを示すコードの抜粋です。

public static void main(String[] args) { ClientResources resources = null; RedisClient client = null; try { resources = DefaultClientResources.builder() .reconnectDelay(Delay.fullJitter( Duration.ofMillis(100), // minimum 100 millisecond delay Duration.ofSeconds(5), // maximum 5 second delay 100, TimeUnit.MILLISECONDS) // 100 millisecond base ).build(); client = RedisClient.create(resources, RedisURI.create(HOST, PORT)); client.setOptions(ClientOptions.builder() .socketOptions(SocketOptions.builder().connectTimeout(Duration.ofMillis(100)).build()) // 100 millisecond connection timeout .timeoutOptions(TimeoutOptions.builder().fixedTimeout(Duration.ofSeconds(5)).build()) // 5 second command timeout .build()); // use the connection pool from above example } finally { if (connection != null) { connection.close(); } if (client != null){ client.shutdown(); } if (resources != null){ resources.shutdown(); } } }