单体服务的容量,不是“一个进程”的容量
单体服务的容量,不是“一个进程”的容量
很多团队认为限流熔断是微服务才需要的东西:单体应用所有调用都在进程内,不会有网络失败。但真实故障恰好相反:单体把所有接口塞进同一个 Tomcat 线程池(默认 maxThreads=200),只要一个慢接口把线程占满,整个进程的所有接口会一起超时,爆炸半径从“一个服务”变成“一个进程内的全部业务”。所以单体里限流的目标不是防外部攻击,而是防止某个接口独吞公共资源(线程、连接、内存)。
第一层:按接口分组做并发隔离
先做并发数隔离,不要一上来就做 QPS 限流,因为线程被打满才是进程级死亡的直接原因。在 Filter 里按 URI 前缀分组,每组一个 Semaphore:
- 普通查询组
permits=60,下单组permits=40,报表导出组permits=8(导出最容易拖垮 JVM) tryAcquire(10, TimeUnit.MILLISECONDS)拿不到直接返回 429 并带Retry-After: 1,不要排队等待- 释放必须放在
finally中,并排除 Servlet 异步请求和 WebSocket 长连接,否则会提前归还令牌
初始阈值别拍脑袋:用压测得出该接口 P99 响应时间,按“目标 QPS × P99 响应时间 ÷ 1000 × 1.5”估算初始并发数,再观察一周调整。
第二层:给下游依赖装熔断器
单体里典型的下游是第三方 HTTP、Redis、DB。DB 靠连接池上限加超时兜底,通常不适合熔断;HTTP 下游则非常适合 Resilience4j 的 CircuitBreaker。关键在别用默认值:
slidingWindowSize=100配minimumNumberOfCalls=20,否则凌晨低谷时 3 次失败就跳闸failureRateThreshold=50,同时配slowCallRateThreshold=60和slowCallDurationThreshold=2s,慢调用也要计入失败waitDurationInOpenState=30s,permittedNumberOfCallsInHalfOpenState=5,半开放行探测成功才闭合
粒度按“下游系统 + 接口分组”,不要整个服务只挂一个熔断器,否则一个边缘接口故障会把主流程一起断掉。
第三层:降级要有返回值,不是抛异常
熔断打开后最怕的是异常直接抛到前端,用户看到 500。给每个被保护调用准备兜底:商品详情读本地缓存、推荐位返回空列表、积分查询返回“稍后重试”。降级分支必须打点,例如 counter("fallback.total", "reason", reason),否则你永远不知道降级实际跑了多久、影响了多少人。同理,429 也要带上明确的错误码,让前端能区分“限流”和“真故障”。
落地顺序与验证
- 先接监控再开限流:用 Micrometer 暴露熔断状态、拒绝计数、信号量等待时长,接入 Prometheus 看板
- 阈值放配置中心并支持热更新,出问题时能在 30 秒内调回去,而不是重新发版
- 用
tc qdisc add dev eth0 root netem delay 3000ms或混沌工具注入延迟,验证熔断是否在 10 次调用内打开、恢复后是否自动闭合 - 上线灰度时先“只记录不拦截”(shadow 模式),对照日志确认阈值合理再真正拒绝
- 点赞
- 收藏
- 关注作者
评论(0)