Redis内存爆掉拖垮LiteLLM网关?一场生产级联故障的复盘与优化实践

发布时间:2026/11/4 3:34:11
Redis内存爆掉拖垮LiteLLM网关?一场生产级联故障的复盘与优化实践 凌晨一点多告警电话把我从梦里拽起来——生产环境的LiteLLM网关大面积超时紧接着下游服务开始连环报错。我们这套基于LiteLLM Proxy搭建的LLM统一网关平时跑得好好的谁也没想到会因为一层Redis内存问题演变成级联故障。这篇文章不是耸人听闻的标题党而是把我们踩过的坑、排查过程和最终的内存优化方案完整记录下来给正在用LiteLLMRedis做生产网关的同学一个参照。可能你的集群规模比我们大也可能只是一个小试点但Redis内存失控导致网关雪崩这件事早晚会碰上不如提前排雷。1. 故障现象与影响范围1.1 第一波告警网关响应缓慢那天的第一波告警是“LiteLLM Proxy 平均响应时间超过 10s”紧接着是“上游调用失败率超过 20%”。我们的LiteLLM Proxy部署在K8s里平时P99延迟在1秒以内这突然升到10秒以上明显不对劲。最开始我以为是模型供应商的接口出了问题结果检查了一遍Bedrock、OpenAI的返回发现上游都正常网关本身也没有进程崩溃或者内存溢出的迹象。于是我把注意力放在网关的依赖组件上。LiteLLM在设计上把Redis当作通用数据层缓存、限流、RPM/TPM配额甚至分布式锁都放在里面。一旦Redis出现性能问题网关的每一次请求都可能被阻塞在等待Redis响应上这就能解释为什么上游正常而网关却慢如蜗牛。1.2 级联扩散上游模型 API 超时与客户端积压网关自己变慢以后更麻烦的事情跟着来了。由于LiteLLM Proxy对上游模型API的调用有超时配置网关处理不过来导致大量请求在内部排队。客户端——也就是我们内部的业务服务——等不到回应就开始自动重试重试又进一步加剧网关的负载形成一个典型的雪崩循环。我们还观察到Redis所在的节点CPU监控没有异常但网络连接数一直在涨。这是因为每个LiteLLM实例都在不断建立新的Redis连接来抢锁、刷新缓存而慢查询让这些连接迟迟得不到释放。到后半夜Redis连接数直接打满连redis-cli ping都要等好几秒。这时候已经不光是LiteLLM网关的问题了其他依赖同一个Redis实例的服务比如消息队列消费者和定时任务也开始出现间歇性卡顿。1.3 影响面不只是网关宕机这次故障持续了将近两个小时最终导致我们内部基于LLM的客服摘要、代码辅助、数据分析助手全线不可用。业务方的反馈是“机器人不回话”“生成结果很慢”实际上就是网关已经被拖垮了。更严重的是因为重试风暴我们在一段时间内对模型供应商的调用量几乎是正常情况的3倍账单也肉眼可见地跳了一截。事后我们梳理影响面发现这次故障并不是单纯的“Redis满了”或“LiteLLM挂了”而是两者互相放大的结果。Redis内存不足触发淘汰策略把一些关键的缓存和限流数据淘汰掉了导致LiteLLM大量请求绕过了缓存直接打到上游同时由于获取分布式锁等待时间变长网关内部线程被长时间占用最终把整个系统拖死。2. 根因定位从表象到 Redis 内存2.1 初步排查怀疑后端模型接口慢排查过程一开始是有误导的。我们首先看了LiteLLM Proxy的日志发现大量upstream_request_timeout和connection_reset错误直观上像是模型供应商网络不稳定。但单独用curl直接调用同一个模型接口响应正常且稳定说明问题不在上游。然后我们查了网关自身的JVM/Go内存和CPULiteLLM Proxy是Python写的我们看了进程RSS没发现明显泄漏。又看K8s pod的CPU监控也是正常水平。真正让我警觉的是Redis监控面板——内存使用率已经达到maxmemory的99.9%used_memory_peak一直在高位。此时我意识到问题大概率不在网关本身而是LiteLLM对Redis的依赖被内存瓶颈放大了。2.2 揪出 Redis内存逼近 maxmemoryLRU 淘汰引发连锁用redis-cli -h redis-host INFO memory一看maxmemory设置为4GBused_memory已经到3.98GBevicted_keys在故障期间以每秒几百个的速度增长。我们的Redis配置是maxmemory-policy allkeys-lru这意味着内存满了以后Redis会按近似LRU算法随机淘汰一些key。问题就出在这里LiteLLM把响应缓存和限流计数器、分布式锁都放在Redis里。如果淘汰的是纯缓存数据最多就是多几次上游调用但如果淘汰的是分布式锁或者限流计数器的key就会导致锁竞争异常、限流阈值被绕过。我们检查发现故障期间大量请求因为获取不到锁直接抛异常同时没有限流保护进一步加重了模型API的负载。2.3 为什么 Redis 内存会爆缓存 key 无限增长定位到Redis内存问题后我开始分析到底是什么key占满了内存。用redis-cli --bigkeys扫描了一遍发现有大量litellm:cache:...开头的字符串key单个value最大的有几MB。这些key是LiteLLM根据模型名、prompt哈希、温度参数等组合生成的本来应该设置了TTL但实际很多key的TTL是永久-1或者超长。进一步看数据分布我们统计了key数量和内存占比其中缓存类key占了70%其次是限流计数器Hash类型和分布式锁String类型。限流计数器虽然key小但数量极其庞大因为LiteLLM的RPM/TPM配额是分用户、分模型、分时间窗口记录的窗口一变就会生成新key。而Redis又是单线程模型内存回收和淘汰都会占用CPU当内存逼近上限时CPU也跟着飙升最终整个Redis进入“内存不足频繁淘汰”的恶性循环。3. Redis 内存优化实践3.1 数据分层缓存、限流、锁的差异化存储故障恢复后我们做的第一件事不是急着加内存而是把Redis里混在一起的数据拆开。LiteLLM这类网关组件对数据延迟和持久性的要求不一样混在一个实例里互相拖累。响应缓存适合放在尽量靠近网关的地方可以容忍丢失内存占大头。我们后来单独开了一个Redis实例甚至可以考虑用本地缓存比如cachelocal但本地缓存无法在多个LiteLLM副本间共享所以我们还是保留Redis但单独部署。限流计数器对一致性要求高但数据量不大且时效性短可以和缓存分开使用不同的逻辑库或者使用独立的Redis实例。我们对这部分key设置了maxmemory策略为noeviction避免限流key被淘汰后失去保护作用。分布式锁需要高可用但占内存极小放在主从Redis上设置绝对过期时间避免客户端崩溃后锁不释放。如果只是临时优化而没条件新增Redis实例至少也要用key前缀区分开并对不同前缀设置不同的maxmemory-policy。不过Redis对混合策略支持有限所以从长远看我更推荐物理隔离或者多实例。3.2 key 设计与过期策略调整LiteLLM默认生成的缓存key太长可读性差而且由于包含完整prompt的哈希没法直观看出对应业务。我们改进了LiteLLM的自定义key生成逻辑在代码里增加了litellm.cache_key钩子将模型名、版本、业务线、prompt哈希截断组合成短key例如//cache/gpt-4o/op1/8f3a2b这种格式。TTL设置上也踩了不少坑。以前图省事统一设置24小时结果某些热点prompt的缓存会在同一个时间窗口集体失效造成缓存雪崩。现在我们对缓存key设置了基础TTL加随机偏差比如TTL 3600 random(0, 600)避免同时过期。同时把限流计数器的窗口对齐到固定时间片key过期时间设置为窗口长度加一个较小的缓冲。这里还特别关注了LiteLLM的缓存行为默认情况下LiteLLM会缓存“模型响应”但如果业务需要实时数据缓存可能不适用。我们增加了no-cache请求头透传在需要跳过缓存时自动绕过。3.3 maxmemory-policy 选择与参数调优以前我们的Redis配置文件几乎是默认值这是最大的失误。maxmemory-policy allkeys-lru看似方便实际对业务数据不友好。在LiteLLM网关场景下正确的做法是对于缓存实例使用allkeys-lru是可以接受的因为缓存数据丢了也无所谓主要是降低cache miss。对于业务实例限流锁使用noeviction或volatile-lru只淘汰设置了TTL的key保留永久key比如分布式锁的key即使设了ttl也是短暂的不会占太多。对缓存实例设置合理的maxmemory不要超过物理内存的70%给操作系统留出Page Cache余量。开启activedefrag yes配合active-defrag-threshold-lower 10、active-defrag-ignore-bytes 100mb在内存碎片化严重时自动整理。我们还把maxmemory-samples从默认的5增大到10提高LRU近似算法的精度减少淘汰错杀的概率。这些参数看起来很小但在高并发场景下对命中率的影响立竿见影。3.4 定期扫描与预警机制内存优化不是一次性工作需要长期监控。我们写了一个简单的Python脚本定期用SCAN遍历Redis key输出各前缀的内存占用Top列表并推送Elasticsearch或者直接打日志。脚本本身不复杂但关键是定时执行和结果可视化。import redis import sys r redis.Redis(hostlocalhost, port6379, db0, decode_responsesTrue) prefix_stats {} cursor 0 sample_count 0 while True: cursor, keys r.scan(cursorcursor, count2000) sample_count len(keys) for key in keys: prefix key.split(:, 1)[0] if prefix not in prefix_stats: prefix_stats[prefix] {count: 0, bytes: 0} try: # 只做近似内存占用评估用 MEMORY USAGE 成本较高按抽样做 size r.memory_usage(key, samples10) except: size 0 prefix_stats[prefix][count] 1 prefix_stats[prefix][bytes] size if cursor 0: break print(fScanned {sample_count} keys) for prefix, stats in sorted(prefix_stats.items(), keylambda x: x[1][bytes], reverseTrue)[:20]: print(f{prefix}: count{stats[count]}, mem{stats[bytes]/1024/1024:.2f}MB)除了脚本我们在Prometheus里添加了Redis exporter采集指标重点监控redis_memory_used_bytes / redis_memory_max_bytes比例超过85%触发告警。以前我们只监控CPU和平均延迟完全没想到内存淘汰才是潜伏最深的雷。4. 网关级联故障的防御重构4.1 Redis 高可用架构从单机到主从 哨兵这次故障也暴露出我们把Redis当成了“永不宕机”的组件。最初是单节点部署恢复过程只能靠重启风险很大。为了不让网关再次因为Redis单点故障而级联崩溃我们部署了标准的主从加哨兵架构用三个哨兵节点监控。哨兵配置我们用了quorum 2保证至少两个哨兵确认master下线才切换避免误判。同时所有LiteLLM容器连接Redis时不在代码里写死IP而是通过哨兵服务发现获取当前master地址。在Python环境里LiteLLM底层用的是redis-py可以通过redis.sentinel.Sentinel封装连接池这样即使master切换网关也能自动重连到新master。这期间我们还做了一次故障演练手动shutdownmaster观察哨兵多久能完成切换。第一次演练花了将近30秒才切换我们调优了sentinel down-after-milliseconds从30000降到10000failover-timeout改成30000最终切换时间稳定在10秒左右。这10秒里LiteLLM网关的请求会快速失败但我们配合了下面的降级策略让损失最小化。4.2 缓存降级与熔断兜底Redis故障期间网关不能直接崩溃。我们优化了LiteLLM的配置增加了缓存降级策略。在LiteLLM里可以通过环境变量LITELLM_LOCAL_CACHE启用本地缓存local_cacheTrue当Redis不可用或者异常时自动切换为进程内缓存虽然多实例间一致性差一些但至少能保证服务不中断。同时LiteLLM本身支持stream接流式响应我们在网关层接入了自定义的熔断逻辑当Redis错误率连续超过阈值时用circuit_breaker断掉流向Redis的请求直接走模型API。这相当于牺牲一部分性能换取核心功能的可用性。此外我们也给LiteLLM Proxy设置了更合理的超时参数比如timeout60但在客户端那一层加上了快速失败机制不让调用方无限等待。当网关超时错误码返回503 Retryable业务方最多重试两次且使用指数退避避免再次引发重试风暴。4.3 可观测性从“故障后排查”到“故障前预警”这次故障最大的教训之一是没有建立健全的Redis监控和告警。传统监控只关注节点存活和CPU但Redis真正危险的是内存和key的缓慢增长。我们现在在三个层面做了观测基础设施层使用Prometheus redis_exporter采集used_memory、mem_fragmentation_ratio、evicted_keys、rejected_connections、blocked_clients等指标并设置了趋势告警。比如evicted_keys如果持续增长哪怕还没有达到maxmemory也会触发attention级别的通知。应用层LiteLLM提供了Prometheus指标端点/metrics我们重点看litellm_proxy_total_requests、litellm_proxy_latency、litellm_redis_errors。通过给请求打上user和model标签可以快速定位到是哪个业务线的哪个模型导致Redis压力上升。日志层所有的Redis慢查询日志我们通过slowlog-log-slower-than 10000捕获采集到日志平台。慢查询往往比CPU飙升出现得更早是一条有效的预警信号。我们还在容量规划上留了余量所有Redis实例按峰值利用率的60%进行评估超过60%就开始考虑扩容而不是等到90%才紧张。给每个实例预留了至少一倍的内存空间应对突发流量和key增长。5. 复盘经验与踩坑清单5.1 我最后悔的几件事说实话这次故障完全可以提前避免。回头复盘最后悔的有三件事第一当初为了省事把LiteLLM的缓存、限流、锁都丢到同一个Redis实例里还随手设置了allkeys-lru完全没想过淘汰策略对不同数据的影响。第二没有对Redis做定期的大key扫描和TTL治理导致大量超过30天都没访问过的key长年占用内存。第三没有做故障演练低估了Redis故障对业务链路的冲击力。还有一点我们的业务团队在故障发生后第一时间改配置、重启pod反而造成了多次短时间抖动。正确的做法应该是先保留现场通过Redis的RDB和AOF日志分析故障期间的key变化再决定恢复策略。5.2 给同样在用 LiteLLM Redis 的同学的建议如果你也是用LiteLLM网关做生产调度我建议你从这几个方面检查自己检查Redis的maxmemory-policy不要用allkeys-lru一把梭。缓存数据和其他业务数据分离或者用volatile-lru。给你的LiteLLM缓存key加一个规范化前缀并认真设置TTL不要依赖默认值。在代码里实际验证一下请求是否真的命中了缓存。至少部署一个哨兵高可用方案并定期演练主从切换。别把“单点Redis”当作不可能发生的状况。配置Redis内存使用率超过75%的告警并监控evicted_keys的增长速率。这个指标比CPU更有预示性。重启Redis前先备份RDB别在没确认内存分布的情况下贸然FLUSHALL。真flush了就等着被业务方追杀吧。这次故障之后我们的网关和Redis架构做了整体升级半个季度过去没有再出现类似问题。但每次看到监控面板上的内存使用率红线我还是会想起那个凌晨的慌乱。生产系统里的每一个默认配置可能都是未来的一个事故炸弹早一点优化就少一次惊吓。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询