如何测试代码定位数据库连接数超限的消耗点?
定位数据库连接超量消耗的测试方案
1. 给连接绑定调用栈追踪
给每个数据库连接绑定获取时的调用栈信息,方便追踪未释放连接的来源。可以通过包装原生连接实现:
import ( "context" "database/sql" "runtime/debug" ) type tracedConn struct { sql.Conn acquireStack []byte } func (tc *tracedConn) Close() error { // 可以选择在连接关闭时打印栈,或仅在连接数超标时输出 // log.Printf("Closing connection acquired from:\n%s", string(tc.acquireStack)) return tc.Conn.Close() } // 替换原DB的连接获取逻辑,注入调用栈追踪 func wrapDBWithTrace(db *sql.DB) { originalConn := db.Conn db.Conn = func(ctx context.Context) (sql.Conn, error) { conn, err := originalConn(ctx) if err != nil { return nil, err } return &tracedConn{ Conn: conn, acquireStack: debug.Stack(), }, nil } }
初始化数据库后调用wrapDBWithTrace(db),后续所有连接都会携带获取时的调用栈。
2. 实时监控连接数并触发栈dump
在测试中定时检查数据库连接统计,一旦开放连接数接近或超过限制,立即dump所有goroutine的栈信息,定位持有连接的goroutine:
import ( "testing" "time" "runtime/debug" ) func TestConnectionOverLimit(t *testing.T) { db := initYourDB() // 替换成你的数据库初始化逻辑 wrapDBWithTrace(db) // 启动监控协程 go func() { ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop() for range ticker.C { stats := db.Stats() t.Logf("Current open connections: %d | Idle: %d", stats.OpenConnections, stats.Idle) if stats.OpenConnections >= 80 { t.Log("=== Connection limit exceeded, dumping goroutine stacks ===") t.Log(string(debug.Stack())) // 可选:触发测试失败以终止流程 // t.FailNow() } } }() // 执行你的业务逻辑测试,模拟实际流量 runYourBusinessScenario(t, db) }
通过db.Stats()可以直观拿到连接池的实时状态,栈dump能直接展示所有活跃goroutine的调用链路,快速定位未释放连接的代码位置。
3. 并发场景下的精准定位
用并发测试模拟高负载,同时追踪每个连接的获取与关闭:
import ( "testing" "context" "golang.org/x/sync/errgroup" "runtime/debug" ) func TestConcurrentConnectionLeaks(t *testing.T) { db := initYourDB() eg := errgroup.Group{} // 模拟100个并发请求,覆盖业务场景 for i := 0; i < 100; i++ { eg.Go(func() error { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() conn, err := db.Conn(ctx) if err != nil { return err } // 记录当前连接的获取栈 acquireStack := debug.Stack() defer func() { if err := conn.Close(); err != nil { t.Logf("Failed to close connection, acquired from:\n%s", string(acquireStack)) } }() // 执行具体的数据库操作(替换成你的业务SQL) _, err = conn.ExecContext(ctx, "SELECT 1") return err }) } if err := eg.Wait(); err != nil { t.Fatal(err) } // 检查测试结束后的连接数,判断是否有泄漏 finalStats := db.Stats() t.Logf("Final open connections: %d", finalStats.OpenConnections) if finalStats.OpenConnections > 30 { // 超过maxIdle配置,疑似泄漏 t.Log("=== Potential connection leak detected, dumping final stacks ===") t.Log(string(debug.Stack())) } }
这种方式能在高并发场景下,精准捕捉到未正确关闭连接的代码分支。
关键排查要点
- 确保所有获取的连接都通过
defer conn.Close()关闭,尤其是错误分支 - 检查
context是否正确传递,超时/取消的context应及时触发连接释放 - 避免在持有连接时执行非数据库操作(如IO、外部API调用),防止连接长时间占用
内容的提问来源于stack exchange,提问作者vovanchello
相关产品推荐
相关产品推荐

