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

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

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

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

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

  • MOVED リダイレクトをサーバーから受信した。例えば、フェイルオーバー時に元のプライマリノードのスロットをすべてレプリカに引き継ぐ場合や、リシャーディング時にスロットがソースプライマリノードからターゲットプライマリノードに移動される場合が該当します。

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

クラスタートポロジーによっては、CLUSTER SLOT コマンドの応答のサイズがクラスターのサイズに応じて変わる場合があります。ノード数が多い大きなクラスターは、応答も大きくなります。したがって、クラスタートポロジー検出を行うクライアントの数が、際限なく増えないようにすることが重要です。例えば、クライアントアプリケーションの起動時やサーバーとの接続の切断時にクラスター検出を実行しなければならない場合に、クライアントアプリケーションで再接続や検出のリクエストを複数回行い、再試行時のエクスポネンシャルバックオフが実装されていないという間違いがよく見受けられます。そのせいで CPU 使用率が 100% になり、そのまま Valkey または Redis OSS サーバーが長時間応答しなくなる可能性があります。各 CLUSTER SLOT コマンドでクラスターバス内の多数のノードを処理しなければならない場合は、こうした停止状態が長引きます。このような動作が原因でクライアントが停止する事態は、これまでも、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(); } } }