데이터베이스

HikariCP의 커넥션 maxLifetime과 MySQL wait timeout간의 관계 알아보기!

SeongOnion 2024. 1. 23. 22:47
728x90

서론

사내 레거시 시스템 문제로 인해 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 시간동안 커넥션 안쓰면 다시 가져올거야~" 이다.

 

둘 사이의 관계

애플리케이션 서버와 같은 데이터베이스 클라이언트의 입장에선 데이터베이스 상에 설정된 여러 값들을 고려하여 커넥션 풀 기술을 사용해야한다.

 

현재 소개한 HikariCPmaxLifetime과 MySQL의 wait timeout 값 또한 밀접한 관계가 있으며, 이를 제대로 이해하지 못한 채 잘못된 값을 설정하면 커넥션 획득 및 관리에 실패할 가능성이 있다.

 

일반적으로 문제는 maxLifeTime이 wait timeout보다 더 길 때 발생한다.

 

만약 wait timeout30초로 설정된 MySQL 서버로부터 커넥션을 받아 HikariCP가 커넥션 풀을 생성한다고 가정해보자.

편의상 커넥션 크기는 3개로 하자

그런데 ConnectionPoolmaxLifeTime이 MySQL의 wait timeout보다 긴 60초로 설정되어 있다고 가정하자.

시간은 흘러흘러 커넥션을 맺은지 30초가 넘었지만, 서버가 한가한지 커넥션을 하나도 사용하지 않았다.

 

설정한 wait timeout 시간동안 커넥션으로 한 번도 요청이 들어오지 않았으므로, MySQL은 커넥션을 수거하기 시작한다.

MySQL은 커넥션 생성시간을 기준으로 wait timeout 시간이 경과한 커넥션을 모두 회수하였지만, HikariCPConnectionPool은 이 사실을 모른 채 죽어있는 커넥션을 계속해서 들고 있다.

 

맺어놓은 커넥션에 대해서 실제 사용 전에는 따로 유효성 체크를 따로 하지 않기 때문이다.

 

그러다가 maxLifetime인 60초가 경과하기 전에 드디어 서버에 요청이 들어와 커넥션을 사용할 일이 생겼다!

 

ConnectionPool은 갖고있던 커넥션을 하나 꺼내 쓰라고 주지만, 해당 커넥션은 이미 죽어있는 상태이다.

결국 서버는 정상적인 응답을 내려주지 못하거나 혹은 ConnectionPool이 그제서야 부랴부랴 새로운 커넥션을 다시 생성해 요청을 처리해주게 된다.

 

 

이러한 상황을 막기 위해서 HikariCP의 공식문서에서는 커넥션 풀의 maxLifetime값을 데이터베이스에 설정된 커넥션 관련 타임 설정보다 더 짧게 설정하도록 권고하고 있다.

https://github.com/brettwooldridge/HikariCP

사내 MySQL 서버의 경우, DBCP 기술을 사용하지 않던 레거시 코드들에서 발생하던 커넥션 누수들이 많아 이를 해결하기 위해 전역 wait timeout을 15초로 설정하였다고 한다.

 

그러나 위 설명에서도 나와있듯, HikariCP에서 설정할 수 있는 최소 maxLifetime은 30초이기 때문에 15초보다 짧은 시간을 설정할 수 없었다.

 

당연히 글로벌한 wait timeout 시간을 변경하는 것은 불가능했으므로 아래와 같이 setConnectionInitSql() 메서드를 통해 애플리케이션과 데이터베이스의 세션 간 wait timeoutmaxLifetime보다 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 개발자가 위와 같은 질문에 남긴 답변 중 일부이다.

https://github.com/brettwooldridge/HikariCP/issues/766

대략적으로 요약하자면, 유휴 상태의 커넥션에 대한 테스팅은 불필요한 쿼리를 발생시키고 데이터베이스에 설정된 타임아웃 값을 무력화하며 네트워크 인프라 팀의 통제를 빼앗는다는 것이다.

끄덕끄덕.. 너무나도 인정되는 부분이다.

 

참고

https://github.com/brettwooldridge/HikariCP

https://pkgonan.github.io/2018/04/HikariCP-test-while-idle