腾讯混元 3.5 接入网关层的连接池耗尽排查:从 HikariCP 参数到 Redis 令牌桶...

发布时间:2026/8/9 13:33:55
腾讯混元 3.5 接入网关层的连接池耗尽排查:从 HikariCP 参数到 Redis 令牌桶... 腾讯混元 3.5 接入网关层的连接池耗尽排查从 HikariCP 参数到 Redis 令牌桶漂移的完整复盘上周五晚高峰监控大屏突然报警模型网关 P99 延迟从 120ms 飙升至 4.7s错误率飙到 23%。这套网关是我们上个月底上线的负责把内部业务对大模型的调用统一路由到混元、DeepSeek、通义等多个供应商。上线前压测过 500 QPS 没问题真实流量才 180 QPS 就崩了。项目背景公司内部有十几个业务组在调大模型原来各自对接供应商 SDK导致密钥泄露风险大、配额管理混乱、无法统一做熔断降级。我们用 Spring Boot 3.2.5 JDK 17.0.12 搭了个统一网关层核心职责请求鉴权、配额扣减、供应商路由、熔断隔离、日志审计。下游供应商走 HTTP/2 长连接网关层用 HikariCP 5.0.1 管理连接池Redis 7.2.5 做分布式令牌桶限流Resilience4j 2.2.0 做熔断。需求分析核心功能需求不复杂按业务组分配每分钟配额超配额返回 429单供应商故障自动切流到备选供应商全链路 TraceID 透传。非功能需求才是坑P99 200ms、支持突发流量 3 倍峰值、配额扣减强一致性不能多扣也不能少扣、零停机变更供应商权重。方案对比| 方案 | 限流实现 | 一致性 | 运维复杂度 | 选型理由 ||------|----------|--------|------------|----------|| 本地 Guava RateLimiter | 内存令牌桶 | 单机强一致 | 低 | 无法满足分布式配额共享 || Redis Lua 脚本 | 原子扣减 | 强一致 | 中 | 单键热点风险后续优化用 Slot 分桶 || Redisson RRateLimiter | 基于 RedLock | 最终一致 | 高 | 网络抖动导致配额漂移生产已踩坑 || Sentinel 集群模式 | Token Server | 最终一致 | 高 | 引入额外组件运维成本超预算 |最终选 Redis Lua键设计为quota:{biz_group}:{minute_window}用EVALSHA保证原子性。熔断选 Resilience4j滑动窗口 10s、最小请求数 20、失败率阈值 50%。核心实现连接池耗尽的现场报警那晚HikariPool-1 - Pool stats (total50, active50, idle0, waiting142)这种日志刷屏。连接池配置yamlspring:datasource:hikari:maximum-pool-size: 50minimum-idle: 10connection-timeout: 3000idle-timeout: 300000max-lifetime: 1200000leak-detection-threshold: 2000供应商 HTTP 客户端用WebClient连接池配置javaBeanpublic HttpClient httpClient() {return HttpClient.create(ConnectionProvider.builder(model-gateway).maxConnections(200).pendingAcquireMaxCount(500).pendingAcquireTimeout(Duration.ofSeconds(5)).maxIdleTime(Duration.ofSeconds(30)).maxLifeTime(Duration.ofMinutes(10)).evictInBackground(Duration.ofSeconds(120)).build()).option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000).responseTimeout(Duration.ofSeconds(30)).doOnConnected(conn - conn.addHandlerLast(new ReadTimeoutHandler(30)).addHandlerLast(new WriteTimeoutHandler(30)));}理论上 200 连接 × 3 供应商 600 并发远超 180 QPS。但监控显示active满 50 就不放了waiting堆到 142。为啥不扩容因为maximum-pool-size卡死在 50。定位过程先看线程栈全卡在HikariPool.getConnection()。再看供应商响应时间混元 P99 从 800ms 涨到 3.2s。原来混元那晚做了灰度发布新版模型首包延迟抖动大导致网关侧连接被长时间占用。但这不该触发连接池耗尽啊WebClient 连接池是 200 啊。翻源码发现pendingAcquireMaxCount500是指等待获取连接的队列长度不是连接数上限。真正上限是maxConnections200。可 HikariCP 只管数据库连接不管 HTTP 连接。网关层没配数据库HikariCP 其实是给 Actuator 健康检查用的默认 10 连接我们改成 50。真正耗尽的是WebClient 连接池。为啥监控看的是 HikariCP因为 Micrometer 暴露的hikaricp.connections.active指标被误读成 HTTP 连接池了。真正的 HTTP 连接池指标是reactor.netty.connection.provider.active和pending。令牌桶漂移的隐形推手修大连接池maxConnections500、pendingAcquireMaxCount1000后延迟降下来了但配额扣减出现负数。Redis 监控显示某业务组某分钟窗口 key 值变成 -17。Lua 脚本lualocal key KEYS[1]local limit tonumber(ARGV[1])local current redis.call(GET, key)if current false thenredis.call(SET, key, limit - 1, EX, 60)return limit - 1endif tonumber(current) 0 thenreturn -1endreturn redis.call(DECR, key)单线程执行没问题但高并发下GET和DECR不是原子的。虽然用了EVALSHA但脚本里分两步先GET判断再DECR。中间穿插别的请求就会少扣或漏扣。改成纯原子lualocal key KEYS[1]local limit tonumber(ARGV[1])local current redis.call(INCR, key)if current 1 thenredis.call(EXPIRE, key, 60)endif current limit thenredis.call(DECR, key)return -1endreturn limit - current 1用INCR反向计数超限再DECR回滚。测试 2000 QPS 持续 5 分钟配额零漂移。熔断器误触发的连锁反应配额修好后发现混元供应商频繁触发熔断流量全切到 DeepSeek导致 DeepSeek 也挂了。Resilience4j 配置yamlresilience4j:circuitbreaker:instances:hunyuan:registerHealthIndicator: trueslidingWindowSize: 10minimumNumberOfCalls: 20failureRateThreshold: 50waitDurationInOpenState: 30spermittedNumberOfCallsInHalfOpenState: 5slidingWindowSize10是 10 个桶默认每桶 1 秒。混元晚高峰 P99 3.2s但成功率 92%为啥触发 50% 失败率翻源码failureRateThreshold算的是调用异常 超时 熔断器拒绝的比例。我们配了responseTimeout 30s但业务层设了Sla(maxLatency 2000ms)超 2s 抛SlaViolationException被熔断器算作失败。把slidingWindowSize改 601分钟窗口、minimumNumberOfCalls改 100、业务层 SLA 放宽到 5s、熔断阈值调到 70%。再压测混元再不误触发。最终生产配置yamlWebClient 连接池reactor:netty:connection:provider:model-gateway:max-connections: 500pending-acquire-max-count: 1000max-idle-time: 30smax-life-time: 10mResilience4j 熔断resilience4j:circuitbreaker:instances:hunyuan:slidingWindowType: TIME_BASEDslidingWindowSize: 60sminimumNumberOfCalls: 100failureRateThreshold: 70slowCallRateThreshold: 80slowCallDurationThreshold: 5swaitDurationInOpenState: 30spermittedNumberOfCallsInHalfOpenState: 10timelimiter:instances:hunyuan:timeoutDuration: 8scancelRunningFuture: trueRedis 限流键分桶缓解热点quota:{biz_group}:{minute_window}:{slot_0~9}效果复盘上线后跑了两周| 指标 | 优化前 | 优化后 | 备注 ||------|--------|--------|------|| 网关 P99 延迟 | 4.7s | 180ms | 混元首包抖动被连接池吸收 || 配额扣减漂移率 | 3.2% | 0% | Lua 原子脚本 分桶 || 熔断误触发次数/天 | 12 | 0 | 窗口拉长 SLA 对齐 || 连接池等待队列峰值 | 142 | 3 | maxConnections 500 够用 || 单机 CPU 峰值 | 85% | 42% | 减少重试风暴 |有个反直觉的地方把熔断窗口从 10s 拉到 60s理论上检测变慢但因为最小调用数从 20 提到 100反而过滤掉了抖动期的噪声。这个方案虽然官方文档推荐短窗口快失败但在我们「下游模型首包延迟高方差」场景下反而更糟—— 这是踩完坑才懂的。还有一处值得记录Redis 热点 key 分桶用slot Math.abs(bizGroup.hashCode()) % 10但业务组 ID 是bg_1001这种字符串hashCode()在 JDK 17 里每次启动都不一样启用了 hash seed 随机化。改成MurmurHash3.hash32(bizGroup.getBytes())才稳。#后端 #Java #SpringBoot #Redis #Resilience4j #网关架构 #性能调优你在实际项目中有遇到类似问题吗欢迎在评论区分享你的经验和解决方案。