本页面提供了如何以最优方式使用 Memorystore for Redis 的指导。本页面还指出了需要避免的潜在问题。
如需问题排查方案的列表,请参阅问题排查。
高可用性和数据耐用性最佳实践
我们建议使用标准层级作为高可用性 (HA) 的主要机制。标准层级提供具有多个副本的高可用性实例,并支持在主实例发生故障时使用自动故障切换功能进行快速恢复。此外,在标准层级实例上启用 RDB 快照可提供额外的节点故障保护。
在某些情况下,您可能还需要确保能够从快照备份中恢复数据。在这些情况下,备份和从 RDB 快照恢复数据的能力可以提供额外的保护,防止数据丢失。
如果您启用 RDB 快照并需要恢复数据,则 Memorystore for Redis 可以从最新快照执行此操作。这些快照仅用于内部系统恢复,您无法访问它们。
耐用性限制和最佳实践
虽然标准层级通过复制和自动故障切换提供高可用性,但高可用性并不能保证完整的数据持久性。即使在某些灾难性事件(例如临时区域故障)期间启用了高可用性,Memorystore for Redis 也无法提供完整的数据耐用性。
为了帮助保护您的数据并提供基准级别的持久性,当您使用 Google Cloud 控制台创建 Memorystore for Redis 实例时,系统会默认启用 RDB 快照。
对于生产工作负载,请考虑采用以下最佳实践,进一步提高数据弹性:
- 灾难恢复:Memorystore for Redis 不支持跨区域复制。如果您的灾难恢复策略需要跨区域复制来实现真正的区域级弹性,我们建议您使用 Memorystore for Valkey 实例。如需详细了解如何跨区域配置和管理次要实例,请参阅 Memorystore for Valkey 文档中的使用跨区域复制。
- 最高耐用性:如果您的应用需要尽可能高的数据耐用性,我们建议您使用配置了跨区域复制、自动每日备份和仅附加文件 (AOF) 持久性的 Memorystore for Valkey 实例。这种组合可提供最可靠的数据丢失防护。如需了解详情,请参阅 Memorystore for Valkey 文档中的持久化概览。
容量规划最佳实践
为确保您能够成功预配资源并构建弹性架构,我们建议您遵循以下容量规划最佳实践:
- 跨多个位置部署:如果您因临时资源限制而无法在首选可用区中创建实例,请尝试在同一区域内的其他可用区或在其他区域中创建实例。
- 使用跨区域访问:将客户端配置为访问位于不同区域的实例。如果您因本地资源限制而需要预配位于其他区域的实例,则客户端可以连接到该实例。
- 选择其他容量层级:如果您因实例大小而遇到资源限制,请选择其他容量层级或调整实例大小。如需了解详情,请参阅容量层级性能。
RDB 导出
导出 RDB 备份时,请遵循以下指南:
- 在写入速率较低的时候导出。
- 如果在写入速率较同的时候导出,请将
maxmemory配置暂时降低到 50% 的实例容量,以便提供足够的开销以保证操作成功。
资源密集型操作
对于标准层级 Redis 实例,以下操作会在操作期间使用额外的内存:
由于复制功能,版本升级、扩缩和手动故障切换会占用额外的内存(对于标准层级实例而言)。这些操作遵循标准层级实例升级行为中所述的复制过程。
导入和导出操作需要额外的内存,因为存在与这些操作相关的分支 Redis 进程和写入时复制数据管理。
为了减少资源密集型操作的缺点,您应该:
- 在操作期间,将最大内存配置降低到 80% 的实例容量。这样做可以提供足够的开销以实现成功的操作。
- 监控系统内存用量指标,并确保在运行其中一项操作之前,此指标低于 80%。
- 在实例流量较低的时段(例如夜间或周末)运行这些操作。
- 在运行这些操作之前,先设置具有指数退避算法的重试逻辑。
需要连接重试的操作和情景
以下操作和情景会中断您的网络与 Redis 实例之间的网络连接:
这些操作会修改您的实例,需要临时中断连接。您需要在运行这些操作前先设置具有指数退避算法的重试逻辑,以便您的应用能够自动重新连接并继续正常运行。
例行维护
Memorystore for Redis 实例会定期进行维护。如需了解详情,请参阅 Memorystore for Redis 维护政策。
实施以下最佳做法,为日常维护做好准备:
- 设置维护窗口以安排何时进行维护更新。
- 将维护窗口安排在实例流量较低且内存开销充足的时间。如需了解详情,请参阅维护更新的影响。
- 开启维护窗口通知,提醒您即将进行维护。
- 设置具有指数退避算法的重试逻辑。
- 对于标准层级实例,您可以使用手动故障切换模拟维护事件,以了解维护导致的故障切换如何影响您的应用。
- 对于基本层级实例,您可以通过暂时将实例伸缩到更大容量来模拟维护更新的影响。观察到影响后,您可以缩减回原始大小。
内存管理
由于开源 Redis 会发生众所周知的内存碎片化,因此内存管理可能是一项挑战。我们建议您降低实例的 maxmemory 配置,以便在出现高内存压力时产生开销。
监控 Memorystore 实例内存压力的最佳方法是使用系统内存用量指标。如需查看详细指南以了解如何管理 Memorystore for Redis 的内存,请参阅内存管理最佳做法。
管理空闲连接
如果连接未正常终止,您可能会看到 Memorystore 实例的连接数量会随着时间的推移不断增加。此操作可能会对性能产生负面影响,尤其是在使用传输加密时,该功能会根据您的容量层级施加最大连接数限制。为了缓解此问题,我们建议使用 timeout Redis 配置参数,该参数可让您设置自动终止空闲客户端连接之前的秒数。
Access Transparency 资源名称
不应将敏感数据存储在 Memorystore for Redis 资源名称中。资源名称是指 Memorystore for Redis 实例名称和实例元数据(例如标记)。存储在资源名称中的数据不一定受 Google Cloud Access Transparency 保护,并且可能与贵组织的 Access Transparency 合规性要求冲突。
某些无服务器环境需要无服务器 VPC 访问通道连接器
某些无服务器环境需要无服务器 VPC 访问通道连接器才能连接到 Memorystore for Redis。如果您想使用其中一种环境进行连接,请为您的项目设置无服务器 VPC 访问通道连接器。
网络
我们建议您使用专用服务访问通道连接模式。Memorystore for Redis 使用两种连接模式:专用服务访问通道和直接对等互连。专用服务访问连接模式使 IP 范围管理更加简单,并允许您使用共享 VPC。
创建实例后,连接模式便无法更改。
如需了解详情,请参阅网络。
监控和提醒
我们建议您使用监控和提醒,因为它们会为您提供有关 Redis 实例内存用量的关键信号。此外,您还可以深入了解 Redis 实例响应传入缓存请求的效率。
您应该设置以下默认警报:
CPU 使用率最佳实践
不当使用高开销 Redis 命令会导致高延迟、无响应或连接问题。标准层级实例在灾难恢复期间提供高可用性,并依赖于主节点和副本节点之间的异步复制。如果其中一个节点有耗时的命令处理操作,导致 Redis 主线程阻塞,则复制可能会受到影响。如果问题仍然存在,并且发生位置服务中断,则在发生服务中断的位置写入的最新数据可能无法在其他位置使用。
我们建议使用 Cloud Monitoring 为主线程 CPU 秒数 (redis.googleapis.com/stats/cpu_utilization_main_thread) 指标设置提醒,以确保主节点的 CPU 利用率不超过 0.8 秒,每个副本节点的 CPU 利用率不超过 0.5 秒(当副本被指定为读取副本时)。
如果您的 Redis 实例超出建议值,我们建议您将实例扩缩到更高的容量层级,或按照问题排查说明操作,以避免 CPU 密集型操作。
如果实例的 CPU 利用率较高或实例的资源耗尽(例如,连接过多),则实例可能会出现异常行为,并且外部指标可能会缺失。
资源密集型命令
我们强烈建议您避免使用资源密集型 Redis 命令。使用这些命令可能会导致以下性能问题:
- 延迟时间较长和客户端超时
- 由增加内存用量的命令引起的内存压力
- 由于 Redis 主线程被阻塞,节点复制和同步期间发生数据丢失
- 健康检查、可观测性和复制功能不足
下表列出了资源密集型 Redis 命令的示例,并为您提供了资源高效型替代方案。
| 类别 | 资源密集型命令 | 资源高效型替代方案 |
|---|---|---|
| 针对整个键空间运行 | KEYS |
SCAN |
| 针对长度可变的密钥集运行 | LRANGE |
限制查询所用范围的大小。 |
ZRANGE |
限制查询所用范围的大小。 | |
HGETALL |
HSCAN |
|
SMEMBERS |
SSCAN |
|
| 阻止脚本运行 | EVAL |
确保脚本不会无限期运行。 |
EVALSHA |
确保脚本不会无限期运行。 | |
| 移除文件和链接 | DEL |
UNLINK |
| 发布和订阅 | PUBLISH |
SPUBLISH |
SUBSCRIBE |
SSUBSCRIBE |
Redis 客户端最佳实践
本部分提供了有关如何以最佳方式使用 Redis 客户端的指导。
检测并处理无响应的连接
我们强烈建议您配置客户端应用,以检测与 Memorystore for Redis 的无响应连接。检测到无响应的连接时,客户端必须重置该连接。为了构建弹性应用,我们建议 采用以下客户端配置:
- 配置 TCP keep-alive 参数:设置
TCP keepalive time、TCP keepalive interval和TCP keepalive probes参数,以便客户端主动检测并丢弃无响应的连接,即使连接处于空闲状态也是如此。例如,如果您将TCP keepalive time参数设置为 30 秒,将TCP keepalive interval设置为 10 秒,将TCP keepalive probes设置为 3,则客户端会在一分钟内重置无响应的空闲连接。 - 配置 TCP 用户超时:在客户端中设置此超时,以重置具有未完成请求且停止响应的连接。例如,如果您将超时设置为 15 秒,则客户端会在 15 秒后重置具有未完成请求的无响应连接。
针对特定客户端的最佳实践
如果您横向扩容应用,则可能会在写入命令时遇到 READONLY 错误。Memorystore for Redis 使用具有静态端点的非 Sentinel 部署,并且客户端不会预先收到动态角色信息。如果您为主端点和副本端点使用单个客户端连接,则客户端可能会意外地向只读副本发送写入命令。副本会返回 READONLY 错误,因为它无法处理写入命令。
为防止写入路由错误,请勿将单个连接同时用于读取和写入。而是通过创建以下单独的客户端实例来拆分操作:
- 主要客户端:仅连接到主要端点
- 副本客户端:仅连接到副本端点
以下标签页展示了如何在 Go、Java、Node.js 和 Python 中配置单独的模板。
Go
package main import ( "context" "fmt" "github.com/redis/go-redis/v9" ) func main() { ctx := context.Background() // Initialize the primary client connecting only to the primary endpoint primaryClient := redis.NewClient(&redis.Options{ Addr: "PRIMARY_HOST:6379", Password: "YOUR_PASSWORD", DB: 0, }) defer primaryClient.Close() // Initialize the replica client connecting only to the replica endpoint replicaClient := redis.NewClient(&redis.Options{ Addr: "REPLICA_HOST:6379", Password: "YOUR_PASSWORD", DB: 0, }) defer replicaClient.Close() // Use the primary client for all mutating commands err := primaryClient.Set(ctx, "example_key", "example_value", 0).Err() if err != nil { fmt.Printf("Failed to write to primary: %v\n", err) } // Use the replica client for all read-only commands val, err := replicaClient.Get(ctx, "example_key").Result() if err != nil { fmt.Printf("Failed to read from replica: %v\n", err) } else { fmt.Printf("Successfully read value: %s\n", val) } }
Java
@Bean public RedisConnectionFactory primaryConnectionFactory() { RedisStandaloneConfiguration config = new RedisStandaloneConfiguration("PRIMARY_HOST", 6379); config.setPassword(RedisPassword.of("YOUR_PASSWORD")); return new LettuceConnectionFactory(config); } @Bean public RedisConnectionFactory replicaConnectionFactory() { RedisStaticMasterReplicaConfiguration config = new RedisStaticMasterReplicaConfiguration("PRIMARY_HOST", 6379); config.addNode("REPLICA_HOST", 6379); config.setPassword(RedisPassword.of("YOUR_PASSWORD")); LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() .readFrom(ReadFrom.REPLICA_PREFERRED) .build(); return new LettuceConnectionFactory(config, clientConfig); } @Bean public RedisTemplate<String, Object> primaryRedisTemplate( @Qualifier("primaryConnectionFactory") RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); return template; } @Bean public RedisTemplate<String, Object> replicaRedisTemplate( @Qualifier("replicaConnectionFactory") RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); return template; }
Node.js
import { createClient } from 'redis'; async function main() { // Initialize the primary client connecting only to the primary endpoint const primaryClient = createClient({ url: 'redis://PRIMARY_HOST:6379', password: 'YOUR_PASSWORD' }); // Initialize the replica client connecting only to the replica endpoint const replicaClient = createClient({ url: 'redis://REPLICA_HOST:6379', password: 'YOUR_PASSWORD' }); primaryClient.on('error', (err) => console.error('Primary Client Error', err)); replicaClient.on('error', (err) => console.error('Replica Client Error', err)); await primaryClient.connect(); await replicaClient.connect(); // Use the primary client for all mutating commands try { await primaryClient.set('example_key', 'example_value'); console.log('Successfully wrote to primary'); } catch (err) { console.error('Failed to write to primary:', err); } // Use the replica client for all read-only commands try { const val = await replicaClient.get('example_key'); console.log(`Successfully read value: ${val}`); } catch (err) { console.error('Failed to read from replica:', err); } await primaryClient.disconnect(); await replicaClient.disconnect(); } main();
Python
import redis def main(): # Initialize the primary client connecting only to the primary endpoint primary_client = redis.Redis( host='PRIMARY_HOST', port=6379, password='YOUR_PASSWORD', decode_responses=True ) # Initialize the replica client connecting only to the replica endpoint replica_client = redis.Redis( host='REPLICA_HOST', port=6379, password='YOUR_PASSWORD', decode_responses=True ) # Use the primary client for all mutating commands try: primary_client.set('example_key', 'example_value') print('Successfully wrote to primary') except redis.RedisError as e: print(f'Failed to write to primary: {e}') # Use the replica client for all read-only commands try: val = replica_client.get('example_key') print(f'Successfully read value: {val}') except redis.RedisError as e: print(f'Failed to read from replica: {e}') if __name__ == '__main__': main()