ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

LLM网关限流实战:令牌桶、滑动窗口与配额管理设计

LLM网关限流实战:令牌桶、滑动窗口与配额管理设计 不管是接 OpenAI、Anthropic 还是各种国产大模型 API只要你的业务流量上来一点最先撞上的墙往往不是模型能力不够而是429 Too Many Requests。这个状态码意味着你已经被服务端限流了。尤其是在LLM应用里一次请求可能耗时几十秒、消耗几千甚至上万token如果等到429才做处理用户体验和成本都已经受到明显影响。这篇文章的内容来自我最近在做的一个面向企业内部的LLM网关中间件。我会把令牌桶、滑动窗口、API配额管理这几个核心模块从选型到落地的思考过程拆开讲为什么普通API的限流方案搬到LLM场景会失灵、Redis在限流里的边界在哪、多级配额应该怎么设计以及生产环境里那些只有跑过才知道的坑。这篇文章适合正在做LLM应用后端、API网关或者给团队搭建模型调用平台的工程师特别是已经遇到了“限流策略明明加了但还是被打爆”这种问题的人。1. LLM场景限流与传统API限流的本质差异1.1 为什么传统RPS限流在LLM场景不够用先说结论传统后端服务做限流盯着RPS每秒请求数基本就够用了。普通接口的耗时都在几十到几百毫秒一个请求消耗的资源是可预测的QPS压住了CPU、内存、数据库连接数就都能稳住。但LLM推理服务完全不是这个玩法。一个对话补全请求可能持续5到60秒期间Token是一个一个流式吐出来的。同样是1个RPS如果是普通API后端每秒最多承受的负载几乎不变放到LLM场景1个请求和10个并发请求之间对推理服务的压力差异可能是一两个数量级。更麻烦的是常见的LLM服务在实际部署中往往依赖共享的推理资源比如GPU显存、KV Cache、batch大小。一个超长上下文的请求就能把batch里其他请求的显存占用顶上去这种资源耦合是普通HTTP接口很少遇到的。所以在LLM应用的限流设计里请求数量只是其中一个维度。Token消耗速率、并发连接数、单请求的最大耗时这几个指标都必须纳入考量。我见过不少团队直接把过去Redis 固定窗口那套RPS限流搬到LLM网关结果线上还是频繁被打穿原因就是没有从“LLM服务是按Token计费且资源高度动态”这个底层逻辑出发设计限流维度。还有一个容易忽略的点LLM API的计费模型。绝大多数商用LLM API按输入输出Token总量计费也就是说你即便把请求数限得再死只要每个请求的上下文特别长账单和推理服务的压力依然会失控。这就意味着光在网关上限制每秒钟的调用次数根本控制不住成本和资源消耗。1.2 LLM请求的容错特殊性快速失败不再是好策略传统API遇到限流通常的做法是直接丢掉多余的请求返回503或者429客户端退避重试即可。请求本身是幂等的、廉价的、无状态的失败了重来一次几乎没有心理负担。LLM调用不一样。用户可能已经等了两三秒前端界面已经渲染出“正在思考”的状态此时后端如果直接返回一个 429前端大概率只能给用户弹一个错误提示而用户精心编辑的那一大段Prompt也白白丢了。更致命的是某些场景里LLM接口不是幂等的比如你已经把它接入了支付订单摘要、自动审核、知识库写入等动作直接丢弃请求可能导致业务状态不一致。因此LLM应用的限流不能只是简单地“挡掉请求”。比较合理的设计是把“能扛就扛、扛不住降级、降级不了再拒绝”做成一条完整的链路。比如面对突发流量时先用排队机制把请求缓冲住而不是立刻拒绝如果排队也满了再考虑返回 429并由客户端配合重试策略。所有这些能力都依赖一个前提你有一个足够精细的限流系统能对请求做分级、分优先级的处理。这也是为什么令牌桶和滑动窗口这类算法在LLM网关里不是“二选一”的关系而往往需要组合使用。2. 令牌桶、滑动窗口的方案选型与实际实现2.1 令牌桶算法为什么适合LLM管控限流算法里最常被提起的就是令牌桶Token Bucket和漏桶Leaky Bucket。不少初学者会把两者混淆但实际上它们应对的场景很不同。漏桶的核心是把请求全部丢进一个固定容量的桶里以恒定的速率流出优点是把流量“削峰填谷”得非常彻底缺点也一样明显无法应对突发流量。如果用户突然有一个大的并发请求进来即使系统此刻完全有能力处理漏桶也会把它们死板地卡在队列里。令牌桶则相反。它以固定的速率往桶里放令牌桶的容量决定了最大突发量。只要桶里有令牌请求就可以立刻被处理这意味着系统允许一定程度的突发流量天然适配“上一秒空闲、下一秒突然来一大批请求”的LLM网关形态。我在LLM网关里尤其看重令牌桶的另一个原因**它可以天然拆成两个维度的限流——放过多少请求以及每秒放多少Token额度。**你可以把桶里的每个令牌理解为“允许消费一定数量Token的凭证”。当用户做一次补全请求时网关根据请求的预估Token消耗量先从桶里扣除对应令牌数不够就排队或者报错。这样设计之后限流系统从一个单纯的“每秒请求次数闸门”升级成了“按Token预算分配资源”的系统更贴近LLM推理服务和API计费的底层计量方式。实际项目里我会同时维护两个令牌桶一个控制请求并发量例如每秒最多接收20个请求另一个控制Token消耗速率例如每分钟不超过10万Token两道闸门一起生效缺一不可。2.2 令牌桶在单机环境的核心实现先看一个单机版本的令牌桶实现用于理解核心逻辑。生产环境如果服务是多实例部署就要换成后面的Redis方案。我们用Python写一个简洁的令牌桶思想是“懒计算”不需要后台线程每隔固定时间往桶里放令牌而是在每次请求进来时通过当前时间和上次填充时间之间的差值计算出这段时间应该补充多少令牌。import time class TokenBucket: def __init__(self, capacity: int, refill_rate: float): capacity: 桶的最大容量决定最大突发能力 refill_rate: 每秒补充的令牌数 self.capacity capacity self.refill_rate refill_rate self.tokens capacity self.last_refill time.monotonic() def consume(self, tokens: int 1) - bool: now time.monotonic() # 先补充令牌 elapsed now - self.last_refill self.tokens min(self.capacity, self.tokens elapsed * self.refill_rate) self.last_refill now # 判断是否足够 if self.tokens tokens: self.tokens - tokens return True return False这里有个关键细节就是使用time.monotonic()而不是time.time()。因为系统时间可能被NTP校准或者人工调整一旦向后跳变time.time()会让令牌桶瞬间多出大量令牌导致限流失效。time.monotonic()是单调递增的不受系统时间跳变影响这算是一个很细节但生产环境必须注意的点。2.3 滑动窗口从固定窗口的临界问题说起固定窗口算法最大的毛病在于临界突发假设我们限制每分钟100次请求窗口从12:00到12:01用户在第59秒打满100个请求紧接着在12:01的第1秒又可以再打100个请求。这200个请求的实际间隔只有两秒如果后端服务承受不住这种毛刺一样会被打爆。滑动窗口就是为了解决这个问题出现的。最精确的做法是滑动窗口日志Sliding Window Log保存每个请求的时间戳当新请求到来时剔除掉窗口之外的时间戳然后统计窗口内的请求数。好处是精确坏处是每个请求都要在内存里存一个时间戳对高QPS服务来说内存开销和清理成本都不低。实际工程里更常用的是滑动窗口计数Sliding Window Counter。它把时间轴继续切成小份比如每1秒一个小窗口记录每个小窗口内的请求数。当请求到达时把当前时间对应的小窗口计数加一同时把所有超出当前大窗口范围的小窗口数据剔除。这时候统计的不是精确值而是近似值但误差通常可控内存占用却比日志法小得多。我在LLM网关里的具体选择是对“请求数量”维度的限流用滑动窗口计数因为请求量通常比较密集需要平滑限制对“Token消耗”维度的限流用令牌桶因为Token消耗是持续性的令牌桶更贴合配额均匀消耗的特征。这里顺带说一个很多人踩过的坑直接用Redis的EXPIRE过期Key来实现滑动窗口看起来简单其实坑很多。比如用List存储每个请求的时间戳窗口一大、请求一多List长度会膨胀得很夸张。一次窗口扫描的复杂度是O(N)高并发下Redis会被这些命令拖垮。后面会专门讲怎么用Lua脚本做比较可靠的生产级实现。2.4 全局多实例限流核心问题与解决思路单机令牌桶在开发环境足够用了一旦网关部署了多个实例比如Kubernetes里三个副本每个实例各自有一个独立令牌桶那就等于把总限流配额放大到了副本数倍。三副本的网关配置了每秒100个请求实际可能放进来300个限流形同虚设。要解决多实例下的全局限流最直接的想法是引入一个集中式的存储所有实例共享同一份令牌数据。Redis几乎成了这个场景的标准答案因为它的读写性能足够高还支持Lua脚本原子性执行很适合实现“检查令牌是否足够、够则扣减、不够则拒绝”这个需要原子性的操作。但引入Redis也引入了两个新问题。第一个是性能和延迟每次请求都要走一次网络往返虽然通常只有零点几毫秒但在极高频的调用下还是会有影响。第二个是Redis本身成为单点Redis一挂整个网关的限流能力就失效了。我的做法是在网关本地做一个小的宕机降级开关当Redis连续心跳失败时自动切换成单机本地限流模式配置一个相对保守的本地阈值虽然整体请求量会下降但总比完全不设防好。3. 基于Redis与Lua的生产级令牌桶设计3.1 Lua脚本为什么能保证原子性现在来看生产级的实现。Redis执行Lua脚本是原子性的脚本运行期间不会被其他客户端的命令插入。这意味着我们可以把“获取令牌状态、计算补充令牌数、扣减令牌”这几步操作全部放进一个脚本里避免多个实例并发更新同一个Key时出现超卖。举个例子如果不用Lua脚本两个请求同时读到当前还剩10个令牌都判断为可以放行并各自扣减最终令牌数可能变成负数限流的准确性就被打破了。Lua脚本能保证读和写在一次Redis调用里完成从根源上规避了竞态条件。这里有个工程设计上的取舍不要在Lua脚本里做太复杂的逻辑最好把脚本控制在几十行以内。因为Redis本身是单线程模型如果每个脚本都执行很长时间比如循环遍历一个大集合会阻塞其他正常请求Redis性能就会下降。限流脚本应该只做简单运算和几个Key的读写通常执行时间在微秒级别不会对Redis造成明显压力。3.2 完整实现Redis Lua令牌桶脚本下面是我在项目里使用的令牌桶Lua脚本注释已经把每一步的逻辑说明白。Redis的有序集合在这里用得非常巧妙令牌本身并没有实体存在而是通过当前时间和上次补充时间的差来计算令牌数。这样就不需要为每个令牌维护一个实际对象。-- KEYS[1]: 令牌桶对应的key -- ARGV[1]: 当前时间戳秒 -- ARGV[2]: 桶容量 -- ARGV[3]: 每秒补充速率 -- ARGV[4]: 本次请求需要消耗的令牌数 local bucket_key KEYS[1] local now tonumber(ARGV[1]) local capacity tonumber(ARGV[2]) local refill_rate tonumber(ARGV[3]) local need tonumber(ARGV[4]) -- 使用hash存储令牌桶状态 -- field: tokens 当前令牌数 -- field: timestamp 最后补充时间 local tokens tonumber(redis.call(hget, bucket_key, tokens)) local last_refill tonumber(redis.call(hget, bucket_key, timestamp)) if tokens nil then tokens capacity last_refill now end -- 计算时间差补充令牌 local refill_amount (now - last_refill) * refill_rate if refill_amount 0 then tokens math.min(capacity, tokens refill_amount) last_refill now end if tokens need then tokens tokens - need -- 写回更新后的令牌数和时间 redis.call(hset, bucket_key, tokens, tokens) redis.call(hset, bucket_key, timestamp, last_refill) -- 同时设置过期时间避免key成为永久垃圾 redis.call(expire, bucket_key, 60) return 1 else redis.call(hset, bucket_key, tokens, tokens) redis.call(hset, bucket_key, timestamp, last_refill) redis.call(expire, bucket_key, 60) return 0 end这段脚本在Redis里的表现稳定但由于每个实例每次请求都要去Redis执行一次EVALSHA网络开销还是有的。批量限流场景下我推荐用pipeline或者redis-cli --eval预先加载脚本不要每次请求都发送完整脚本代码减少不必要的带宽和解析开销。3.3 滑动窗口的生产化实现思路接着看一下滑动窗口的生产实现。同样基于Redis和Lua我用了一个更节省内存的近似方案把时间窗口切分成固定大小的小格比如1秒一格用Hash存储每个小格的计数。-- KEYS[1]: 滑动窗口对应的key -- ARGV[1]: 当前时间戳毫秒 -- ARGV[2]: 窗口大小毫秒 -- ARGV[3]: 小格大小毫秒 -- ARGV[4]: 窗口内允许的最大请求数 local window_key KEYS[1] local now tonumber(ARGV[1]) local window_size tonumber(ARGV[2]) local bucket_size tonumber(ARGV[3]) local limit tonumber(ARGV[4]) local current_bucket math.floor(now / bucket_size) local window_start now - window_size -- 获取所有小格的key和计数 local counts redis.call(hgetall, window_key) local total 0 local i 1 while i #counts do local bucket tonumber(counts[i]) * bucket_size local count tonumber(counts[i 1]) -- 只统计在窗口内的小格 if bucket window_start then total total count else -- 清理过期小格释放内存 redis.call(hdel, window_key, counts[i]) end i i 2 end if total limit then return 0 end -- 当前小格计数加一 redis.call(hincrby, window_key, tostring(current_bucket), 1) redis.call(expire, window_key, math.ceil(window_size / 1000) 1) return 1需要说明的是这是一个近似滑动窗口精确度取决于小格的大小。理论上当小格无限小的时候精度趋向于滑动窗口日志法的精确结果但内存占用也会无限膨胀。工程上我一般取窗口大小的1/10到1/20作为小格大小。比如一个10秒的窗口每0.5秒一格精度已经能满足绝大部分场景。这个脚本在执行时有大量Key清理操作如果Redis里堆积了大量过期的Hash field建议额外加一个后台任务周期扫描清理防止内存持续膨胀。我就遇到过因为滑动窗口Key忘记设置过期时间导致Redis内存缓慢增长的问题排查了很久才定位到是一个只写不删的限流Key在作怪。3.4 Redis集群环境下的Key设计在Redis集群模式下KEY[1]和脚本内其他Key必须落在同一个Hash Slot上否则执行会报错。解决办法是把所有相关Key设计成相同的Hash Tag比如用{rate_limit:user_123}:token_bucket的形式花括号里的内容决定了Key的Hash Slot。我在代码里对限流Key做了统一的命名规范rate_limit:{scope}:{identifier}:traffic -- 请求数限流滑动窗口 rate_limit:{scope}:{identifier}:tokens -- Token配额限流令牌桶scope是限流层级比如全局、某个API Key、某个用户ID、某个模型名。identifier是对应层级的标识。这样分的好处是可以通过Redis的Key前缀直接查看某个用户当前的所有限流状态排查问题时特别方便。注意Hash Tag的使用要克制。如果所有Key都套上同一个Hash Tag例如全部写成rate_limit:{global}:xxx会导致这些Key全部落在同一个Redis分片上集群的分片均衡就被破坏了。要让不同层级的Key用不同范围的Tag比如用户级就用用户ID做前缀避免全局热点。4. API配额管理的分层设计4.1 为什么配额管理不是简单加一个计数器Rate Limit和Quota配额在概念上的区别经常被忽略。Rate Limit关心的是“单位时间内最多允许多少”比如每秒20个请求Quota关心的是“一段时间内总计可以用多少”比如这个月总共可以用1亿Token。Rate Limit是瞬时的水流闸门Quota更像是一个月的流量包。LLM网关的配额管理必须同时处理这两个维度。用户一次大请求可能瞬时消耗2万Token瞬时速率没问题比如每秒只发了一个请求但已经快把这个小时的低优先级配额额度用完了。所以配额系统实际上是一个“多维度的请求记账系统”而不只是计数器。我的做法是采用层级配额树从最上层的组织资源池开始逐层往下分配每个层级有自己的速率限制和预算限制。一个具体的请求会被应用到五个层级从全局到单个API Key逐层判定。层级维度示例配置作用全局所有模型总速率总量不超过后端推理服务上限的80%保护自建推理服务不被打爆模型级单个模型的速率GPT-4级别模型共享一组配额防止某个热门模型拖垮其他模型用户组团队月度Token预算A组每月5000万Token控制成本支出用户每分钟请求数每用户每分钟不超过20次保证公平使用API Key单Key并发数单Key并发不超过5个防止异常调用这里要强调一个思路配置时要为“慢请求”预留余量。LLM请求动辄二三十秒不是秒回如果只是把速率限制在某个值但并发数没有限制照样会让后端频繁排队甚至超时。实践中我习惯在配置模型级限流时为响应极慢的长尾请求预留20%-30%的配额缓冲。4.2 预扣与补偿处理长耗时请求的关键LLM请求还有一个与普通请求不同的特征请求发出去到真正结束中间你不知道它到底会消耗多少Token。有些API支持返回usage字段但那是请求完成之后才有的信息。当请求还在跑的时候你该怎么记账我采用的是**预扣Reserve 实际结算Settle**模式。请求进来时根据Prompt长度估算一个大概的消耗量先从配额里扣掉这部分请求结束后拿到实际的usage数据把多扣的部分退回或者补扣差额。# 伪代码示例预扣与结算 async def call_llm_with_quota(request): estimated_cost estimate_token_usage(request.prompt, request.max_tokens) # 预扣 if not quota.reserve(user_id, model_name, estimated_cost): raise RateLimitExceeded(token quota exceeded) try: response await llm_client.chat(request) actual_cost response.usage.total_tokens # 实际结算多退少补 quota.settle(user_id, model_name, estimated_cost, actual_cost) return response except Exception: # 请求失败退回所有预扣的额度 quota.refund(user_id, model_name, estimated_cost) raise这里容易踩的坑是预扣的估算值误差太大。如果Prompt里有很长的历史对话估算低了实际消耗可能远超预扣值导致用户额度超支。更稳妥的做法是预扣时取一个比较保守的上限值宁可多扣最后再退。因为退款操作在系统里比补扣好处理得多——补扣意味着用户额度已经变成了负数需要额外的“欠费”处理逻辑。另外一个细节是失效请求的退款机制。如果用户在等待LLM返回时取消请求或请求因网络错误中断网关必须能感知到并主动退款否则用户额度会被白白消耗。这个逻辑不能依赖客户端主动通知而要在网关层面对所有未正常完成的请求统一做补偿。4.3 多维配额优先级高优先级用户如何“抢”资源生产环境里不可能人人都拥有相同的配额优先级。企业内部的LLM网关通常要区分部门、项目甚至是单个服务的优先级。我在设计里引入了一个权重因子决定当某个共享配额池快耗尽时谁有资格继续使用。具体做法是给每个请求打上优先级标签low、default、high、critical。限流系统在处理请求时先检查当前配额使用率配额余量充足使用率低于70%所有优先级都放行。配额余量一般70%-90%只放行default及以上优先级low请求开始排队或降级。配额余量紧张超过90%只有high和critical请求能通过其余一律拒绝并提示稍后重试。这种分级策略在实际业务中拿捏得比较准。比如内部有批量数据分析任务低优先级和一个正在给客户实时演示的对话应用高优先级当GPU推理资源紧张时系统会自动把算力优先让给后者而不是一视同仁地暴力拒绝。实现上并不复杂就是在限流脚本里多传入一个优先级参数然后按优先级使用不同的阈值判断。真正的难点在于运维侧要维护好各个服务的优先级定义否则会出现“所有人都标成critical”的尴尬局面。5. 工程化落地中的踩坑记录与解决建议5.1 时钟不一致带来的限流误差多实例部署时各个实例的系统时间如果不同步限流判断就会出现误差。令牌桶依赖时间戳计算补充的令牌数如果一台机器的时间比Redis服务器快了10秒它可能一次性补充了10秒的令牌量瞬间放行大量请求。我在部署检查清单里强制要求所有网关实例统一使用NTP时间同步并且在代码里避免把纯客户端时间作为唯一依据。实际上更稳妥的办法是网关定期向Redis请求一次服务端时间以Redis时间为准计算token补充。因为令牌桶状态本身存在Redis上让Redis来决定“当前时间是多少”逻辑上更自洽。用Redis的TIME命令获取时间是原子操作开销可控。这里要注意不要在Lua脚本里直接调用redis.call(time)因为Redis脚本执行期间不允许调用会改变服务器状态的命令但TIME是只读的实际可以用。如果担心兼容性更通用的方案是在调用脚本前用一次TIME命令从Redis拿到时间然后作为参数传入脚本。5.2 限流应对突发流量时应该在网关上做哪些事限流不只是“拒绝请求”网关的职责还包括让被拒绝的请求用更合理的方式处理。我在网关里做了三个层级的响应策略第一排队。突发流量来临时把暂时无法处理的请求放进一个长度有限的队列排队等待令牌补充。这一步能吸收掉大部分尖峰流量因为LLM请求天然有等待体验用户对几秒钟的排队并不敏感。队列长度要控制好我一般只保留几十个位置满了就进入下一级处理。第二降级重试。如果是低优先级的非实时任务比如批量总结、离线内容生成可以返回一个“任务已接收后台排队处理”的异步结果而不是直接报错。同时建议客户端配合指数退避算法在429之后等待随机时间再重试。第三拒绝并明确告知。所有排队和降级都失效后返回标准化的错误响应。响应头里带上Retry-After字段告诉客户端多久之后可以重试。这个字段在很多HTTP客户端库里有内置支持能自动处理重试等待。HTTP/1.1 429 Too Many Requests Content-Type: application/json Retry-After: 15 { error: { code: rate_limit_exceeded, message: 请求过于频繁请在 15 秒后重试, retry_after: 15 } }很多初学限流的人会忽略一个工程点限流系统自身要做成“可观测的”。每个请求是因为哪个维度的哪个限制被拒绝的必须以结构化日志的形式记录下来。否则线上出问题时运维根本不知道用户到底是被全局限流拦了还是被自己的配额拦了排查问题会非常痛苦。我会在网关日志里记录完整的拒绝链路实例ID、用户ID、限流Key、当前配额使用率、被哪个策略拒绝。5.3 常见故障速查表我把生产环境里比较容易踩的坑整理成了表格这些基本都是有真实案例支撑的可以直接当排查手册用。现象可能原因排查方式解决方案明明配置了全局限流实际请求量超过预期好几倍多个网关实例各自维护了独立的本地令牌桶检查Redis里全局限流Key是否存在各实例日志中本机放行量是否接近改用Redis Lua全局限流关闭本地独立限流Redis偶发超时网关所有请求被阻塞限流操作在请求的关键路径上Redis延迟直接影响主流程查看Redis慢日志和延迟监控给Redis限流Key设置合理的本地缓存或者把限流模式改为异步上报用户额度莫名其妙被扣光预扣完成后请求失败没有正确退款核对结算日志计算预扣与最终结算差额完善异常处理请求失败必须补偿限流Key越积越多Redis内存不断增长限流Key没有统一设置过期时间用redis-cli --bigkeys扫描大Key和过期Key分布在脚本中统一设置EXPIRE增加定期清理任务某个热门模型的请求把其他模型的资源挤占一空全局配额只有总量限制没有做模型级隔离查看各模型的实际并发数、请求量增加模型级限流和优先级调度客户端收到429后疯狂重试导致限流系统被打出更高流量客户端没有使用指数退避策略查看网关日志里重复请求的UA或IP特征集成标准429处理机制响应头返回Retry-After客户端按退避算法重试同一用户的多个请求几乎同时到达全部被放行令牌桶代码有竞态检查与扣减不是一次性原子操作用并发压测复现改写为Redis Lua脚本保证原子性5.4 本地熔断与兜底机制最后讲一个比较重要的工程兜底思路。接入Redis做全局限流之后Redis本身就成了新的单点。假如Redis服务不可用网关不能就此失去限流能力。我的方案是本地二级限流兜底每个网关实例启动时加载一份本地配置包含一个相对保守的实例级限流阈值。当Redis健康检查连续失败达到阈值比如连续3次连接失败网关自动进入“本地限流模式”每实例按自身配置独立限流。这里合理的预期是降级期间系统整体吞吐会下降但服务不会崩溃。等Redis恢复后网关自动切回全局限流模式这里要加一个平滑过渡机制避免切换瞬间出现大量请求积压。注意本地限流的阈值不能设置成“全局阈值除以实例数”的精确值因为你无法保证多个实例流量完全均衡。保守一点的策略是把每个实例的本地阈值设置为全局阈值的40%-50%宁可牺牲一点吞吐也要保证兜底时不会打爆后端。实操过程中的几点体会这套系统上线后我们经历过几次比较典型的流量冲击比如市场部门做活动时短时间内涌进大量用户请求过去可能直接导致后端推理服务被打满升级网关限流体系之后高峰期的429错误率反而降到了1%以下后端稳定性和账单曲线都平滑了很多。从实际运维数据来看LLM应用的限流做得越精细系统整体稳定性收益越明显。如果现在让我重新设计一次可能最想早期就做好的事情有两件一是把“可观测性”前置所有限流拒绝和额度结算都应该从第一天就开始记录结构化日志别等到出了问题再补二是给客户端提供标准化的SDK封装把429处理、指数退避、超时重试都内置进去否则你网关设计得再好客户端实现不规范一样会把后端打穿。最后分享一个很实际的小技巧上线限流策略时先在灰度环境把阈值设成正常值的五分之一压上一批真实业务流量观察各个层级的限流触发频率是否合理再逐步调回目标值。这一步能帮你提前发现很多配置问题比如某个层级的配额写得太小、某个模型的限流维度配错了远比直接上生产后再回滚要省心得多。
返回列表