

本文属于机器翻译版本。若本译文内容与英语原文存在差异，则一律以英文原文为准。

# 集群客户端发现和指数回退（Valkey 和 Redis OSS）
<a name="BestPractices.Clients.Redis.Discovery"></a>

在已启用集群模式的情况下连接到 ElastiCache Valkey 或 Redis OSS 集群时，相应的客户端库必须能够感知集群。客户端必须获取哈希槽与集群中相应节点的映射，才能将请求发送到正确的节点，并避免处理集群重定向时产生的性能开销。因此，在两种不同的情况下，客户端必须发现槽和映射节点的完整列表：
+ 客户端将初始化，并且必须填充初始槽配置
+ 从服务器接收 MOVED 重定向，例如在前主节点提供的所有槽都由副本接管时进行失效转移的情况下，或者在槽从源主节点移动到目标主节点时进行重新分片的情况下

通常，通过向 Valkey 或 Redis OSS 服务器发出 CLUSTER SLOT 或 CLUSTER NODE 命令来完成客户端发现。我们建议使用 CLUSTER SLOT 方法，因为它会将一组槽范围以及关联的主节点和副本节点发送回客户端。这不需要从客户端进行额外分析，并且效率更高。

根据集群拓扑，CLUSTER SLOT 命令的响应大小可能会因集群大小而异。带多个节点的集群越大，响应越大。因此，请务必确保执行集群拓扑发现的客户端的数量不会无限增长。例如，在客户端应用程序启动或丢失与服务器的连接且必须执行集群发现时，通常会出现的一个错误是，客户端应用程序会在重试时未添加指数回退的情况下触发多个重新连接和发现请求。这可能导致 Valkey 或 Redis OSS 服务器长时间无响应，且 CPU 利用率达到 100%。如果每条 CLUSTER SLOT 命令均必须处理集群总线中的大量节点，则中断时间会延长。过去，我们在包括 Python（redis-py-cluster）和 Java（Lettuce 和 Redisson）在内的许多不同语言中观察到，此行为导致多次发生客户端中断。

在无服务器缓存中，由于公布的集群拓扑是静态的，并且包含两个条目（写入端点和读取端点），因此许多问题会自动得到缓解。在使用缓存端点时，集群发现还会自动将负载分布到多个节点。但以下建议仍然有用。

为了减少突然涌入的连接和发现请求所造成的影响，我们建议采取以下措施：
+ 实施一个大小有限的客户端连接池，以限制来自客户端应用程序的并发传入连接数。
+ 当客户端因超时而断开与服务器的连接时，请使用带抖动的指数回退进行重试。这有助于避免多个客户端同时给服务器带来压力而导致其不堪重负。
+ 使用[查找 ElastiCache 中的缓存连接端点](Endpoints.md)中的指南查找用于执行集群发现的集群端点。这样一来，您便能将发现负载分布到集群中的所有节点（最多 90 个）上，而不是分布到集群中的几个硬编码的种子节点上。

以下是 redis-py、PHPRedis 和 Lettuce 中指数回退重试逻辑的一些代码示例。

**回退逻辑示例 1：redis-py**

redis-py 具有一个内置的重试机制，可在失败后立即重试一次。您可以通过在创建 [Redis OSS](https://redis.readthedocs.io/en/stable/examples/connection_examples.html#redis.Redis) 对象时提供的 `retry_on_timeout` 参数启用此机制。在这里，我们演示了一种带指数回退和抖动的自定义重试机制。我们提交了一个拉取请求，以便在 [redis-py (\$11494)](https://github.com/andymccurdy/redis-py/pull/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 次（不可配置）。可以配置两次尝试之间的延迟（从第二次重试开始会有抖动）。有关更多信息，请参阅以下[示例代码](https://github.com/phpredis/phpredis/blob/b0b9dd78ef7c15af936144c1b17df1a9273d72ab/library.c#L335-L368)。我们提交了一个拉取请求，以便在 [PHPredis (\$11986)](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();
		}

	}
}
```