Golang DB connection이 exhausted일 때 무엇부터 볼까
마지막 업데이트

Golang DB connection이 exhausted일 때 무엇부터 볼까


Golang에서 DB connection이 exhausted 되면, 원인은 느린 query, 긴 transaction, resource release 누락, 혹은 예상보다 빨리 pool을 압박하는 concurrency 패턴일 수 있습니다.

그래서 “DB connection이 부족하다”는 말만으로는 아직 원인이 아닙니다. 어떤 경우에는 pool이 현재 workload에 비해 작아졌고, 어떤 경우에는 rows, statement, transaction이 팀이 생각한 것보다 오래 살아 있습니다.

이 글은 실전 순서에 집중합니다.

  • pool sizing pressure와 오래 열린 connection을 어떻게 구분할지
  • database/sql 뒤에 caller가 쌓일 때 무엇을 먼저 볼지
  • query time, hold time, concurrency shape가 어떻게 exhaustion으로 이어지는지

짧게 말하면 active connection과 waiting caller를 먼저 비교하고, query가 느린 것인지 resource를 너무 오래 잡는 것인지 본 뒤, workload 변화인지 cleanup bug인지 pool 가정 붕괴인지 판단해야 합니다.

더 넓은 Go 분기부터 다시 보고 싶다면 Golang 트러블슈팅 가이드로 가세요.


active와 waiting부터 비교

가장 먼저 비교해야 할 것은 아래입니다.

  • active connection 수
  • waiting caller 수
  • query time과 hold time 중 무엇이 먼저 올라갔는지

이 구분이 중요한 이유는, 겉으로는 비슷해 보여도 실제로는 다른 두 장애가 있기 때문입니다.

  • 데이터베이스가 실제로 느려져서 connection이 오래 바쁨
  • 애플리케이션이 query가 끝난 뒤에도 connection을 오래 잡고 있음

이 분기 없이 보면 pool size만 의심하다가 실제 resource lifecycle 버그를 놓치기 쉽습니다.


pool pressure와 resource retention의 차이

connection pool이 exhausted 되는 이유는 크게 세 가지로 볼 수 있습니다.

  • 수요가 늘어서 pool 가정이 더 이상 맞지 않음
  • query가 느려져서 connection이 오래 바쁨
  • 애플리케이션이 connection을 제때 release하지 않음

이 이유들은 서로 겹칠 수 있지만 같은 운영 문제는 아닙니다.

결국 더 중요한 질문은 “connection이 모자라는가?”보다 “왜 caller가 이전보다 더 오래 기다리는가?”입니다.


자주 나오는 원인

1. 느린 query

query가 오래 걸리면 connection이 오래 점유되고 뒤쪽 caller가 빠르게 쌓입니다.

자주 보이는 신호는 아래와 같습니다.

  • query duration이 먼저 증가함
  • query latency 상승 뒤 waiting caller가 늘어남
  • 하나의 query path가 pool 점유를 대부분 차지함

이 경우 pool 증상은 분명하지만 주원인은 query, DB, 또는 그 주변 dependency 경로에 있을 가능성이 큽니다.

2. transaction이나 rows가 제때 release되지 않음

resource가 의도보다 오래 열려 있는 경우도 많습니다.

특히 아래에서 자주 나옵니다.

  • rows.Close() 누락
  • transaction이 애플리케이션 로직 전체를 너무 넓게 감쌈
  • error path에서 cleanup 전에 return
  • query 뒤에 DB와 무관한 작업을 하면서 connection을 계속 잡고 있음

이 경우 pool이 작아 보이지만, 실제 더 큰 문제는 connection hold time이 팀 예상보다 길다는 점입니다.

3. pool size와 concurrency가 더 이상 맞지 않음

현재 workload가 초기 pool 가정을 넘어섰을 수도 있습니다.

예를 들면:

  • API 동시성이 이전보다 높아짐
  • 같은 DB를 두드리는 worker가 많아짐
  • batch job이 커짐
  • fan-out이나 retry가 query volume을 늘림

이 경우 pool 조정이 필요할 수는 있지만, query slowdown이나 긴 hold time이 더 깊은 원인이 아닌지 먼저 확인해야 합니다.


실전 점검 순서

DB connection이 바닥날 때는 아래 순서가 보통 가장 빠릅니다.

  1. active connection과 waiting caller 확인
  2. query time과 connection hold time 비교
  3. rows, transaction, statement cleanup 경로 점검
  4. 최근 트래픽이나 job concurrency 변화 비교
  5. 느린 작업인지, 긴 hold time인지, pool 가정 붕괴인지 판단

이 순서가 중요한 이유는 두 가지 흔한 실수를 막기 때문입니다.

  • 왜 바쁜지 모르는데 pool size부터 키우는 실수
  • application hold time을 보지 않고 slow query부터 단정하는 실수

timeout이 먼저 보이는 증상이라면 Golang context deadline exceeded와 같이 비교해 보세요.


pool pressure가 어떻게 생기는지 보는 간단한 예시

db.SetMaxOpenConns(10)

for i := 0; i < 100; i++ {
	go query(db)
}

caller가 pool보다 빠르게 몰리고, 각 connection이 오래 점유되면 database/sql 뒤에서 대기가 빠르게 쌓입니다.

예시는 단순하지만 질문은 늘 같습니다. concurrent work가 pool에 비해 너무 많은가, 아니면 각 작업이 connection을 필요 이상 오래 잡는가입니다.


DB 경로마다 물어볼 질문

각 query나 transaction 경로마다 아래를 물어보면 도움이 됩니다.

  • connection은 언제 획득되는가
  • 잡고 있는 동안 무슨 일이 일어나는가
  • 무엇이 release를 보장하는가
  • 성공/실패 모든 경로에서 release가 되는가

이 프레이밍이 좋은 이유는, 많은 pool 장애가 capacity 문제처럼 보이지만 실제로는 lifecycle bug이기 때문입니다.


pool size를 늘리는 것이 틀린 첫 대응일 때

MaxOpenConns를 올리는 것이 맞을 때도 있습니다. 하지만 아래 상황에서는 첫 대응으로 적절하지 않습니다.

  • 특정 query path가 새로 느려진 경우
  • transaction이 예상보다 오래 열린 경우
  • 애플리케이션 부가 작업 동안 connection을 계속 잡는 경우
  • DB 자체가 이미 압박받고 있는 경우

이때 pool을 키우면 대기가 잠시 줄 수는 있지만, DB 압박을 더 크게 밀어 넣어 실제 병목을 악화시킬 수도 있습니다.

caller가 왜 기다리는지 이해한 뒤에 pool을 조정하세요.


FAQ

Q. pool exhaustion이면 항상 DB가 느린 건가요?

아닙니다. 애플리케이션이 connection을 너무 오래 잡고 있거나, rows를 닫지 않거나, pool 설계보다 더 많은 concurrency를 만들어도 같은 증상이 날 수 있습니다.

Q. 무엇부터 보는 게 가장 빠른가요?

pool size를 바꾸기 전에 active connection, waiting caller, query time, hold time을 먼저 비교하세요.

Q. query 수가 아주 많지 않아도 rows나 transaction 때문에 이 문제가 생길 수 있나요?

네. 동시성이 아주 크지 않아도 각 경로가 connection을 오래 잡고 있으면 pool은 충분히 exhausted 될 수 있습니다.


Sources:

먼저 읽어볼 가이드

검색 유입이 많은 핵심 글부터 이어서 보세요.