서론
사내 레거시 시스템 문제로 인해 DBCP(HikariCP
)를 사용하는 프로젝트에서 잦은 오류 및 경고 메시지가 발생했던 적이 있다.
경고 메시지는 대부분 아래와 같았다.
Failed to validate connection... Possibly consider using a shorter maxlifetime value.
해당 문제의 원인 파악을 위해 많은 서치를 했었고 결국엔 해결했었는데, 최근에 동료 개발자분께 동일한 문제가 발생하여 원인 및 해결책을 설명해줬다.
생각난김에 해당 내용을 다시 정리해보고자 한다.
해당 내용은 Spring Boot에서 HikariCP
를 사용해 MySQL 서버와의 커넥션을 맺는 상황을 전제로 작성된다.
HikariCP의 connection max lifetime
HikariCP
는 커넥션 풀링을 위해 사용자가 지정한 개수만큼의 커넥션을 DB 서버와 미리 만들어놓고, 커넥션이 필요한 상황마다 커넥션 풀에서 유휴상태(Idle)인 커넥션을 꺼내 사용할 수 있도록 한다.
그렇다면 처음 커넥션 풀을 채울 때 생성한 커넥션을 애플리케이션 서버가 떠있는 동안 계속해서 사용하는가?
물론 그렇지 않다.
HikariCP
는 사용자가 설정한 커넥션 최대 생명주기 즉, maxLifetime
만큼만 커넥션을 살려두고 해당 시간이 지난 커넥션은 제거(retire)시킨 후 새로운 커넥션을 맺는다.
이러한 동작방식은 데이터베이스 서버의 최신 변경사항을 반영하고 커넥션 유지 동안 발생할 수 있는 리소스의 누수를 막기 위한 선택이다.
다시 말해, HikariCP
에서 설정하는 maxLifetime
옵션은 커넥션이 커넥션 풀에서 살아있을 수 있는 최대 시간이다.
MySQL의 wait timeout
MySQL 서버는 애플리케이션 서버를 비롯한 여러 종류의 클라이언트들과 커넥션을 맺고 소통한다.
하지만 연결되는 클라이언트들이 많을수록 커넥션을 무한정하게 연결해두는 것은 리소스 낭비가 될 수 있다.
만약 커넥션이 연결되었음에도 얼마간 실제로 사용되고 있지 않은 클라이언트가 있다면 커넥션을 직접 닫아 리소스 낭비를 줄일 수 있다.
이러한 리소스 효율화를 위해 MySQL은 wait timeout
값을 사용한다.
클라이언트가 데이터베이스와 연결된 후, 정해둔 wait timeout
시간동안 아무런 작업을 하지 않는다면 MySQL 서버는 해당 커넥션을 닫아 낭비 가능성이 있는 리소스를 회수한다.
간단히 말하자면 "wait timeout 시간동안 커넥션 안쓰면 다시 가져올거야~" 이다.
둘 사이의 관계
애플리케이션 서버와 같은 데이터베이스 클라이언트의 입장에선 데이터베이스 상에 설정된 여러 값들을 고려하여 커넥션 풀 기술을 사용해야한다.
현재 소개한 HikariCP
의 maxLifetime
과 MySQL의 wait timeout
값 또한 밀접한 관계가 있으며, 이를 제대로 이해하지 못한 채 잘못된 값을 설정하면 커넥션 획득 및 관리에 실패할 가능성이 있다.
일반적으로 문제는 maxLifeTime이 wait timeout보다 더 길 때 발생한다.
만약 wait timeout이 30초로 설정된 MySQL 서버로부터 커넥션을 받아 HikariCP
가 커넥션 풀을 생성한다고 가정해보자.
그런데 ConnectionPool
의 maxLifeTime이 MySQL의 wait timeout
보다 긴 60초로 설정되어 있다고 가정하자.
시간은 흘러흘러 커넥션을 맺은지 30초가 넘었지만, 서버가 한가한지 커넥션을 하나도 사용하지 않았다.
설정한 wait timeout
시간동안 커넥션으로 한 번도 요청이 들어오지 않았으므로, MySQL은 커넥션을 수거하기 시작한다.
MySQL은 커넥션 생성시간을 기준으로 wait timeout
시간이 경과한 커넥션을 모두 회수하였지만, HikariCP
의 ConnectionPool
은 이 사실을 모른 채 죽어있는 커넥션을 계속해서 들고 있다.
맺어놓은 커넥션에 대해서 실제 사용 전에는 따로 유효성 체크를 따로 하지 않기 때문이다.
그러다가 maxLifetime
인 60초가 경과하기 전에 드디어 서버에 요청이 들어와 커넥션을 사용할 일이 생겼다!
ConnectionPool
은 갖고있던 커넥션을 하나 꺼내 쓰라고 주지만, 해당 커넥션은 이미 죽어있는 상태이다.
결국 서버는 정상적인 응답을 내려주지 못하거나 혹은 ConnectionPool
이 그제서야 부랴부랴 새로운 커넥션을 다시 생성해 요청을 처리해주게 된다.
이러한 상황을 막기 위해서 HikariCP
의 공식문서에서는 커넥션 풀의 maxLifetime
값을 데이터베이스에 설정된 커넥션 관련 타임 설정보다 더 짧게 설정하도록 권고하고 있다.
사내 MySQL 서버의 경우, DBCP 기술을 사용하지 않던 레거시 코드들에서 발생하던 커넥션 누수들이 많아 이를 해결하기 위해 전역 wait timeout
을 15초로 설정하였다고 한다.
그러나 위 설명에서도 나와있듯, HikariCP
에서 설정할 수 있는 최소 maxLifetime
은 30초이기 때문에 15초보다 짧은 시간을 설정할 수 없었다.
당연히 글로벌한 wait timeout
시간을 변경하는 것은 불가능했으므로 아래와 같이 setConnectionInitSql()
메서드를 통해 애플리케이션과 데이터베이스의 세션 간 wait timeout
을 maxLifetime
보다 5초 더 길게 설정하는 방식으로 문제를 해결했다.
long waitTimeOut = TimeUnit.MILLISECONDS.toSeconds(hikariConfig.getMaxLifetime()) + 5;
hikariConfig.setConnectionInitSql(String.format("SET SESSION wait_timeout = %s", waitTimeOut));
wait timeout이 경과하기 전에 커넥션에 대해 지속적으로 헬스체크를 하면 되지 않나?
MySQL서버에서 설정한 wait timeout
이 경과하기 전에 SELECT 1과 같은 핑 쿼리를 지속적으로 날려 데이터베이스에서 커넥션을 회수 할 수 없도록 할수도 있다.
실제로 또 다른 DBCP 기술인 Tomcat DBCP에선 testWhileIdle
이라는 설정 값을 통해 해당 작업을 해주도록 설정할 수도 있다.
하지만 HikariCP
의 경우는 조금 다른 철학을 가지고 있다.
아래는 HikariCP
개발자가 위와 같은 질문에 남긴 답변 중 일부이다.
대략적으로 요약하자면, 유휴 상태의 커넥션에 대한 테스팅은 불필요한 쿼리를 발생시키고 데이터베이스에 설정된 타임아웃 값을 무력화하며 네트워크 인프라 팀의 통제를 빼앗는다는 것이다.
끄덕끄덕.. 너무나도 인정되는 부분이다.
참고
'데이터베이스' 카테고리의 다른 글
Redis의 HA(High Availability) 전략 알아보기! (1) | 2024.05.02 |
---|---|
MySQL의 Using temporary, Using filesort (+ 정렬 방식) 정리! (1) | 2024.02.12 |
SQL의 SELECT 쿼리가 실행되는 순서 이해하기 (1) | 2024.01.05 |
[데이터베이스] 트랜잭션과 ACID (2) | 2021.08.08 |