Redis Cluster 在 AI 平台:会话缓存、结果缓存和队列要分开实例

Redis Cluster 在 AI 平台:会话缓存、结果缓存和队列要分开实例
Redis Cluster 在 AI 平台会话缓存、结果缓存和队列要分开实例基础设施不需要漂亮话。一个 Redis 实例承载所有缓存和队列的场景在生产环境中就是一颗定时炸弹。一、为什么不能用一个 Redis 实例扛一切刚上线的 AI 平台项目常见一种朴素架构启动一个 Redis Cluster会话状态存这里、推理结果缓存存这里、离线推理任务的排队也走这里。在日请求量 10 万量级时一切正常。当请求量突破 100 万/天问题就逐一浮现。这三种用途对 Redis 的资源需求完全不同会话缓存Session Cache数据量小KB 级读写延迟要求极高1ms数据存活时间短5-30 分钟需要高可用但允许少量丢失。推理结果缓存Result Cache数据量中到大KB-MB 级读多写少命中率直接影响 GPU 成本数据量可能达到几十 GB需要 LRU/LFU 淘汰策略。任务队列Task Queue基于 List/Stream 的 FIFO 队列写入频繁消息不能丢失消费者故障时需要消息可重试。这三种负载在一个实例上会产生相互干扰。场景一结果缓存的内存淘汰导致 Redis 响应延迟升高会话查询的 P99 从 2ms 飙到 50ms前端页面出现卡顿。场景二任务队列堆积大量消息内存被 List 占满缓存开始大量淘汰结果缓存命中率从 85% 跌到 30%同一批请求全部穿透到 GPU 推理引发雪崩。graph TB subgraph 推荐架构 Redis1[Redis Cluster Abr/会话缓存br/内存: 2GBbr/淘汰策略: volatile-ttl] Redis2[Redis Cluster Bbr/结果缓存br/内存: 32GBbr/淘汰策略: allkeys-lru] Redis3[Redis Cluster Cbr/任务队列br/内存: 8GBbr/淘汰策略: noeviction] end App[AI 应用服务] -- Redis1 App -- Redis2 App -- Redis3 Redis1 -.-|故障隔离| S1[会话不丢失] Redis2 -.-|故障隔离| S2[缓存可重建] Redis3 -.-|故障隔离| S3[队列可重放]二、会话缓存实例低延迟与高可用会话缓存的核心指标是P99 延迟而不是吞吐量。对于一个单个会话对象 2-5KB 的场景Redis 的单线程模型处理 GET/SET 操作的开销微乎其微。真正影响 P99 延迟的是 Redis 内部的淘汰操作eviction和持久化策略。对于会话缓存实例的配置建议淘汰策略使用volatile-ttl而非allkeys-lru。会话数据都有过期时间让 Redis 按 TTL 自然淘汰即可避免 LRU 淘汰引发的额外 CPU 开销。设置maxmemory时必须留出 20% 的缓冲空间因为 Redis 在内存接近上限时的淘汰操作是异步的可能导致短暂的阻塞。持久化使用 RDB AOF 混合持久化但 AOF 同步策略设置为everysec而非always。会话数据允许丢失 1 秒的数据换来延迟的稳定。哨兵模式的切换时间控制在 10 秒以内基本不会影响用户体验。// 会话缓存的连接池配置 sessionClient : redis.NewClusterClient(redis.ClusterOptions{ Addrs: []string{node1:6379, node2:6379, node3:6379}, PoolSize: 20, // 连接池大小会话场景不需要很大 MinIdleConns: 5, ReadTimeout: 100 * time.Millisecond, // 读超时严格限制 WriteTimeout: 100 * time.Millisecond, MaxRetries: 1, // 重试一次即可避免级联延迟 })三、推理结果缓存实例内存与命中率的平衡推理结果缓存的数据量通常很大。以一个图像生成模型为例单次推理结果512x512 图像 元数据占用约 500KB。如果缓存最近 10 万次推理结果总内存需求约 50GB。加上哈希表开销约 1.2 倍实际需要 60GB 内存。在这种场景下allkeys-lru 淘汰策略是唯一合理的选择。但默认的 LRU 实现近似 LRU采样 5 个 key在大数据量下有精度问题可以调高maxmemory-samples到 10 以提升淘汰精度。Key 的设计直接影响内存效率。推荐使用以下格式infer:{model_name}:{input_hash}:{params_hash}对输入内容计算 SHA256 哈希而非将原始文本作为 Key。一个推理结果缓存的内存分布组件单条大小100 万条总量Key 字符串80 bytes80 MBredisObject 开销16 bytes16 MBValue结果数据500 KB500 GBdictEntry 开销24 bytes24 MB将结果数据压缩后再存储可以显著降低内存。对于文本类结果使用 Zstandard 压缩比例可达 3-5 倍对于图像类结果压缩比例约 1.2-1.5 倍。四、任务队列实例消息可靠性与流控A I 平台中的异步推理任务是队列的主要消费者。一个离线推理任务的生命周期是用户提交 → 排队 → 分配 Worker → 推理执行 → 结果写入。使用 Redis Stream 比 List 更适合作为任务队列原因是消费者组支持多个 Worker 组成消费组自动负载均衡。消息确认通过XACK实现消息的显式确认未确认的消息可以被XPENDING检出并重新投递。消息回溯Stream 中的消息不会在消费后立即删除可以按时间范围回溯。// 任务生产者 func EnqueueTask(ctx context.Context, task *InferTask) error { return queueClient.XAdd(ctx, redis.XAddArgs{ Stream: infer:queue:v1, MaxLen: 100000, // 限制队列长度防止 OOM Values: map[string]interface{}{ model: task.Model, input: task.Input, priority: task.Priority, }, }).Err() } // 任务消费者带重试机制 func WorkerLoop(ctx context.Context, group, consumer string) { for { msgs, _ : queueClient.XReadGroup(ctx, redis.XReadGroupArgs{ Group: group, Consumer: consumer, Streams: []string{infer:queue:v1, }, Count: 1, Block: 5 * time.Second, }).Result() for _, msg : range msgs[0].Messages { processMessage(ctx, msg) queueClient.XAck(ctx, infer:queue:v1, group, msg.ID) } } }关键配置队列实例的maxmemory-policy必须设置为noeviction。在任何情况下都不允许队列中的消息被淘汰消息丢失意味着推理任务的丢失。五、总结三个实例是基线不是过度设计将会话缓存、结果缓存和任务队列分开部署到不同的 Redis Cluster在很多人看来是过度设计——我们日均请求量才 100 万用一个 Redis 够了。但问题不在于请求量多大而在于故障爆炸半径。一个 Redis 实例宕机三种功能同时不可用。三个独立实例任何一个故障都只影响单一功能域并且各自有独立的恢复策略和优先级。这属于运维上的最小爆炸半径原则和系统规模无关。还有一个容易被忽视的成本问题结果缓存需要大内存512GB本地 NVMe 做缓存的方案如使用 Redis on Flash可以将内存成本降低 70%。但这种混合存储方案只适用于结果缓存实例不适合会话缓存延迟要求高和队列实例写入频繁。分开实例才能按需选择存储方案实现成本最优。