缓存锁竞争致系统崩溃?异步 stale-while-revalidate 修复 p99 延迟

原文: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)

发布评论
全部评论(0)