,

[백엔드 운영] 커넥션을 얼마나 기다리고 얼마나 오래 살릴까 — timeout과 lifetime을 Hikari·pgxpool로 비교

이 글의 위치: 기술 블로그 → 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 closedconnection 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는 connectionTimeoutSQLException(전역 예외), Go는 ctx deadline→context.DeadlineExceeded(호출 단위). 정밀함 vs 단순함.
  • fast-fail: 긴 timeout은 에러를 0으로 만들지만 장애를 숨긴다. 짧은 timeout으로 빨리 실패시키는 쪽이 시스템 전체로는 건강하다.
  • lifetime: maxLifetime은 인프라의 어떤 타임아웃보다 짧아야 한다. 안 그러면 풀이 죽은 커넥션을 빌려준다.
  • 풀과 DB의 시간 설정은 따로 보면 안 된다 — 같이 봐야 함정이 보인다.

다음 편은 시리즈의 백미 — 커넥션 누수. Hikari가 옵션 한 줄로 해주던 누수 추적을, pgxpool에선 직접 만들어 보며 "프레임워크가 공짜로 해주던 게 뭔지"를 들여다본다.


참고

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다