클러스터 클라이언트 검색 및 지수 백오프(Valkey 및 RedisOSS) - Amazon ElastiCache

기계 번역으로 제공되는 번역입니다. 제공된 번역과 원본 영어의 내용이 상충하는 경우에는 영어 버전이 우선합니다.

클러스터 클라이언트 검색 및 지수 백오프(Valkey 및 RedisOSS)

클러스터 모드가 활성화된 상태에서 ElastiCache Valkey 또는 Redis OSS 클러스터에 연결할 때는 해당 클라이언트 라이브러리가 클러스터를 인식해야 합니다. 요청을 올바른 노드로 보내고 클러스터 리디렉션을 처리하는 데 따른 성능 오버헤드를 피하려면 클라이언트가 클러스터의 해당 노드에 대한 해시 슬롯 맵을 가져와야 합니다. 따라서 클라이언트는 다음과 같은 2가지 상황에서 전체 슬롯 목록과 매핑된 노드를 검색해야 합니다.

  • 클라이언트가 초기화되었으므로 초기 슬롯 구성을 채워야 합니다.

  • 이전 프라이머리 노드에서 제공하는 모든 슬롯을 복제본에서 인계받을 때 장애 조치 상황이나 슬롯을 소스 프라이머리에서 대상 프라이머리 노드로 이동할 때 재분배하는 경우와 같이 서버로부터 리MOVED디렉션이 수신됩니다.

클라이언트 검색은 일반적으로 Valkey 또는 Redis OSS 서버에 CLUSTER SLOT 또는 CLUSTER NODE 명령을 발급하여 수행됩니다. 슬롯 범위 세트와 연결된 기본 및 복제본 노드를 클라이언트로 반환하기 때문에 CLUSTER SLOT 메서드를 사용하는 것이 좋습니다. 이렇게 하면 클라이언트의 추가 구문 분석이 필요하지 않아 더 효율적입니다.

클러스터 토폴로지에 따라 CLUSTER SLOT 명령에 대한 응답 크기는 클러스터 크기에 따라 다를 수 있습니다. 큰 클러스터의 노드 수가 많을수록 응답 크기도 커집니다. 따라서 클러스터 토폴로지 검색을 수행하는 클라이언트 수가 무제한으로 증가하지 않도록 하는 것이 중요합니다. 예를 들어 클라이언트 애플리케이션이 시작되거나 서버와의 연결이 끊기고 클러스터 검색을 수행해야 하는 경우, 일반적인 실수 중 하나는 클라이언트 애플리케이션이 재시도 시 지수 백오프를 추가하지 않고 여러 번 재연결 및 검색 요청을 실행하는 것입니다. 이렇게 하면 Valkey 또는 Redis OSS 서버가 100%의 CPU 사용률로 장기간 응답하지 않을 수 있습니다. 각 CLUSTER SLOT 명령이 클러스터 버스에서 많은 수의 노드를 처리해야 하는 경우 중단이 연장됩니다. Python(redis-py-cluster) 및 Java(Lettuce 및 Redisson)를 비롯한 여러 언어에서 이러한 동작으로 인해 과거에 여러 클라이언트가 중단된 것이 관찰되었습니다.

서버리스 캐시의 경우, 알려진 클러스터 토폴로지가 정적이고 쓰기 엔드포인트와 읽기 엔드포인트의 두 항목으로 구성되어 있으므로 많은 문제가 자동으로 완화됩니다. 또한 캐시 엔드포인트를 사용하면 클러스터 검색이 여러 노드에 자동으로 분산됩니다. 하지만 여전히 다음 권장 사항을 따르는 것이 좋습니다.

연결 및 검색 요청의 갑작스러운 유입으로 인한 영향을 완화하려면 다음 내용을 따르세요.

  • 제한된 크기의 클라이언트 연결 풀을 구현하여 클라이언트 애플리케이션에서 동시에 들어오는 연결 수를 제한합니다.

  • 제한 시간 초과로 인해 서버에서 클라이언트 연결이 끊어지면 지터가 있는 지수 백오프를 사용하여 다시 시도합니다. 이렇게 하면 여러 클라이언트가 동시에 서버에 과부하를 가하지 않도록 할 수 있습니다.

  • 에서 연결 엔드포인트 찾기 ElastiCache 섹션의 가이드를 사용하여 클러스터 검색을 수행할 클러스터 엔드포인트를 확인합니다. 이렇게 하면 클러스터에서 몇 개의 하드코딩된 시드 노드에 도달하지 않고 클러스터의 모든 노드(최대 90개)에 검색 부하를 분산할 수 있습니다.

다음은 redis-py, PHPRedis및 Lettuce의 지수 백오프 재시도 로직에 대한 몇 가지 코드 예제입니다.

백오프 로직 샘플 1: redis-py

redis-py에는 실패하자마자 한 번 재시도하는 재시도 메커니즘이 내장되어 있습니다. 이 메커니즘은 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회 (구성 불가)를 재시도하는 재시도 메커니즘이 내장되어 있습니다. 시도 사이에는 지연을 구성할 수 있습니다(두 번째 재시도부터 지터 사용). 자세한 내용은 다음 샘플 예제를 참조하세요. 이후 병합되고 문서화된 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에는 지수 백오프 및 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(); } } }