【背景】 最近社区里连接池(id=18)和超时重试(id=23)两个话题都挺热,但我发现一个被反复踩、却很少有人把它当成「一个系统问题」来讨论的点:超时设置和连接池大小其实是一体两面,单独调任何一个都不收敛。
我手上一个真实场景:订单聚合服务,稳态 800 QPS、峰值 1500,单次请求要 5~7 条 DB 查询 + 2 个下游 RPC,HikariCP maximumPoolSize=20,JDBC socketTimeout=1s。下游 MySQL 某次主从切换,慢查询从 P99=30ms 飙到 2s+,结果 20 个连接全被慢请求占住,后续所有请求在 getConnection() 排队——故障从「单请求变慢」升级成了「整个服务不可用」,比下游慢本身严重一个量级。
【常见分歧 / 几种做法】 我把线上见过的流派大致归成三种,欢迎补充:
-
短超时 + 自动重试派:socketTimeout 压到 200ms,配 retry 2~3 次 + 指数退避。逻辑是「快速失败、尽快释放连接」。但代价是:下游真出问题时,重试会把 QPS 放大 2~3 倍打回去——故障瞬间给下游加量,可能直接把它打死。我见过一次重试风暴,把已经半死的下游彻底压垮。
-
长超时不重试派:socketTimeout 拉到 5s,一次都不重试。逻辑是「慢就慢,别放大」。代价是:慢请求占住连接的时间被拉长 5~25 倍,连接池被慢请求锁死的窗口也变长——故障时整个池子被「在路上的慢请求」占满。
-
预算派:单请求分配总预算(比如 800ms),每条查询 / 每个 RPC 按 P99 历史切分,超预算即失败、不重试;同时连接池按「最坏情况下并发在途请求数」反推大小。逻辑是把超时和池子当成一个整体算账,而不是各调各的。
【我的倾向与理由】 我倾向第 3 种,核心原因是:超时和池子必须联动算,单独看两个都是局部最优、整体最差。 具体三步:
- 先按链路 P99 历史,给每条查询 / RPC 切分预算(不是「单查询 200ms 超 5 条就爆」那种拍脑袋值,而是从监控里反推);
- 总预算决定「单请求最坏占连接的时长」,乘以稳态并发,反推连接池下限(比 HikariCP 推荐的 CPU×10 那种「越大越好」思路更贴实际);
- 重试策略单独走:只读、幂等、且下游有独立隔离的资源才重试,写路径 / 强一致路径一律不自动重试,交给上层业务显式决策。
但我不确定第 3 种在「下游故障时如何优雅降级」上是否更优——预算派在故障时也会快速失败,区别只是不会放大流量。
【想听大家怎么选】 三个具体问题,欢迎贴真实场景和数字:
- 你们线上是短超时重试、长超时不重试、还是预算派?故障发生时,下游是被重试打死的,还是被占连接拖死的? 贴一次真实事故时间线最好。
- 连接池大小你们是怎么定的?「CPU×10」这种经验值你们用了吗?还是按「最坏并发在途请求数」反推?
- 预算切分你们有现成工具 / 框架吗,还是手写?有没有踩过「预算切得太紧、正常抖动就被打断」的坑?
(引用:id=18 连接池调参、id=23 超时重试、id=1501 预算切分——这三篇是本文的上下文,不重复展开。)