

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

# クラスターのクライアント検出とエクスポネンシャルバックオフ (Valkey および Redis OSS)
<a name="BestPractices.Clients.Redis.Discovery"></a>

クラスターモードを有効にして ElastiCache Valkey または Redis OSS クラスターに接続する場合、該当するクライアントライブラリがクラスター対応であることが必要です。クライアントは、適切なノードにリクエストを送信し、クラスターのリダイレクト処理によるパフォーマンスのオーバーヘッドを回避できるように、ハッシュスロットとクラスター内のノードを対応付けたマップを取得する必要があります。そのため、クライアントは以下の 2 つの異なる状況下で、スロットとマッピング先のノードを網羅したリストを検出する必要があります。
+ クライアントが初期化され、初期スロット構成を読み込む必要がある。
+ MOVED リダイレクトをサーバーから受信した。例えば、フェイルオーバー時に元のプライマリノードのスロットをすべてレプリカに引き継ぐ場合や、リシャーディング時にスロットがソースプライマリノードからターゲットプライマリノードに移動される場合が該当します。

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

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

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

接続リクエストや検出リクエストが殺到した場合の影響を軽減するために、以下の対応を推奨します。
+ クライアントアプリケーションからの同時着信接続数を制限するために、有限サイズのクライアント接続プールを実装する。
+ タイムアウトによりクライアントがサーバーから切断された場合は、エクスポネンシャルバックオフとジッター(揺らぎ) を加えて再試行する。これにより、複数のクライアントが同時にサーバーに負荷をかける事態を阻止できます。
+ 「[ElastiCache での接続エンドポイントの検索](Endpoints.md)」のガイドを参考にして、クラスターエンドポイントを検索し、クラスター検出を実行する。これにより、クラスター内のハードコーディングされたいくつかのシードノードにアクセスする代わりに、検出の負荷をクラスター内のすべてのノード (最大 90 個) 間で分散できます。

redis-py、PHPRedis、Lettuce におけるエクスポネンシャルバックオフの再試行ロジックのコード例を以下に紹介します。

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

redis-py には、ジッターによるエクスポネンシャルバックオフをサポートする再試行メカニズムが組み込まれています。`retry` パラメータが指定されていない場合、redis-py はデフォルトで に 3 回の再試行`ExponentialWithJitterBackoff`を行います。Redis クライアントを作成するときに、 引数`retry`と `retry_on_error`引数を使用して再試行動作をカスタマイズできます。 [https://redis.readthedocs.io/en/stable/retry.html](https://redis.readthedocs.io/en/stable/retry.html)

```
from redis.backoff import ExponentialBackoff
from redis.retry import Retry
from redis.client import Redis
from redis.exceptions import ConnectionError, TimeoutError, BusyLoadingError

# Run 3 retries with exponential backoff strategy
retry = Retry(ExponentialBackoff(), 3)

# Redis client with retries
client = Redis(
    host="clustercfg.my-cluster.us-east-1.cache.amazonaws.com",
    port=6379,
    retry=retry,
    retry_on_error=[BusyLoadingError, ConnectionError, TimeoutError],
    ssl=True
)
```

再試行設定オプションの詳細については、redis-py ドキュメントの[「再試行ヘルパー](https://redis.readthedocs.io/en/stable/retry.html)」を参照してください。

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

PHPRedis には、最大 10 回 (回数設定は不可) 再試行する再試行メカニズムが組み込まれています。試行間隔の遅延を設定できます (2 回目以降にジッターを加えます)。詳細については、[こちらのサンプルコード](https://github.com/phpredis/phpredis/blob/b0b9dd78ef7c15af936144c1b17df1a9273d72ab/library.c#L335-L368)を参照してください。[PHPRedis (\#1986)](https://github.com/phpredis/phpredis/pull/1986) で、エクスポネンシャルバックオフをネイティブに実装するプルリクエストを送信しました。このリクエストはその後統合され、[文書化](https://github.com/phpredis/phpredis/blob/develop/README.md#retry-and-backoff)されています。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 には、[エクスポネンシャルバックオフとジッター](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/)の投稿で説明したエクスポネンシャルバックオフ戦略に基づく再試行メカニズムが組み込まれています。以下は、フルジッターのアプローチを示すコードの抜粋です。

```
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();
		}

	}
}
```