Postgres 커넥션 풀 고갈 대응 플레이북: 배포 30분 안에 타임아웃 폭주를 멈추는 실무 순서
💡 Key Takeaways
- 커넥션 고갈 대응은 DB 튜닝보다 요청 패턴 차단과 롤백 기준 고정이 먼저다.
- 증상 확인, 완화, 원인 수정, 재발 방지 단계를 분리하면 복구 시간이 크게 줄어든다.
- 풀 사이즈 확대는 마지막 수단이며, 누수와 장기 트랜잭션부터 확인해야 한다.
Postgres 커넥션 풀 고갈 대응 플레이북
월요일 오전 배포 직후부터 결제 API p95가 400ms에서 4초대로 치솟았고, 애플리케이션 로그에는 timeout acquiring client from pool이 반복됐습니다. 처음에는 DB CPU가 70% 수준이라 인프라 병목이 아니라고 판단해 대응이 늦어질 뻔했습니다. 결정적 단서는 같은 시각에 워커 큐 지연이 함께 올라가면서 웹 트래픽이 아닌 백그라운드 잡에서 연결을 오래 붙잡고 있다는 정황이 나온 점이었습니다. 온콜 채널에서 "일단 풀 사이즈를 두 배로 늘리자"는 의견이 나왔지만, 지난 장애에서 임시 증설 뒤 누수가 더 커졌던 경험이 있어 이번엔 순서를 바꿨습니다. 먼저 신규 배포 기능 중 대량 정산 배치를 중지하고, 읽기 전용 리포트 엔드포인트에 429 제한을 걸어 커넥션 점유 시간을 즉시 줄였습니다. 이 15분 완화 조치만으로 타임아웃 비율이 18%에서 6%로 떨어졌고, 원인 분석을 위한 시간을 확보했습니다. 같은 시간대 고객센터 실패 주문 ID를 샘플링했더니 오류가 특정 배치 실행 구간에 집중돼 있어, 인프라 증설보다 작업 흐름 분리가 우선이라는 판단을 빠르게 확정할 수 있었습니다.
판단 기준은 네 가지로 고정했습니다. 첫째, active, idle, waiting 커넥션 수를 같은 1분 창에서 보고 실제 병목이 대기열인지 확인합니다. 둘째, 평균 쿼리 시간보다 "트랜잭션 열린 상태 유지 시간"을 우선 봅니다. 짧은 쿼리라도 트랜잭션이 오래 열려 있으면 풀은 빠르게 고갈됩니다. 셋째, 애플리케이션 인스턴스 수와 풀 설정의 곱이 DB 최대 연결 수를 넘는지 계산합니다. 넷째, 최근 배포 diff에서 await 누락, 스트리밍 응답 중 DB 핸들 보유, 예외 경로의 client.release() 누락을 집중 점검합니다. 우리 팀은 이 체크를 템플릿으로 만들어 슬랙에 붙여 두고, 매번 같은 순서로 숫자를 채워 넣습니다. 감으로 원인을 토론하는 시간을 줄이고, "어떤 가설을 버렸는지"까지 기록해 재현 없는 추측 대응을 막는 것이 핵심이었습니다. 체크리스트 마지막 줄에는 "풀 수치 증설은 원인 확인 이후"를 명시해 즉흥적인 설정 변경으로 관측 데이터를 오염시키지 않도록 했습니다.
실행 절차는 증상 봉합과 코드 수정을 분리해야 흔들리지 않습니다. 봉합 단계에서는 트래픽 컷오프 대상을 먼저 정합니다. 이번엔 매출 영향이 낮은 백오피스 리포트와 대량 배치부터 제한했고, 사용자 핵심 경로는 유지했습니다. 이어서 DB에서 장기 세션 상위 목록을 뽑아 서비스와 매핑했는데, 신규 배치 워커 한 종류가 BEGIN 후 외부 API 재시도를 3회 수행하면서 트랜잭션을 붙든 채 대기하고 있었습니다. 수정 단계에서는 외부 API 호출을 트랜잭션 밖으로 이동하고, 실패 시 즉시 롤백되도록 단위를 쪼갰습니다. 동시에 커넥션 헬스 메트릭에 "대기열 30초 이상 지속" 경보를 추가했습니다. 배포 직전에는 롤백 기준도 명시했습니다. waiting > 20이 10분 지속되거나 결제 성공률이 2%p 하락하면 즉시 이전 버전으로 복귀. 기준을 숫자로 고정하니 의사결정이 빨라졌고, 실제로 재배포 25분 후 모든 지표가 복구됐습니다. 복구 직후에는 핫픽스 브랜치를 바로 닫지 않고 한 시간 동안 동일 경보 조건으로 관찰해, 반짝 정상화 뒤 재악화되는 패턴이 없는지도 확인했습니다.
운영 체크리스트에서 가장 효과가 컸던 항목은 두 가지입니다. 하나는 PR 단계에서 "트랜잭션 범위 안에서 네트워크 I/O 금지"를 정적 규칙으로 검사한 것, 다른 하나는 스테이징 부하 테스트에 풀 고갈 시나리오를 별도로 추가한 것입니다. 특히 max_connections를 늘리는 조치는 마지막 카드로 남겨 두는 것이 안전했습니다. 숫자를 키우면 당장은 버티지만, 누수 코드가 남아 있으면 장애 주기만 짧아집니다. 또 배포 당일에는 앱 로그만 보지 말고 DB 관점의 pg_stat_activity 스냅샷을 5분 간격으로 남겨야 원인 회고가 가능합니다. 우리는 이번 사고 이후 "연결 고갈은 성능 이슈가 아니라 릴리스 안전성 이슈"로 분류를 바꿨고, 배치 코드 병합 전 체크리스트를 필수화했습니다. 그 결과 같은 유형의 경고가 떠도 즉시 차단 지점을 고를 수 있게 됐고, 장애 대응 시간이 절반 이하로 줄었습니다. 지금은 신규 배치가 추가될 때마다 "최대 동시 실행 수, 트랜잭션 길이, 실패 재시도 간격" 세 값을 릴리스 노트에 함께 남기도록 운영 규칙을 바꿔 재발 확률을 낮추고 있습니다.