이 글의 위치: 기술 블로그 → Backend · 운영 판단. 작성·검증 방식은 Editorial Policy 참조.
커넥션 풀 운영 시리즈 2편. 1편(지표 읽는 법)에서 풀의 상태를 숫자로 읽었다면, 이 글은 풀의 두 시간 설정 — 얼마나 기다릴까(timeout)와 얼마나 오래 살릴까(lifetime) — 를 HikariCP와 Go pgxpool로 비교한다.
풀이 비었을 때, 그 다음
1편에서 active = max + awaiting > 0이 풀 고갈이라고 했다. 그렇다면 풀이 비어 커넥션을 못 빌릴 때 얼마나 더 기다릴 것인가, 그리고 못 받으면 어떻게 실패할 것인가가 다음 질문이다. 그리고 한 발 더 — 빌려준 커넥션을 얼마나 오래 살려둘 것인가. 이 두 시간 설정이 장애의 모양을 바꾼다.
1. 얼마나 기다릴까 — 그리고 어떻게 실패할까
풀이 비었을 때의 동작에서 두 진영의 설계 철학이 갈린다.
- Hikari:
connectionTimeout이 경과하면SQLException을 던진다. "풀이 던지는 예외." - Go pgxpool:
pool.Acquire(ctx)에 넘긴ctx의 데드라인이 지나면context.DeadlineExceeded로 호출자가 cancel 신호를 받는다. "호출자가 부여한 deadline."
| Hikari | pgxpool | |
|---|---|---|
| 풀에서 빌릴 때 대기 | connectionTimeout |
ctx.WithTimeout(...) |
| DB로 TCP connect 시 | (위와 묶여 있음) | ConnConfig.ConnectTimeout (분리) |
| 실패 형태 | SQLException |
context.DeadlineExceeded |
차이가 단순하지 않다. Hikari는 풀 전역에 하나의 timeout을 건다. Go는 호출 경로마다 다른 deadline을 줄 수 있다 — 결제 API엔 200ms, 배치 작업엔 5s. 더 정밀하지만 매번 호출자가 context를 챙겨야 한다. 전역 설정의 단순함 vs 호출 단위의 정밀함이라는 트레이드오프다.
fast-fail — 실패를 빨리 만들수록 건강하다
이 트랙에서 가장 반직관적인 관찰. 같은 부하(풀 4, 동시성 50, 긴 작업)에 timeout만 바꿔본다.
connectionTimeout = 30000(30초): 에러가 0이다. 대신 p99가 끝없이 늘어난다. 모든 요청이 30초까지 묵묵히 기다리기 때문이다. 겉보기엔 "에러 없음"이지만 실제로는 감춰진 장애 — 사용자는 30초째 멈춰 있고, 그 스레드/고루틴은 묶여 회복도 못 한다.connectionTimeout = 300(300ms): errors 카운트가 명확히 뜬다. 하지만 나머지 트래픽의 latency는 안정적이다. 못 빌린 요청은 빨리 실패해 자원을 반납하고, 시스템은 계속 응답한다.
결론은 이렇다. 실패를 빨리 만들수록 시스템은 건강하다. 길게 기다리는 timeout은 에러 그래프를 깨끗하게 만들지만, 그 대가로 장애를 지연시켜 숨긴다. 묶인 스레드가 쌓이면 결국 전체가 함께 쓰러진다. 빠른 실패는 회로 차단기(circuit breaker)와 재시도가 작동할 여지를 준다.
2. 얼마나 오래 살릴까 — 그리고 죽은 커넥션의 함정
커넥션은 영원하지 않다. 세 종류의 수명이 있다.
| Hikari | pgxpool | 역할 | |
|---|---|---|---|
| 최대 수명 | maxLifetime |
MaxConnLifetime |
생성 후 N분 지나면 강제 폐기 |
| 유휴 폐기 | idleTimeout |
MaxConnIdleTime |
minIdle 초과분 중 안 쓰인 conn 폐기 |
| keepalive | keepaliveTime |
HealthCheckPeriod |
idle 상태에서 주기적 점검(방화벽 끊김 방지) |
maxLifetime이 필요한 이유는 커넥션이 조용히 썩기 때문이다 — DB 재시작, 페일오버, 방화벽의 NAT 테이블 만료, TLS 세션 만료. 일정 시간마다 갈아주면 이런 좀비 커넥션을 예방한다.
가장 중요한 함정: 풀이 "죽은 커넥션"을 빌려준다
DB나 방화벽이 끊는 시간보다 풀의 maxLifetime이 더 짧아야 한다. 그렇지 않으면 풀은 살아있다고 믿는 죽은 커넥션을 빌려주고, 첫 쿼리에서 connection closed나 connection reset이 터진다.
학습 환경의 Postgres엔 idle_in_transaction_session_timeout = 15000(15초)이 걸려 있다. 즉 트랜잭션을 열어둔 채 15초를 넘기면 DB가 먼저 그 커넥션을 끊는다. 긴 트랜잭션 시나리오(sleepMs = 20000, 20초)를 돌리면 — DB가 15초에 커넥션을 죽이고, 풀이 그걸 모른 채 다시 빌려주려 할 때 무슨 일이 나는지 관찰할 수 있다. validation(빌려줄 때 살아있는지 확인)이 켜져 있어야 풀이 이걸 잡아내고 새 커넥션으로 교체한다.
실무 규칙: 풀의 maxLifetime을 인프라의 어떤 idle/세션 타임아웃보다도 짧게 잡는다. 흔히 maxLifetime을 30분 정도로 두고, DB·로드밸런서·방화벽의 타임아웃이 그보다 길도록 맞춘다. 풀 설정만으로는 DB가 끊는 걸 막을 수 없다 — 풀과 DB의 타임아웃을 함께 봐야 한다는 게 이 트랙의 핵심이다.
정리
- timeout: Hikari는
connectionTimeout→SQLException(전역 예외), Go는ctxdeadline→context.DeadlineExceeded(호출 단위). 정밀함 vs 단순함. - fast-fail: 긴 timeout은 에러를 0으로 만들지만 장애를 숨긴다. 짧은 timeout으로 빨리 실패시키는 쪽이 시스템 전체로는 건강하다.
- lifetime:
maxLifetime은 인프라의 어떤 타임아웃보다 짧아야 한다. 안 그러면 풀이 죽은 커넥션을 빌려준다. - 풀과 DB의 시간 설정은 따로 보면 안 된다 — 같이 봐야 함정이 보인다.
다음 편은 시리즈의 백미 — 커넥션 누수. Hikari가 옵션 한 줄로 해주던 누수 추적을, pgxpool에선 직접 만들어 보며 "프레임워크가 공짜로 해주던 게 뭔지"를 들여다본다.
참고
- HikariCP —
connectionTimeout·maxLifetime·keepaliveTime· github.com/brettwooldridge/HikariCP - pgx ·
pgxpool.Config(MaxConnLifetime·MaxConnIdleTime·HealthCheckPeriod) · pkg.go.dev/github.com/jackc/pgx/v5/pgxpool - PostgreSQL —
idle_in_transaction_session_timeout· postgresql.org/docs - 같은 시리즈 — 1편 지표 읽는 법 · 커넥션 풀 크기 정하기
답글 남기기