이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • Connection 고갈은 왜 DB 장애의 증폭기가 되는가?
  • 커넥션 고갈이 발생했을 때 어떤 원인을 구분해야 하는가?
  • 재시도와 timeout은 장애 전파에 어떤 영향을 주는가?

개요

Connection 고갈은 DB에 연결할 수 있는 통로가 부족한 상태다. 애플리케이션 풀에서 고갈될 수도 있고, DB 서버의 max_connections에 도달할 수도 있다.

이 문제는 단독으로 끝나지 않는다. 요청 스레드 대기, gateway timeout, 재시도 폭주, DB 추가 부하로 이어지며 전체 장애를 증폭한다.

원리

장애 전파는 다음처럼 진행된다.

특정 쿼리 지연
  커넥션 반납 지연
    pool active maximum 도달
      새 요청 대기
        timeout 발생
          클라이언트 재시도
            더 많은 요청 유입

이때 DB CPU가 높지 않을 수도 있다. lock wait나 외부 API 포함 트랜잭션 때문에 커넥션이 묶여 있다면 CPU는 낮고 대기만 많을 수 있다.

원인 구분

Connection 고갈을 보면 다음을 나눠야 한다.

  • 애플리케이션 pool이 작은가?
  • DB가 느려 커넥션 반납이 늦는가?
  • lock wait가 있는가?
  • 커넥션 누수가 있는가?
  • 트랜잭션 안에서 외부 작업을 하는가?
  • batch가 pool을 독점하는가?
  • 서버 인스턴스 증가로 총 연결 수가 늘었는가?

예외 메시지만으로는 원인을 알 수 없다.

HikariPool-1 - Connection is not available, request timed out after 3000ms.

이것은 커넥션을 못 얻었다는 결과다. 왜 못 얻었는지는 지표와 쿼리 상태를 봐야 한다.

DB max connections

애플리케이션 풀은 DB max_connections와 함께 봐야 한다.

API 서버 12대 x pool 20 = 240 connections
Batch 서버 2대 x pool 20 = 40 connections
Admin 서버 2대 x pool 10 = 20 connections
합계 = 300 connections

DB max connections가 300이면 운영자 접속, migration, 모니터링 여유가 없다. 장애 중에는 이 여유가 없어서 진단도 어려워진다.

오토스케일링도 연결 예산에 포함해야 한다.

평시: API 8대 x pool 12 = 96
스케일아웃: API 20대 x pool 12 = 240
batch/admin/monitoring 여유 = 60
필요 예산 = 300+

트래픽이 늘 때 애플리케이션 인스턴스가 자동으로 늘어나면 DB 연결도 같이 늘어난다. DB가 감당할 수 없는 연결 수까지 확장되면 오토스케일링이 장애 완화가 아니라 증폭기가 된다.

재시도 폭주

Timeout이 난 요청을 즉시 재시도하면 장애가 커진다.

@Retryable(maxAttempts = 3)
public OrderResult placeOrder(OrderCommand command) {
    return orderClient.place(command);
}

재시도는 일시 오류에 유용하지만, DB가 이미 포화된 상황에서는 트래픽을 세 배로 만들 수 있다. 재시도에는 backoff, jitter, circuit breaker, idempotency가 필요하다.

실전 대응

장애 중에는 다음을 우선한다.

  • 비핵심 batch 중지
  • 문제 endpoint rate limit
  • 재시도 폭주 차단
  • 오래 열린 트랜잭션 확인
  • blocking query 확인
  • pool과 DB 연결 총량 확인
  • DB가 여유 있을 때만 제한적으로 pool 조정

근본 해결은 별도로 진행한다.

  • 느린 쿼리 개선
  • 트랜잭션 경계 축소
  • 외부 I/O 분리
  • pool 분리
  • timeout 계층 정비
  • read/write 분리

실전 팁

  • 사용자 API와 batch의 DataSource를 분리하면 batch가 API pool을 고갈시키는 일을 줄일 수 있다.
  • pool acquire timeout은 짧게 두어 장애를 빨리 감지한다.
  • timeout 오류율은 알림 기준에 포함한다.
  • 재시도는 오류 종류별로 제한하고 backoff를 둔다.
  • DB max connections는 운영 접속 여유를 남긴다.
  • 오토스케일링 최대 인스턴스 수까지 포함해 connection budget을 계산한다.
  • 사용자 API, batch, admin은 가능하면 pool과 rate limit을 분리한다.
  • pool acquire time, active, idle, pending, timeout을 endpoint 지표와 함께 본다.

위험 신호!

  • 모든 서비스가 같은 DB 계정과 같은 pool 예산을 쓴다.
  • timeout마다 즉시 3회 재시도한다.
  • DB max connections에 거의 붙어 있는데 오토스케일링이 켜져 있다.
  • pool 고갈이 발생해도 어떤 query가 커넥션을 잡고 있는지 모른다.
  • 장애 중 pool size를 늘리는 것 말고 다른 완화책이 없다.
  • 오토스케일링으로 인스턴스가 늘 때 DB connection 총량도 같이 늘어난다는 점을 계산하지 않는다.
  • batch와 export가 사용자 API와 같은 pool에서 무제한 실행된다.

확인 질문

  • Connection 고갈이 장애를 증폭하는 이유는 무엇인가?
    • 커넥션을 기다리는 요청이 쌓이고 timeout과 재시도가 늘어 DB와 애플리케이션에 더 많은 부하를 만들기 때문이다.
  • DB CPU가 낮은데 connection 고갈이 생길 수 있는 이유는 무엇인가?
    • lock wait나 외부 작업 포함 트랜잭션처럼 CPU를 쓰지 않고 대기하는 작업이 커넥션을 오래 점유할 수 있기 때문이다.
  • 재시도 정책에 idempotency가 필요한 이유는 무엇인가?
    • 같은 요청이 여러 번 실행되어도 중복 주문, 중복 결제, 중복 포인트 지급이 발생하지 않도록 하기 위해서다.
  • 오토스케일링 환경에서 connection budget을 계산해야 하는 이유는 무엇인가?
    • 인스턴스 수가 늘면 pool 총합도 늘어 DB max connections와 운영 접속 여유를 빠르게 소진할 수 있기 때문이다.

참고 문서