缓存锁竞争致系统崩溃?异步 stale-while-revalidate 修复 p99 延迟
3浏览 • 5小时前 •
软件教程
• MA123649
原文:https://dev.to/cogumellum/our-system-crashed-at-1422-it-wasnt-the-database-1p6a(作者 @cogumellum)
摘要: 在高并发负载下,由于锁竞争问题,我们的5分钟缓存键意外地被提前驱逐。解决方案是迁移到异步的 stale-while-revalidate 模式,并引入内存信号量。改动仅18行代码,就将 p99 延迟从 1.8 秒降至 240 毫秒。
生产环境发生了什么故障
上周二,我们主要的流式接口开始出现间歇性超时。
测量到的影响包括:
- p99 延迟: 从 280 毫秒飙升至 3,400 毫秒。
- 504 错误率: 在18分钟窗口内达到4.2%。
- 连接池: 100% 耗尽。
我们最初的错误假设
我们的第一反应是认为上游 LLM 服务商的限流导致了问题。这看起来像是典型的 HTTP 429 背压。我们重启了 Celery 工作进程,但90秒内连接池再次被阻塞。
真正的根本原因
罪魁祸首是一个内部的惊群效应问题。当300个并发请求在第300秒同时命中一个已过期的缓存键时,每一个工作进程都在同一瞬间触发了完全相同的、重新计算上游的查询请求。
[请求 A] ──┐ [请求 B] ──┼─► [过期的缓存键] ──► 300 个同时发出的上游调用 [请求 C] ──┘
代码修复方案
我们没有在请求线程中同步地重新计算,而是实现了一种非阻塞的锁获取机制:在单个后台协程刷新缓存的同时,返回过期的陈旧数据。
import asyncio
async def get_with_revalidation(cache, lock, key: str, factory_coro):
data, expired = await cache.get_stale(key)
if expired and not await lock.is_locked(key):
asyncio.create_task(revalidate_background(cache, lock, key, factory_coro))
return data or await factory_coro()
async def revalidate_background(cache, lock, key: str, factory_coro):
async with lock.acquire(key):
fresh = await factory_coro()
await cache.set(key, fresh, ttl=300)
何时不应使用此方案
如果你的系统处理严格的金融余额或账本交易,其中2秒的陈旧读取会导致双重支付,那么请勿使用 stale-while-revalidate 模式。在我们的场景中,服务的是模型元数据和提示词路由规则,这种权衡是安全且高度推荐的。
复现与性能测试
我们完整的 Locust 负载测试工具和合成工作负载已记录在公开的工程运维手册中:
- 测试方法:GitHub/BeefAPI Gateway
- 生产实现:
gateway de alta resiliência e medição de tokens para LLMs(高弹性网关与LLM令牌计量)。
原文:https://dev.to/cogumellum/our-system-crashed-at-1422-it-wasnt-the-database-1p6a(作者 @cogumellum)
#缓存
#性能优化
#Python
#系统架构
#高并发
#stale-while-revalidate