第一章:Golang驱动是自带连接池吗
Go 语言标准库 database/sql 包本身不实现底层网络连接逻辑,但内置了完备的连接池管理机制。它与具体数据库驱动(如 github.com/lib/pq、github.com/go-sql-driver/mysql、github.com/jackc/pgx/v5)协同工作:驱动仅负责建立/关闭单个连接,而连接的复用、空闲回收、最大连接数控制等均由 database/sql.DB 实例统一调度。
连接池的核心行为特征
- 连接按需创建,首次
db.Query()或db.Exec()时初始化连接; - 空闲连接保留在池中,默认最大空闲数为 2(可通过
SetMaxIdleConns调整); - 池中连接总数受
SetMaxOpenConns限制(默认 0 表示无上限); - 连接空闲超时由
SetConnMaxIdleTime控制(Go 1.15+ 支持),超时后自动关闭; - 连接生命周期超时由
SetConnMaxLifetime控制(如设为 30m),到期强制回收。
验证连接池是否生效
可通过以下代码观察连接复用行为:
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql" // 替换为实际驱动
)
func main() {
db, err := sql.Open("mysql", "user:pass@tcp(127.0.0.1:3306)/test")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 显式配置连接池参数
db.SetMaxOpenConns(10) // 最大打开连接数
db.SetMaxIdleConns(5) // 最大空闲连接数
db.SetConnMaxLifetime(30 * 60 * time.Second) // 连接最长存活时间
// 执行两次查询,观察底层是否复用同一连接(依赖驱动日志或抓包验证)
for i := 0; i < 2; i++ {
rows, _ := db.Query("SELECT CONNECTION_ID()")
defer rows.Close()
var connID uint64
rows.Next()
rows.Scan(&connID)
fmt.Printf("Query %d → Connection ID: %d\n", i+1, connID)
}
}
注意:
CONNECTION_ID()是 MySQL 特有函数,用于识别服务端连接标识;其他数据库需使用对应方式(如 PostgreSQL 的pg_backend_pid())。
常见误区澄清
| 误解 | 事实 |
|---|---|
| “驱动自带连接池” | 驱动只提供 Driver.Open() 接口;池由 database/sql 实现 |
“每次 sql.Open() 创建新池” |
sql.Open() 仅初始化 *sql.DB,不建连接;池在首次操作时惰性构建 |
“db.Close() 关闭所有连接” |
db.Close() 会等待所有已获取连接归还后关闭全部底层连接 |
因此,开发者无需引入第三方连接池库——只要正确使用 database/sql.DB 并合理调优其池参数,即可获得生产就绪的连接复用能力。
第二章:深入剖析Go标准库与主流驱动的连接池实现真相
2.1 net/http.DefaultTransport背后的连接复用机制与局限性
连接复用核心:http.Transport 的 idle 管理
DefaultTransport 实际是 &http.Transport{} 的实例,其复用依赖 IdleConnTimeout(默认90s)和 MaxIdleConnsPerHost(默认2)。
// 默认 Transport 关键配置(精简)
tr := http.DefaultTransport.(*http.Transport)
fmt.Printf("MaxIdleConnsPerHost: %d\n", tr.MaxIdleConnsPerHost) // 2
fmt.Printf("IdleConnTimeout: %v\n", tr.IdleConnTimeout) // 90s
该配置限制每主机最多保留2个空闲连接,超时即关闭;若并发请求突增,将频繁新建TCP连接,引发TIME_WAIT堆积与延迟上升。
复用失效的典型场景
- 请求 Host 不一致(如
api.example.comvswww.example.com)→ 视为不同主机 - TLS 握手参数差异(SNI、ALPN)→ 连接池隔离
- 自定义
DialContext或TLSClientConfig→ 池键(key)不匹配
性能对比:默认 vs 调优后
| 配置项 | 默认值 | 推荐生产值 |
|---|---|---|
| MaxIdleConnsPerHost | 2 | 100 |
| IdleConnTimeout | 90s | 30s |
| MaxIdleConns | 0(不限) | 1000 |
graph TD
A[HTTP Client 发起请求] --> B{连接池中存在可用空闲连接?}
B -->|是| C[复用连接,跳过TCP/TLS握手]
B -->|否| D[新建TCP连接 → TLS握手 → 发送请求]
D --> E[响应后,若未超时且未达MaxIdle上限,则归还至idle队列]
2.2 database/sql包中driver.ConnPool接口的真实契约与默认实现陷阱
driver.ConnPool 并非导出接口,而是 database/sql 内部约定的隐式契约:任何 driver 必须提供 Get()、Put()、Close() 三方法,且需满足线程安全与连接有效性校验。
核心契约约束
Get()不得阻塞无限期(受sql.Open()的maxOpen和maxIdleTime间接约束)Put(conn)必须验证conn是否仍可用,否则导致“幽灵连接”泄漏Close()需同步终止所有待处理连接,但标准sql.DB默认不调用它(仅调用 driver 的Close())
默认实现陷阱示例
// sql.Open("mysql", "...") 实际创建的 pool(简化)
type connPool struct {
mu sync.Mutex
conns []driver.Conn // 无健康检查,Put 后直接复用
}
逻辑分析:该结构体未实现连接空闲超时清理或
Ping()验证。若网络闪断后Put()一个已失效连接,下次Get()将返回 stale conn,触发driver.ErrBadConn—— 此时database/sql会丢弃它并重试,但不通知 driver 重连逻辑,造成连接池雪崩。
| 行为 | 标准期望 | 常见 driver 实现偏差 |
|---|---|---|
Put() 失效连接 |
应拒绝或主动关闭 | 直接存入池,静默污染 |
Get() 超时 |
返回 context.DeadlineExceeded |
返回 nil, nil 导致 panic |
graph TD
A[Get()] --> B{连接是否有效?}
B -->|是| C[返回 conn]
B -->|否| D[调用 driver.Ping]
D --> E{Ping 成功?}
E -->|是| C
E -->|否| F[丢弃 conn,新建]
2.3 MySQL驱动(go-sql-driver/mysql)连接池行为的源码级验证与压测实证
源码关键路径定位
sql/db.go 中 DB.conn() 调用 db.getConn(ctx),最终进入 db.connectionOpener() 和 db.putConn() 的闭环调度逻辑。连接复用、超时驱逐、空闲回收均由 db.freeConn([]*driverConn)和 db.waitCount 协同控制。
连接池核心参数对照表
| 参数 | 默认值 | 作用 |
|---|---|---|
MaxOpenConns |
0(无限制) | 最大并发活跃连接数 |
MaxIdleConns |
2 | 空闲连接上限 |
ConnMaxLifetime |
0 | 连接最大存活时间(禁用则永不淘汰) |
压测验证片段(含注释)
db, _ := sql.Open("mysql", "user:pass@tcp(127.0.0.1:3306)/test")
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(30 * time.Second) // 强制30s后释放旧连接
此配置下,压测时观察到:第11个并发请求触发新建连接阻塞;空闲连接在30s后被
connCleanergoroutine主动关闭;freeConn长度稳定≤5,验证了maxIdle的硬限生效。
连接生命周期流程
graph TD
A[GetConn] --> B{freeConn非空?}
B -->|是| C[复用空闲连接]
B -->|否| D[新建driverConn]
C & D --> E[执行Query/Exec]
E --> F[PutConn回池]
F --> G{idle超时或maxIdle超限?}
G -->|是| H[Close并从freeConn移除]
2.4 PostgreSQL驱动(pgx/v5)如何绕过标准池并自建异步连接管理器
当高吞吐、低延迟场景下标准 pgxpool.Pool 的锁竞争与预分配策略成为瓶颈时,可基于 pgx.Conn 构建轻量级异步连接管理器。
核心思路:连接即资源,按需获取与归还
- 使用
pgx.Connect()获取裸连接(无池封装) - 通过
sync.Pool复用*pgx.Conn实例(避免频繁 TLS 握手) - 配合
context.WithTimeout控制连接生命周期
连接复用安全边界
| 属性 | 标准池 | 自建管理器 |
|---|---|---|
| 并发安全 | ✅ 内置锁 | ❌ 需调用方保证单 goroutine 持有 |
| 连接健康检查 | ✅ 自动 ping | ⚠️ 需手动 conn.Ping(ctx) |
// 从 sync.Pool 获取已认证连接(跳过重连开销)
conn := connPool.Get().(*pgx.Conn)
if err := conn.Ping(ctx); err != nil {
conn.Close(ctx) // 丢弃失效连接
conn = pgx.Connect(ctx, connStr) // 新建
}
该代码绕过 pgxpool.Acquire() 的全局互斥锁,将连接生命周期交由业务逻辑控制;Ping 验证确保连接有效性,失败后重建——代价是放弃自动重连语义,换取毫秒级调度自由度。
2.5 SQLite与Redis驱动(如go-redis)为何根本不存在“内置连接池”——协议层本质解析
SQLite 是嵌入式文件数据库,无网络协议栈;其“连接”本质是进程内句柄(*sql.DB 封装的是 sqlite3 C 库的 sqlite3* 指针),所有操作在单线程/序列化上下文中完成:
db, _ := sql.Open("sqlite3", "test.db?_busy_timeout=5000")
// ⚠️ 此处无 TCP 连接、无 socket、无连接生命周期管理
逻辑分析:
sql.Open()仅初始化驱动注册与连接参数解析;db.Query()实际调用sqlite3_step(),全程零网络 I/O。所谓“连接池”在此语境下是概念误用——*sql.DB的SetMaxOpenConns()对 SQLite 仅控制并发执行的 goroutine 数量上限,而非连接复用。
Redis(如 github.com/redis/go-redis/v9)则完全不同:它基于 纯文本 RESP 协议 + TCP 长连接,但驱动本身不实现连接池——而是依赖 net.Conn 抽象与 redis.Pool(v8)或 redis.Client 内置的 *redis.Pool(v9 中已重构为 *redis.ConnPool 接口):
| 组件 | 是否“内置池” | 说明 |
|---|---|---|
database/sql |
✅ | 标准接口,统一管理 *sql.Conn |
go-sqlite3 |
❌ | 无网络层,*sql.Conn = 内存句柄 |
go-redis/v9 |
❌ | Client 依赖外部 ConnPool 实现 |
graph TD
A[go-redis Client] --> B[ConnPool interface]
B --> C[SingleConnPool]
B --> D[ClusterConnPool]
B --> E[自定义实现]
根本原因在于:连接池是传输层抽象,而 SQLite 无传输层,Redis 驱动只实现协议编解码。
第三章:为什么“自动管理”是危险幻觉?从三个典型故障场景反推设计缺陷
3.1 连接泄漏导致OOM的线上事故复盘与pprof火焰图定位
事故现象
凌晨2点告警:服务RSS内存持续攀升至16GB,GC频次激增,http: Accept error: accept tcp: too many open files。
根因定位
通过 go tool pprof -http=:8080 http://localhost:6060/debug/pprof/heap 查看火焰图,发现 database/sql.(*DB).conn 调用栈长期驻留,占比超78%。
关键泄漏代码
func syncUser(ctx context.Context, id int) error {
db, _ := sql.Open("mysql", dsn) // ❌ 每次调用新建DB实例
defer db.Close() // ⚠️ 但未保证执行(panic时跳过)
row := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = ?", id)
// ... 处理逻辑
return nil
}
sql.Open仅初始化连接池配置,不创建物理连接;但此处反复新建*sql.DB导致旧实例的连接池无法被GC回收,底层freeConn切片持续累积空闲连接。
修复方案
- ✅ 全局复用单例
*sql.DB - ✅ 设置
db.SetMaxOpenConns(20)与db.SetMaxIdleConns(10) - ✅ 使用
context.WithTimeout防止阻塞泄漏
| 参数 | 默认值 | 建议值 | 作用 |
|---|---|---|---|
MaxOpenConns |
0(无限制) | 20 | 控制最大并发连接数,防句柄耗尽 |
MaxIdleConns |
2 | 10 | 缓存空闲连接,降低建连开销 |
graph TD
A[HTTP请求] --> B[syncUser]
B --> C{db == nil?}
C -->|Yes| D[sql.Open 创建新DB]
C -->|No| E[复用全局DB]
D --> F[连接池泄漏]
E --> G[连接复用+自动回收]
3.2 事务超时引发连接卡死与池饥饿的并发竞态模拟实验
实验目标
复现高并发下事务超时未释放连接,导致连接池耗尽的典型竞态场景。
关键模拟代码
// 模拟长事务 + 超时配置不当
DataSource dataSource = HikariConfigBuilder.newBuilder()
.maxPoolSize(5) // 连接池上限
.connectionTimeout(3000) // 获取连接超时:3s
.transactionTimeout(2000) // Spring事务超时:2s(但底层连接未主动中断)
.build();
逻辑分析:transactionTimeout=2000 仅触发 Spring 的 TransactionAspectSupport 回滚,若 JDBC 驱动未响应中断(如 MySQL 5.7+ 默认 cancelQueryTimeout=0),物理连接将持续占用,无法归还池中。
竞态链路
- 5 个线程并发开启事务
- 全部执行
Thread.sleep(2500)超过事务超时 - Spring 回滚后连接仍被阻塞 → 池中可用连接降为 0
| 状态阶段 | 连接占用数 | 池中可用数 | 是否可新建连接 |
|---|---|---|---|
| 初始 | 0 | 5 | 是 |
| 第5个事务启动 | 5 | 0 | 否(等待超时) |
| 第6个请求到达 | 5 | 0 | 触发 connectionTimeout |
graph TD
A[客户端发起请求] --> B{连接池获取连接}
B -->|成功| C[开启事务]
B -->|失败| D[阻塞等待 connectionTimeout]
C --> E[执行 sleep 2500ms]
E --> F[Spring 检测 transactionTimeout]
F --> G[尝试回滚但 JDBC 未中断]
G --> H[连接持续占用不归还]
3.3 DNS轮询+长连接空闲驱逐失效导致的跨AZ连接断裂案例
问题现象
某微服务集群部署在多可用区(AZ),客户端通过 DNS 轮询解析到不同 AZ 的 VIP。当连接池复用长连接,且服务端启用 keepalive_timeout 60s,但客户端未同步配置 idleTimeout=30s 时,DNS 缓存过期后新请求被调度至其他 AZ,而旧连接仍滞留在原 AZ 实例上——造成连接“跨 AZ 悬挂”,最终因服务端主动 FIN 导致 RST。
关键配置失配
# 客户端连接池(错误配置)
pool:
maxIdleTime: 0 # ❌ 未设空闲驱逐,连接永不释放
idleEvictMs: 0 # ❌ 驱逐线程不触发
逻辑分析:
maxIdleTime: 0表示禁用空闲超时机制;idleEvictMs: 0停止后台驱逐扫描。即使 DNS 已刷新,连接仍长期绑定旧后端 IP,无法感知 AZ 切换。
DNS 与连接生命周期冲突示意
| 组件 | TTL | 实际行为 |
|---|---|---|
| DNS 解析缓存 | 30s | 客户端每30s获取新IP列表 |
| 连接空闲检测 | 0(禁用) | 连接持续复用,无视IP变更 |
| 后端保活 | 60s | 服务端60s后关闭无流量连接 |
根本修复路径
- ✅ 客户端启用
maxIdleTime: 25s( - ✅ 开启
idleEvictMs: 5000(5s 扫描一次) - ✅ 服务端同步调低
keepalive_timeout至 20s
graph TD
A[客户端发起请求] --> B{DNS轮询解析}
B --> C[AZ1: 10.0.1.10]
B --> D[AZ2: 10.0.2.10]
C --> E[建立长连接]
D --> F[建立长连接]
E --> G[60s后服务端FIN]
F --> H[60s后服务端FIN]
G --> I[客户端无驱逐→重用失效连接]
H --> I
第四章:真正可控的连接生命周期管理——5个可编程钩子的工程化落地
4.1 driver.Conn.PrepareContext:在连接就绪前注入上下文感知的认证与路由策略
PrepareContext 不仅预编译 SQL,更是在物理连接建立前完成策略注入的关键钩子。
上下文驱动的策略决策点
当 ctx 携带 authz.Scope 或 route.Hint 时,驱动可提前解析并绑定策略:
func (c *conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
// 提前提取上下文元数据,不等待网络握手
auth := authz.FromContext(ctx) // 如:authz.NewScope("tenant:prod-us-east")
route := route.HintFromContext(ctx) // 如:route.ReadReplicaOnly()
// 构建带策略的预编译句柄
return &stmt{query: query, auth: auth, route: route}, nil
}
逻辑分析:
ctx在PrepareContext阶段即被解构,authz.FromContext提取租户/权限标识,route.HintFromContext获取读写偏好或地域标签。这些信息被固化到stmt实例中,后续ExecContext/QueryContext执行时直接生效,避免运行时重复解析。
策略注入时机对比
| 阶段 | 是否可影响连接选择 | 是否可拒绝预编译 |
|---|---|---|
Driver.Open |
否(仅凭DSN) | 否 |
Conn.Prepare |
否(无 ctx) | 否 |
Conn.PrepareContext |
✅ 是(含 ctx) | ✅ 是(可基于 scope 返回 error) |
路由决策流程
graph TD
A[PrepareContext] --> B{Has route.Hint?}
B -->|Yes| C[Select matching endpoint]
B -->|No| D[Use default pool]
C --> E[Bind stmt to endpoint]
4.2 driver.Conn.Close:实现优雅下线与连接状态归档的日志埋点与指标上报
Close() 不仅释放底层网络资源,更是连接生命周期终点的可观测性枢纽。
日志埋点设计
func (c *conn) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.closed {
return nil
}
c.closed = true
// 埋点:记录关闭原因、存活时长、最后活跃时间
log.Info("connection_closed",
zap.String("id", c.id),
zap.String("reason", c.closeReason), // "idle_timeout" / "explicit" / "error"
zap.Duration("uptime", time.Since(c.createdAt)),
zap.Time("last_active", c.lastActive.Load().(time.Time)),
zap.Int64("queries_executed", c.queryCount.Load()),
)
return c.netConn.Close()
}
该实现确保日志在锁保护下原子写入,closeReason 由上层调用方预设(如连接池驱逐或用户显式调用),uptime 和 last_active 提供连接健康画像。
指标上报维度
| 指标名 | 类型 | 说明 |
|---|---|---|
db_conn_close_total |
Counter | 按 reason 标签分组统计 |
db_conn_uptime_seconds |
Histogram | 归档连接存活时长分布 |
状态归档流程
graph TD
A[Close 被调用] --> B{是否已关闭?}
B -->|是| C[直接返回]
B -->|否| D[标记 closed=true]
D --> E[记录元数据到归档队列]
E --> F[异步上报指标 + 写入审计日志]
4.3 driver.ConnPool.Get:定制化健康检查、权重路由与熔断降级逻辑
ConnPool.Get() 不仅返回连接,更承载了动态流量治理的核心逻辑。
健康检查与权重融合策略
func (p *ConnPool) Get(ctx context.Context) (*Conn, error) {
candidates := p.selectByWeightAndHealth() // 基于实时健康分(0–100)与配置权重归一化
for _, c := range candidates {
if p.isHealthy(c) && p.circuitBreaker.Allow(c.ID) {
return c, nil
}
}
return nil, ErrNoAvailableConn
}
selectByWeightAndHealth() 对节点按 weight × healthScore/100 动态排序;isHealthy() 支持可插拔探测(如 PING 延迟 Allow() 触发熔断器滑动窗口统计。
熔断状态决策依据
| 状态 | 连续失败阈值 | 半开探测请求数 | 冷却期 |
|---|---|---|---|
| 关闭 | — | — | — |
| 打开 | 5 | 1 | 30s |
| 半开 | — | 3 | — |
流量调度流程
graph TD
A[Get请求] --> B{健康检查通过?}
B -->|否| C[剔除并更新健康分]
B -->|是| D{熔断器允许?}
D -->|否| E[降级至备用集群]
D -->|是| F[返回连接]
4.4 driver.ConnPool.Put:基于响应延迟与错误率的动态连接保活/驱逐决策引擎
Put 不是简单归还连接,而是触发实时健康评估的决策入口。
健康评分模型
连接被放入池前,计算综合健康分:
score = 0.6 × (1 − norm_latency) + 0.4 × (1 − error_rate)
其中 norm_latency ∈ [0,1] 是最近3次P95延迟归一化值(以200ms为满分基准),error_rate 为过去60秒错误占比。
决策逻辑流程
graph TD
A[Put conn] --> B{score ≥ 0.75?}
B -->|Yes| C[标记idle,加入LRU队尾]
B -->|No| D{error_rate > 0.15 ∨ latency_p95 > 800ms?}
D -->|Yes| E[立即close,不入池]
D -->|No| F[降权入池,标记“观察中”]
驱逐策略参数表
| 参数 | 默认值 | 说明 |
|---|---|---|
min_health_score |
0.75 | 入池最低健康阈值 |
max_observed_age |
30s | “观察中”连接最长驻留时间 |
error_window_sec |
60 | 错误率统计滑动窗口 |
func (p *ConnPool) Put(conn *Conn) {
score := p.calcHealthScore(conn) // 调用延迟采样器+错误计数器
if score < p.minHealthScore &&
(conn.errRate > 0.15 || conn.latencyP95 > 800*time.Millisecond) {
conn.Close() // 硬驱逐:避免雪崩传播
return
}
p.enqueue(conn, score) // 按分加权插入
}
该实现将连接生命周期管理从静态超时升级为实时反馈闭环。
第五章:连接池控制权回归开发者手中的终极范式
连接泄漏的现场复现与根因定位
某电商订单服务在大促压测中出现 Connection reset by peer 频发,Prometheus 监控显示活跃连接数持续攀升至 287(配置上限为 300),但业务 QPS 仅维持在 1200。通过 jstack -l <pid> 抓取线程快照,发现 42 个 HikariPool-1 housekeeper 线程处于 WAITING 状态,同时 netstat -anp | grep :3306 | wc -l 返回 291 条 ESTABLISHED 连接——证实连接未被归还。进一步启用 HikariCP 的 leakDetectionThreshold=60000 后,日志精准捕获到 com.example.order.service.PaymentService.processRefund() 中 connection.close() 被包裹在 try-catch 内却未在 finally 块中执行的缺陷。
手动管理连接生命周期的生产级实践
在金融对账核心模块中,我们放弃 Spring Boot AutoConfigure 的默认连接池,采用显式声明方式初始化 HikariCP 实例:
@Bean("auditDataSource")
public DataSource auditDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://audit-db:3306/ledger?useSSL=false");
config.setUsername("auditor");
config.setPassword("******");
config.setMaximumPoolSize(15);
config.setMinimumIdle(5);
config.setConnectionTimeout(3000);
config.setLeakDetectionThreshold(30000);
config.setValidationTimeout(2000);
config.setConnectionTestQuery("SELECT 1");
return new HikariDataSource(config);
}
该配置强制要求所有 DAO 层调用必须通过 try-with-resources 获取连接,并在 @PostConstruct 方法中注入 HikariPoolMXBean 实时监控 getActiveConnections() 和 getIdleConnections() 指标。
多租户场景下的动态池隔离策略
面对 SaaS 平台 37 个客户共享同一数据库实例的架构,我们基于租户 ID 构建连接池路由表:
| 租户类型 | 最小空闲连接 | 最大连接数 | 连接超时(ms) | 适用场景 |
|---|---|---|---|---|
| VIP | 8 | 40 | 5000 | 实时风控查询 |
| Standard | 3 | 12 | 3000 | 日常报表导出 |
| Trial | 1 | 4 | 1000 | 新用户试用环境 |
路由逻辑通过 TenantConnectionPoolRouter 实现,其 getConnection(tenantId) 方法根据租户等级查表并返回对应 HikariDataSource 实例,避免单池争抢导致 VIP 客户响应延迟突增。
连接池与分布式事务的协同治理
在 Seata AT 模式下,我们重写 SeataDataSourceProxy 的 getConnection() 方法,在获取物理连接后立即调用 setAutoCommit(false) 并注册 ConnectionProxy 监听器。当全局事务 xid=TX-20240521-173244-889 提交时,监听器触发 pool.evictConnection(connection) 主动驱逐该连接,防止脏连接滞留池中污染后续事务。
基于 OpenTelemetry 的连接行为追踪
通过自定义 HikariCP 的 ProxyConnection 实现类,我们在 prepareStatement() 和 executeQuery() 方法入口埋点,将连接获取耗时、SQL 类型、租户标识注入 Span Attributes。Grafana 看板中可下钻分析:tenant_id="t_8821" 的 SELECT * FROM account_balance 查询平均连接等待时间达 187ms,进而定位到其所在 Pod 的 MySQL 客户端 DNS 缓存失效问题。
flowchart LR
A[应用发起 getConnection] --> B{池中有空闲连接?}
B -- 是 --> C[返回连接并更新 lastAccessed]
B -- 否 --> D[触发 addConnection 创建新连接]
D --> E{创建成功?}
E -- 是 --> C
E -- 否 --> F[触发 eviction 清理失效连接]
F --> G[重试 maxLifetime 检查] 