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이 바닥날 때는 아래 순서가 보통 가장 빠릅니다.
- active connection과 waiting caller 확인
- query time과 connection hold time 비교
- rows, transaction, statement cleanup 경로 점검
- 최근 트래픽이나 job concurrency 변화 비교
- 느린 작업인지, 긴 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 될 수 있습니다.
Read Next
- Go 전체 분기부터 다시 보고 싶다면 Golang 트러블슈팅 가이드로 가세요.
- timeout이 먼저 보이는 증상이라면 Golang context deadline exceeded를 보세요.
- memory나 worker 압박도 함께 보인다면 Golang 메모리 사용량이 높을 때 무엇부터 볼까도 같이 보세요.
Related Posts
Sources:
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.
먼저 읽어볼 가이드
검색 유입이 많은 핵심 글부터 이어서 보세요.
- 미들웨어 트러블슈팅 가이드: Redis vs RabbitMQ vs Kafka 개발자를 위한 미들웨어 트러블슈팅 허브 글입니다. Redis, RabbitMQ, Kafka 중 어떤 증상부터 먼저 봐야 하는지와 어떤 문제 패턴이 각 시스템에 가까운지 정리합니다.
- Kubernetes CrashLoopBackOff: 먼저 볼 것들 startup failure, probe, config, resource limit 관점에서 CrashLoopBackOff를 어떻게 나눠서 봐야 하는지 정리한 가이드입니다.
- Kafka consumer lag가 계속 늘 때: 트러블슈팅 가이드 Kafka consumer lag가 계속 늘어날 때 무엇부터 봐야 하는지 정리합니다. poll 주기, 처리 속도, rebalance, consumer 설정까지 실전 기준으로 다룹니다.
- Kafka Rebalancing Too Often 가이드 Kafka consumer group에서 rebalance가 너무 자주 일어날 때 membership flapping, poll timing, protocol, assignment churn을 어떤 순서로 봐야 하는지 설명하는 실전 가이드입니다.
- Docker container가 계속 재시작될 때: 먼저 확인할 것들 exit code, command failure, environment mistake, health check 관점에서 Docker restart loop를 푸는 실전 가이드입니다.
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.