

本文為英文版的機器翻譯版本，如內容有任何歧義或不一致之處，概以英文版為準。

# 叢集用戶端探索和指數退避 (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();
		}

	}
}
```