本页面提供了如何以最优方式使用 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,那么客户端会在 1 分钟内重置无响应的空闲连接。 - 配置 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()