Agent高并发防护:缓存、限流、负载均衡、熔断四层策略
先看一个很常见的线上场景网关监控面板上普通接口稳稳跑在 1000 QPSP99 只有几十毫秒旁边的 Agent 接口只有不到 100 QPS服务却已经出现大量超时和 5xx数据库连接池被打满内存一路飙升最后只能重启。很多人第一次接触 Agent 服务时会下意识把它当成“普通接口”来优化加缓存、扩线程池、调 JVM 参数。结果发现这套常规手段几乎不奏效。原因很简单——Agent 接口根本不是普通接口。它是带着长耗时、大模型外部依赖、上下文状态和成本消耗的“重事务”。普通接口几百毫秒内能结束的战斗Agent 接口要跑几秒甚至几十秒。这篇文章的核心判断是Agent 高并发防护重点不是“把单机吞吐压到极限”而是“通过分层手段把每一层的压力控制在服务能力范围内”。我会结合缓存、限流、负载均衡、熔断四层防护讲清楚每一层分别解决什么问题、适合用什么方案、最容易踩哪些坑。如果你正在做 Agent 应用或者准备把 Agent 能力接入生产环境这篇文章能帮你建立一套可落地的防雪崩思路。1. Agent 高并发为什么比普通接口“猛多了”在动手设计防护之前先理解 Agent 接口和普通接口在高并发下的差异。不是并发数更大而是崩溃路径完全不同。对比维度普通接口Agent 接口单次响应时间几十毫秒到几百毫秒几秒到几十秒长任务可能分钟级资源占用CPU、内存、数据库查询长连接、线程池长期占用、上下文内存、外部模型调用下游依赖数据库、缓存、内部服务大模型 API、向量库、外部工具任何一个都不可控是否有状态通常无状态多轮对话、会话上下文、结果聚合天然有状态业务特征读多写少规律性强请求相似度高但表述千变万化难以精确缓存失败模式快失败释放资源慢失败线程和连接耗尽后才暴露所以 Agent 服务的高并发问题本质上是“慢请求积累”问题。当流量进入后每个请求都在长时间占用一个线程、一条连接、一段内存甚至一次昂贵的模型调用。一旦入口流量超过系统的并发处理能力线程池先满随后请求排队接着超时最后触发级联问题。三种最典型的崩溃路径第一线程池耗尽。Agent 请求平均耗时 5 秒如果线程池是 200 个线程理想情况下只能支撑 40 QPS。超过这个值所有请求都要排队接口响应时间从“慢”变成“不可用”。第二数据库连接池被打满。Agent 内部可能涉及多轮查询、会话存储、结果写入长事务会把数据库连接池占满直接拖累同进程里的其他普通业务接口。这才是“雪崩”最可怕的地方——Agent 接口挂了把不相关业务也带崩。第三大模型 API 触发限流或超时。外部 Provider 有 QPM、TPM 限制并发打上去之后模型请求大量失败。此时如果业务代码没有兜底等待、重试、再失败形成恶性循环。理解这个前提之后再来看四层防护要怎么设计。缓存是减法限流是闸门负载均衡是分摊熔断是断臂自救。2. 四层防护的整体定位与前置条件在写代码之前先明确每一层在整个高并发防护链路中的职责避免出现“配置了工具但不知道解决什么问题”的情况。缓存解决的是“同一个问题不要重复算”。Agent 的高成本来自模型计算如果能命中缓存一次外部调用的成本直接归零。但 Agent 的缓存难点在于用户表达不是精确 Key而是语义相似。限流解决的是“入口流量不能超过系统容量”。不管你怎么优化每台机器能同时处理的 Agent 请求是有限的。限流的意义是在服务被打垮之前先拒绝一部分请求保证核心用户可用。负载均衡解决的是“流量不能集中在一台机器上”。通过水平扩展让多台实例分摊压力同时让流量在生产环境中可以灰度发布。熔断解决的是“下游不行的时候自己要快速失败”。大模型 API、向量库、外部工具都可能变慢或异常熔断能将依赖故障隔离在局部不让它拖垮整个服务。这一套组合需要的基础设施可以沿用你现有的技术栈不需要专门采购新组件。实践时建议具备以下环境应用服务Spring Boot 2.7 或 3.xJDK 17版本以实际项目为准缓存中间件Redis 5.0 以上生产环境建议主从或集群负载均衡Nginx 或云厂商负载均衡服务内部 RPC 场景可以考虑 OpenFeign 自带的负载均衡能力熔断组件Resilience4j、Sentinel或者云平台的熔断策略压测工具wrk、JMeter、Locust 选一个即可下面按“进服务前 → 进服务后 → 调用下游”的顺序逐层拆解。3. 第一层防护缓存把重复消耗挡在门外3.1 Agent 缓存和普通接口缓存有什么不同普通接口缓存是典型的 Key-Value 缓存同一个 userId 查同一个订单直接返回查过的结果。Agent 缓存要复杂得多同一个业务问题用户可能用不同表述去问比如“请用一句话介绍高并发缓存设计”和“帮我总结缓存高并发的设计要点”语义几乎一致但字符串完全不同。如果用用户原文做 Key缓存命中率会很低。所以在 Agent 场景里缓存方案需要拆成两层第一层结果缓存。核心是给请求算一个稳定 Key。做法是先对用户输入做归一化包括去除标点、统一大小写、做同义词替换如果业务允许也可以调用 Embedding 模型为问题生成向量在 Redis 中用向量检索找到最相似的历史问题。命中后再决定是直接复用历史答案还是基于历史答案做二次改写。第二层Prompt 缓存。Agent 的调用链里System Prompt、Few-shot 示例、知识库片段通常是相对固定的。很多大模型接口本身支持 Prompt 缓存把不变的上下文预加载只把变化的用户内容拼接进去能明显降低首字延迟和成本。这一层属于模型服务侧能力业务侧能做的是尽量保证 System Prompt 固定只在尾部拼接动态内容。3.2 缓存层落地示例先看一个最基础的结果缓存配置使用 Spring Cache Redis。// 文件路径src/main/java/com/example/config/CacheConfig.java Configuration EnableCaching public class CacheConfig { Bean public RedisCacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(10)) .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(new GenericJackson2JsonRedisSerializer())); return RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); } }这段配置做了两件事把缓存默认过期时间设为 10 分钟把值序列化方式改成 JSON方便在 Redis 里直接查看内容。实际使用时建议为不同业务配置不同的 TTL。高频但允许轻微过期的数据可以短一点知识库类答案可以长一点。再看一个语义缓存的简化示例。需要说明的是这里用 String 直接做 Key 只是演示存储结构真正的语义匹配需要引入 Embedding 模型和向量检索生产环境建议使用 Redis 的向量模块或专门的向量数据库。// 文件路径src/main/java/com/example/agent/service/AgentCacheService.java Service public class AgentCacheService { Autowired private StringRedisTemplate redisTemplate; private static final String CACHE_KEY_PREFIX agent:cache:v1:; public String getAnswer(String userQuery) { String queryKey normalizeQuery(userQuery); String cached redisTemplate.opsForValue().get(CACHE_KEY_PREFIX queryKey); if (cached ! null) { return cached; } String answer invokeAgent(userQuery); redisTemplate.opsForValue().set(CACHE_KEY_PREFIX queryKey, answer, Duration.ofMinutes(10)); return answer; } private String normalizeQuery(String query) { // 生产环境调用 Embedding 模型生成向量再进行向量相似度匹配 // 演示环境先做简单归一化 return query.trim().replaceAll([。?!], ).toLowerCase(); } }这段代码的关键点是无论缓存怎么做都要考虑穿透和击穿。Agent 场景下如果多个相似问题同时未命中会同时发起模型调用造成不必要的开销。建议结合 Redis 分布式锁做单飞保证同一个 Key 只有一个请求真正调用模型其他请求等待结果后复用。3.3 缓存的坑缓存一致性在 Agent 场景里要求相对低因为答案是生成式内容允许一定程度的时移。真正要警惕的是三个问题第一个是命中率测不准。上线缓存前没有在测试环境模拟真实用户问题分布导致缓存 Key 设计不合理命中率趋近于 0。建议上线前用一批真实用户问题做回放统计相似度分布。第二个是缓存穿透。恶意攻击者或爬虫用大量无意义的随机问题打过来缓存永远不命中全部打到模型服务上。这时候限流比缓存更关键。第三个是缓存雪崩。大量缓存同时过期流量瞬时涌入服务端。解决办法包括缓存过期时间加随机值或者在回源时加锁限流。4. 第二层防护限流控制进入系统的流量4.1 为什么 Agent 接口更依赖限流普通接口限流是为了防止突发流量打垮服务器Agent 接口限流多了一层含义大模型 Provider 本身就有限流。外部 API 通常按 QPM 或 TPM 限制你本地服务再快下游不接一样失败。限流是把自己对下游的请求控制在安全额度内。另外Agent 接口调用成本高、并发容量低限流是“在入口拒绝”和“把系统打挂后被动拒绝”之间的唯一选择。与其让 1000 个请求全部排队超时不如只放 100 个进去保障其中 80 个能正常返回。4.2 限流算法选型高并发场景下限流算法主要有四种各有适用场景。算法原理优点缺点适用场景固定窗口每个时间窗口内计数超过阈值拒绝实现简单内存占用少窗口边界可能出现双倍流量简单场景对边界不敏感滑动窗口细粒度记录每个子窗口的计数解决边界突刺需要保存多个时间片数据对流量曲线要求高的场景令牌桶以固定速率往桶里放令牌请求需要获取令牌允许一定突发流量突发流量可能冲击下游Web 接口限流漏桶请求以固定速率流出超出则排队或拒绝平滑流量保护下游无法应对突发可能增加延迟下游有严格速率要求对于 Agent 接口我的建议是网关层用令牌桶保护自身应用对下游大模型 API 的调用用漏桶或更严格的预计算额度控制因为外部 Provider 对速率更敏感。固定窗口的一个典型问题是窗口切换瞬间可能放行双倍流量。滑动窗口能缓解但代价是内存和计算量上升。实际项目里网关层主流的做法是令牌桶既能平稳限速又允许小规模突发。4.3 限流落地示例下面用 Redisson 的 RRateLimiter 做一个用户维度限流示例。这个实现的核心思路是每个用户一个 Key每个 Key 一分钟最多放行 N 次超出的请求直接拒绝。// 文件路径src/main/java/com/example/agent/controller/AgentController.java RestController public class AgentController { Autowired private RedissonClient redissonClient; Autowired private AgentService agentService; GetMapping(/agent/chat) public String chat(RequestParam String userId, RequestParam String query) { RRateLimiter limiter redissonClient.getRateLimiter(agent:limit:user: userId); // 同一个 Key 每次设置相同的速率不会重置已获取的令牌 limiter.trySetRate(RateType.OVERALL, 10, 1, RateIntervalUnit.MINUTES); if (!limiter.tryAcquire()) { throw new RateLimitException(请求过于频繁请稍后再试); } return agentService.chat(userId, query); } }限流维度要比算法更值得关注。用户维度限流解决“单用户刷爆服务”的问题实例维度限流解决“服务总容量”问题模型维度限流解决“下游额度”问题。实践中三个维度通常都要配优先级从高到低展开。网关层限流是另一道重要入口可以直接使用云网关或 Nginx 等组件完成。与代码内限流相比网关限流的好处是请求不会进入业务线程池占用更少资源限流维度也容易配置。4.4 滑动窗口限流的坑搜索材料里出现“滑动窗口限流存在什么问题”这里展开说一下。滑动窗口虽然解决了固定窗口的边界突刺但实现时往往以“每个时间片一个计数”方式存储窗口越细存储越多。如果时间片只有 1 秒窗口是 1 分钟就要维护 60 个计数。集群场景下需要 Redis 存储每个请求都要读取和更新窗口数据反而比固定窗口更容易成为热点。更稳妥的做法是在网关层用令牌桶解决大部分场景只有对流量精确度要求极高的场景才用滑动窗口。另外无论用哪种算法限流参数必须通过压测得出不能拍脑袋。5. 第三层防护负载均衡让服务能横向分摊5.1 Agent 服务的负载均衡和普通服务有什么不同普通接口的负载均衡主要关注分发策略和健康检查。Agent 服务由于响应时间长、存在流式响应、调用链依赖多对负载均衡层有额外的要求一是连接保持。Agent 请求持续几秒甚至十几秒负载均衡器不能过早断开空闲连接。二是流式响应支持。流式返回时Nginx 或网关需要关闭响应缓冲否则首字时间会被缓冲策略拖慢。三是健康检查要真正反映 Agent 服务的可用性不能只看进程是否存活。最有效的做法是配置一个专门的健康检查接口负载均衡器定期探测接口内部检查下游依赖是否正常。5.2 Nginx 负载均衡配置示例# 文件路径conf/nginx.conf 片段 upstream agent_cluster { least_conn; server 192.168.1.11:8080 max_fails3 fail_timeout30s; server 192.168.1.12:8080 max_fails3 fail_timeout30s; server 192.168.1.13:8080 max_fails3 fail_timeout30s; } server { listen 80; server_name agent.example.com; location /agent/ { proxy_pass http://agent_cluster; proxy_http_version 1.1; proxy_set_header Connection ; proxy_connect_timeout 5s; proxy_read_timeout 120s; proxy_buffering off; } }这里有几个关键配置值得注意。least_conn表示转发给当前连接数最少的实例比轮询更适合长短请求混合的场景。proxy_read_timeout设成 120 秒是因为 Agent 响应慢默认 60 秒很容易误杀正常请求。proxy_buffering off用于流式响应如果后端是 SSE 或流式输出必须关闭缓冲否则用户会感觉模型“很久不说话”。如果是微服务内部调用OpenFeign 默认集成了 Ribbon 或 Spring Cloud LoadBalancer 能力原理相同注意配置超时时间。RPC 场景下网关层负载均衡关心的是外部流量服务注册发现和负载均衡则交给注册中心完成。5.3 多 Provider 负载均衡Agent 服务还要考虑另一层负载均衡模型 Provider 的冗余。很多团队只有一个大模型 API 供应商一旦对方限流或故障整个 Agent 服务就不可用。做法是在应用中抽象一层模型网关为同一个模型配置多个供应商或多个 Key正常时按权重分配某个 Provider 连续失败时自动摘除。这一层的负载均衡策略和流量调度往往比业务层的横向扩容更有效。6. 第四层防护熔断保护系统最脆弱的一环6.1 为什么 Agent 场景必须用熔断缓存解决重复计算限流控制入口负载均衡增加容量但这些都不解决一个关键问题当大模型 API 或向量库已经出现异常时服务该怎么自我保护。如果下游变慢上游服务还不知道继续请求、等待、超时、重试线程池很快被拖垮。熔断器的思想是下游一旦达到失败阈值立即切断对该下游的调用快速返回降级结果避免故障蔓延。6.2 熔断器状态与关键参数熔断器通常有三个状态关闭、打开、半开。关闭状态下请求正常通过但持续统计失败率和慢调用率。当指标超过阈值熔断器打开后续请求直接走降级逻辑不再调用下游。打开一段时间后熔断器进入半开放少量试探请求如果成功恢复关闭状态失败则继续打开。Agent 场景里更推荐看“慢调用率”。大模型 API 经常出现 HTTP 200 但响应时间长达 10 秒以上的情况只看错误率根本发现不了问题。把响应超过 3 秒的调用算作慢调用当慢调用比例超过阈值时熔断器直接打开。6.3 Resilience4j 熔断配置示例# 文件路径src/main/resources/application.yml resilience4j.circuitbreaker: instances: agentLlm: slidingWindowSize: 20 minimumNumberOfCalls: 10 failureRateThreshold: 40 slowCallRateThreshold: 60 slowCallDurationThreshold: 3s permittedNumberOfCallsInHalfOpenState: 3 waitDurationInOpenState: 30s配置说明slidingWindowSize统计最近 20 次调用的结果。minimumNumberOfCalls至少统计 10 次才开始熔断判断防止样本太少引发误判。failureRateThreshold错误率达到 40% 时打开熔断器。slowCallRateThreshold慢调用率达到 60% 时打开熔断器。slowCallDurationThreshold响应超过 3 秒即视为慢调用。permittedNumberOfCallsInHalfOpenState半开状态放 3 个试探请求。waitDurationInOpenState熔断打开 30 秒后进入半开。代码中使用// 文件路径src/main/java/com/example/agent/service/AgentService.java Service public class AgentService { CircuitBreaker(name agentLlm, fallbackMethod fallback) public String callLlm(String query) { return llmClient.chat(query); } public String fallback(String query, Throwable t) { // 降级方案返回兜底话术、读取缓存结果、或切到备用模型 return 当前智能体服务繁忙已为你保留提问记录请稍后重试。; } }熔断的降级不是越复杂越好。最简单可靠的方式是直接返回一个明确的提示同时把请求路由到备用模型。如果既想减少等待又不想丢失请求可以在降级逻辑里把用户问题写入消息队列后续异步补偿处理。7. 四层防护的联动顺序与配置验证7.1 一个请求进来先经过哪一层很多人配置完四层防护之后依然不知道流量到底走了哪条链路。实际上一个请求从外部到模型的完整路径是这样的负载均衡层Nginx 或云负载均衡把请求分发到某个 Agent 实例。网关限流层请求进入实例前先被网关做一次全局限流超出阈值直接返回 429。服务内限流按用户维度、模型维度检查是否超配额超限直接拒绝。缓存层查询语义缓存命中则直接返回结果不进入模型调用。熔断层检查模型调用链路健康状态熔断开则直接走降级。模型调用请求到达大模型 API完成后写缓存返回给用户。缓存层虽然在限流之后但要注意两者的配合。限流是兜底缓存是效率。如果缓存命中率很高真正打到模型的请求很少限流压力也会降低。7.2 缺少某一层会发生什么没有缓存重复问题消耗大量 Token 成本模型 API 经常被顶上限。没有限流大促或攻击流量直接把服务线程池打满所有请求排队超时。没有负载均衡单机崩了业务就完全不可用。没有熔断一次模型 API 故障会拖垮所有线程最终牵连同一进程里的其他接口。这四层不是平行选项而是路径上不同位置的关卡。任何一层缺失整体防护能力都会出现明显短板。7.3 如何验证防护效果配置完成后先用压测验证各层是否按预期生效。可以先用 wrk 打一个简单压测wrk -t4 -c100 -d60s --latency http://agent.example.com/agent/chat?userIdtest01queryhello观察几个关键指标检查指标预期表现QPS达到配置的限流阈值后不再上涨错误率弹性区间内错误率明显下降限流返回 429 而不是 5xxP99 响应时间不再无限上涨超时请求被快速拒绝Redis 缓存命中率缓存生效后命中率曲线上升模型调用量下降线程池活跃数熔断生效后不再长期满负荷下游模型调用量限流和缓存共同作用下调用量被控制在安全额度内8. 常见问题与排查方法8.1 问题速查表问题现象可能原因排查方式解决方案缓存命中率接近 0用户问题变化太多字符串 Key 不匹配统计真实问题分布打印缓存 Key 前缀引入语义缓存/向量检索或对 Prompt 做归一化限流误伤正常用户限流维度设置错误全局共享了用户限额检查限流 Key 的设计看日志里被拒流量的 userId 分布按用户、实例、模型三层分开配置熔断器频繁打开又恢复慢调用阈值设置太敏感查看统计窗口内的慢调用占比和 P99调大 slowCallDurationThreshold 或 failureRateThreshold请求偶尔超时但服务本身没有异常负载均衡层连接的 read timeout 太短查看 Nginx error.log 中的 upstream timed out调大 proxy_read_timeout开启流式缓冲关闭缓存过期后瞬间请求量暴涨缓存雪崩TTL 设置过于集中查看 Redis 中 key 的过期时间分布给缓存 TTL 加随机值或回源时加锁限流模型 API 报限流错误但自身服务负载不高外部 Provider 维度限流未配置看模型调用日志和状态码在模型网关层按 QPM/TPM 做调用前预检8.2 一个实战调试思路如果你的 Agent 接口还是出现了雪崩建议按顺序排查第一步看线程池。线程池被打满说明入口流量超过了处理能力优先检查限流是否生效再看缓存命中率。第二步看下游调用。如果线程池没满但响应特别慢检查模型 API 的耗时分布看一下是否触发慢调用熔断。第三步看缓存。如果模型调用量很高但用户问题大量重复排查语义缓存是否正常工作命中率是否合理。第四步看负载均衡。如果单实例负载不均查看连接数和健康检查状态是否有个别实例被摘除后又频繁恢复。9. Agent 高并发防护的最佳实践9.1 用压测结果而不是直觉来定阈值限流阈值、熔断阈值、线程池大小、缓存 TTL这些参数必须基于压测数据来定。建议先在测试环境做一个压测矩阵纯模型调用无缓存确定单实例最大能承受的并发。开启缓存后观察命中率带来的容量提升。开启限流后模拟超阈值流量确认限流响应是否符合预期。注入模型 API 延时验证熔断是否能及时保护线程池。9.2 监控围绕“请求生命周期”搭建监控不是看几个图表就完了而是围绕请求生命周期来建入口层监控 QPS、限流拦截数、429 比例。应用层监控线程池活跃数、请求耗时分布、熔断器状态变化。下游层监控模型调用量、Token 消耗、调用成功率、平均延时。缓存层监控命中率、缓存 Key 总量、过期驱逐数量。出现雪崩前往往是限流拦截数突然上涨、模型调用耗时持续走高、线程池活跃数接近上限。这几个信号先出现服务不可用是最后的结果。9.3 发布和配置变更要带保护新上线 Agent 功能时不要在高峰期直接放开全部流量。建议先在网关层配置一个小比例灰度观察缓存命中率和模型调用耗时逐步放量。限流参数和熔断参数修改后可以用压测快速验证而不是改完就上生产。9.4 降级方案要能独立于主链路运行降级不等于报错。好的降级是“服务还在能力降级”。模型调用失败时可以先返回最近一次相似问题的缓存结果或者进入人工处理队列。这样用户至少能得到一个可用的结果而不是一句冷冰冰的“系统繁忙”。当然降级逻辑本身要保持简单可靠不要在降级链路上再引入复杂的依赖。10. 给 Agent 服务做一次“防雪崩”自查缓存、限流、负载均衡、熔断单独拿出来都不难理解难的是把它们放到同一条链路上形成一道完整的防线。最后整理一份自查问题你可以拿它去对照自己的 Agent 服务你的重复请求有多少比例能命中缓存如果命中率很低瓶颈是 Key 设计还是用户问题本身就高度离散你的限流是只做了单机维度还是用户、实例、模型 Provider 三层都覆盖了你的负载均衡层有没有针对长连接、流式响应做特殊配置你的熔断器统计的是错误率还是慢调用率大模型超时这种情况你的熔断能不能及时反应模型 Provider 故障时你的服务能不能自动降级还是会把所有线程池资源耗尽后牵连其他业务你有没有一套可用于压测和故障注入的测试环境如果这些问题里有一半回答不上来说明你的 Agent 服务在高并发场景下还处于“裸奔”状态。建议先挑最简单的一层落地比如把用户维度限流先加上把缓存 Key 设计优化一轮再逐步补全其他防护。每做一层都用压测和数据来验证比一次性叠加全部方案更稳妥也能真正看清每一层带来的收益。