集群客户端发现和指数级退缩(Valkey 和 Redis)OSS - 亚马逊 ElastiCache

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

集群客户端发现和指数级退缩(Valkey 和 Redis)OSS

在启用OSS集群模式的情况下连接到 ElastiCache Valkey 或 Redis 集群时,相应的客户端库必须具有集群感知能力。客户端必须获取哈希槽与集群中相应节点的映射,才能将请求发送到正确的节点,并避免处理集群重定向时产生的性能开销。因此,在两种不同的情况下,客户端必须发现槽和映射节点的完整列表:

  • 客户端将初始化,并且必须填充初始槽配置

  • 从服务器接收到MOVED重定向,例如在故障转移的情况下,前主节点提供的所有插槽都被副本接管,或者在插槽从源主节点移动到目标主节点时进行重新分片

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

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

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

为了减少突然涌入的连接和发现请求所造成的影响,我们建议采取以下措施:

  • 实施一个大小有限的客户端连接池,以限制来自客户端应用程序的并发传入连接数。

  • 当客户端因超时而断开与服务器的连接时,请使用带抖动的指数回退进行重试。这有助于避免多个客户端同时给服务器带来压力而导致其不堪重负。

  • 使用在中查找连接端点 ElastiCache中的指南查找用于执行集群发现的集群端点。这样一来,您便能将发现负载分布到集群中的所有节点(最多 90 个)上,而不是分布到集群中的几个硬编码的种子节点上。

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

回退逻辑示例 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内置重试机制,最多可重试 a(不可配置)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 具有基于指数回退和抖动文章中描述的指数回退策略的内置重试机制。以下是显示完整抖动方法的代码摘录:

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