我不会起名字322 · 后端 / 算法 / 数据库
文章目录
- [Redis 从 0 到 1(二十五):连接池又打满了------PoolSize 公式、泄漏排查与 PoolStats](#Redis 从 0 到 1(二十五):连接池又打满了——PoolSize 公式、泄漏排查与 PoolStats)
-
- [一、现象:timeout 就加池,加完还是 timeout](#一、现象:timeout 就加池,加完还是 timeout)
- [二、PoolSize 计算公式(收藏点 1)](#二、PoolSize 计算公式(收藏点 1))
- [三、用 PoolStats 的 4 个指标判断病因(收藏点 2)](#三、用 PoolStats 的 4 个指标判断病因(收藏点 2))
- [四、Go 实操:PoolStats 监控与典型泄漏(收藏点 3)](#四、Go 实操:PoolStats 监控与典型泄漏(收藏点 3))
- [五、服务端配套配置(收藏点 4)](#五、服务端配套配置(收藏点 4))
- [六、排查步骤:connection pool timeout](#六、排查步骤:connection pool timeout)
- 七、总结
Redis 从 0 到 1(二十五):连接池又打满了------PoolSize 公式、泄漏排查与 PoolStats
上一篇《Redis 从 0 到 1(二十四):1 万次 GET 从 800ms 降到 9ms------Pipeline 的 Go 压测》我们把 1 万次 RTT 压成 50 次,链接:https://blog.csdn.net/2501_92769340/article/details/167399903 。
本篇处理优化的"最后一公里":连接池。流量一涨就
connection pool timeout,加 PoolSize 真的有用吗?
一、现象:timeout 就加池,加完还是 timeout
服务 QPS 涨到 3000 后开始报 connection pool timeout。第一反应该是把 PoolSize 从 20 调到 50------没用,又调到 100------报得更频繁了。最后定位到真相:不是池太小,是连接泄漏。有段代码拿到连接执行命令后,在错误分支提前 return,连接没归还(虽然 go-redis 用完后自动回池,但阻塞在慢命令上的连接占用时间被拉长)。
加池为什么可能更糟:池越大,慢命令并发越多,Redis 单线程排队越长,单命令 RTT 越高 → 每条连接占用更久 → 更需要连接......恶性循环。池大小必须按公式来。
二、PoolSize 计算公式(收藏点 1)
所需连接数 ≈ 峰值 QPS × 平均单命令耗时(秒)× 安全系数(1.2~1.5)
例:3000 QPS,平均命令耗时 2ms(含 RTT)
→ 3000 × 0.002 × 1.3 ≈ 8 个连接
对照直觉检查:如果你的 PoolSize 算出 8,实际配了 100,多出来的 92 个连接只会制造"能发更多并发命令"的假象,把 Redis 的排队延迟推高。
还要同时看服务端:maxclients(默认 10000)减去保留(约 32),是所有客户端加起来的天花板。几十个微服务实例 × 每实例 100 连接 = 可能顶到 maxclients,出现 ERR max number of clients reached(第 34 篇专门排查)。
三、用 PoolStats 的 4 个指标判断病因(收藏点 2)
go-redis 暴露 rdb.PoolStats(),4 个关键指标:
| 指标 | 含义 | 什么时候报警 |
|---|---|---|
Timeouts |
从池取连接超时次数 | >0 就说明池不够或泄漏;这是症状指标 |
TotalConns vs PoolSize |
当前连接数/上限 | TotalConns 长期顶满 PoolSize = 池满 |
Hits/(Hits+Misses) |
池命中率 | <90% 说明连接复用差(频繁建连) |
StaleConns |
被服务端断掉的死连接 | 持续增长说明配平没做好(第 2 篇) |
判断逻辑:
Timeouts涨 +TotalConns顶满 + 慢查询多 → 池不小,是命令慢(去第 21 篇),加池无效;Timeouts涨 +TotalConns顶满 +Hits高 + 命令很快 → 真池小,按公式加;Timeouts涨 +TotalConns波动大、新连接创建频繁 → 有地方没复用连接(代码里反复 NewClient?)。
四、Go 实操:PoolStats 监控与典型泄漏(收藏点 3)
监控上报(Prometheus 格式,第 35 篇接采集):
go
package main
import (
"context"
"fmt"
"time"
"github.com/redis/go-redis/v9"
"redis-0to1/redisx"
)
func ReportPoolStats(rdb *redis.Client) {
t := time.NewTicker(30 * time.Second)
for range t.C {
fmt.Println(redisx.PoolStats(rdb)) // 生产替换为 metrics 上报
}
}
典型泄漏场景复盘------长事务占用连接:
go
// ❌ 错误示范:一个 context 里串行执行 100 条命令,每条 20ms
// 高峰时这个请求独占 1 条连接 2 秒;并发 50 就把 20 的池打满
func BadBatchGet(ctx context.Context, rdb *redis.Client, keys []string) {
for _, k := range keys {
rdb.Get(ctx, k) // 逐条 RTT(第 24 篇的反面教材)
}
}
// ✅ 修法一:Pipeline 把 2 秒压缩成 20ms,连接占用降 100 倍
// ✅ 修法二:批量接口加超时上限,防止个别请求拖死连接池
func GoodBatchGet(ctx context.Context, rdb *redis.Client, keys []string) {
ctx, cancel := context.WithTimeout(ctx, 200*time.Millisecond) // 兜底
defer cancel()
// ... Pipeline 实现见第 24 篇
}
第 2 篇的 redisx 封装里已经带 PoolStats() 输出------现在你知道它是为哪一篇准备的了。
五、服务端配套配置(收藏点 4)
conf
# redis.conf
maxclients 10000 # 天花板,按"实例数 × PoolSize + 运维余量"核算
timeout 300 # 空闲断开(客户端 ConnMaxIdleTime 要比它小,第 2 篇)
tcp-keepalive 60
实例数 × PoolSize 的账要定期重算:服务从 3 个 pod 扩到 30 个,池配置不变,服务端连接数就涨了 10 倍------这就是第 34 篇《maxclients 打满但 QPS 只有 500》的事故现场预告。
六、排查步骤:connection pool timeout
PoolStats()抓指标:Timeouts/TotalConns/Hits三对照(第三节判断树);- 命令慢不慢:slowlog 有货 → 先治慢命令;
- 代码有没有逐条 RTT 循环:有 → 改 Pipeline;
- 真池小:按公式重算 PoolSize,同时检查服务端 maxclients 余量;
- 改完压测回归:
Timeouts归零、TotalConns稳定在公式值附近。
七、总结
- PoolSize 按
QPS × 单命令耗时 × 1.3算,拍脑袋加池可能适得其反; - PoolStats 四指标判断树:Timeouts 是症状,Hits/TotalConns 是病因;
- 连接泄漏的头号形态是"逐条 RTT 的长事务",解法在第 24 篇;
- 服务端 maxclients 要按"实例数 × PoolSize"定期核算。
下一篇预告:连接稳了,下一篇往内存里看------下一篇《INFO memory 里 8 个关键数字------内存构成、编码转换(listpack)与容量规划》,我们搞清楚 Redis 的内存都花在哪了。