单体服务不是没有"下游"
单体服务不是没有"下游"
很多团队觉得限流熔断是微服务才需要的东西,单体应用里"函数直接调用,哪来的网络故障"。但一个真实请求往往串行依赖 MySQL、Redis、第三方支付、短信网关,还和所有其他接口共享同一个 Tomcat 线程池。任何一个慢依赖都能把 200 个工作线程吃干净,然后整个应用 502,包括那些本来完全健康的接口。这就是单体最典型的故障放大:没有隔离,故障就是全站级的。
入口限流:先保住线程池
第一道闸门放在 HTTP 入口。先确认 server.tomcat.threads.max(常见 200)和 server.tomcat.accept-count(100),这两个数字决定了你的服务最多能"卡住"多少请求。然后用 Resilience4j 的 RateLimiter 或 Sentinel 的 FlowRule 做单机限流,粒度按"接口 + 调用方"划分,比如 /api/export 对每个租户限 5 QPS,而不是只做全局限流——全局限流会让一个刷接口的租户挤掉所有人。
被限流时返回 429 并带上 Retry-After,不要图省事返回 500,否则上游会重试,把限流变成雪崩。单机限流优先用 Bucket4j 这类本地令牌桶,别为了"精确"上 Redis 集中限流,那样限流器自己就成了新的故障点。
熔断要挂在"依赖边界"上
熔断器的划分维度是依赖,不是接口。paymentClient、smsClient、userCache 各一个,这样支付挂了只影响支付,不会把读缓存也一起断掉。典型配置:slidingWindowSize=20、minimumNumberOfCalls=10、failureRateThreshold=50、waitDurationInOpenState=30s、permittedNumberOfCallsInHalfOpenState=3。
比异常率更值得开的是慢调用熔断:slowCallDurationThreshold=2s。线上大部分故障不是抛异常,而是"变慢",异常率熔断往往等线程池已经打满才触发。第三方调用建议配独立线程池隔离(core=8, max=16, queue=32),队列满了直接拒绝,绝不要用无界队列——无界队列等于没有熔断。
几个真实踩过的坑
- 熔断器 key 用完整 URL 动态创建,带了 query 参数,导致熔断器实例数量爆炸、内存缓慢泄漏。key 必须收敛到有限集合。
- 半开状态放行太多请求,恢复瞬间把刚缓过来的下游再次打挂。半开只放 1~3 个探测请求。
- 阈值按日均 QPS 定,没看峰值,大促时把正常用户限掉了。阈值应取历史 P99 再留 30% 余量。
- 降级返回
null而不是空对象,上游 NPE,故障从"慢"升级成"错"。 - 熔断打开没有告警,靠用户投诉才发现。打开时必须打点 + ERROR 日志。
落地顺序:先超时,再熔断,最后限流
收益排序很清楚:第一步给所有外部调用加超时(连接 1s、读 3s),这一步几乎不写代码却能解决大半问题;第二步按依赖加熔断;第三步才是入口限流。灰度上先跑 count 模式,只统计不拦截,观察一周确认阈值合理后再开拦截,否则第一次上线就可能误伤正常流量。
- 点赞
- 收藏
- 关注作者
评论(0)