MariaDB에서 Connection Pool 연결 끊김 문제
한창 개발중이던 프로젝트를 개발서버에 배포해놓고 단위 테스트를 진행했었는데(JUnit 사용 안하는 프로젝트)
다음날만 되면 DB Connetion이 끊어져 있어서 매번 톰캣을 재구동 시켜야 하는 상황이 생겼었다.
유사 프로젝트에서는 한번도 발생하지 않았던 상황이라 되짚어 보면 좋을 것 같아 남긴다.
서버 구성
- Server 1에 WAS 2대, Server 2에 MariaDB 1대
- WAS 1 : 어드민 웹 애플리케이션
- WAS 2 : 대시보드 형태이기 때문에 로그인 세션, DB 커넥션 유지가 필요한 웹 애플리케이션
- 공통적으로 두대의 WAS 모두 10초마다 DB Polling 으로 읽어들이고 있는 데이터가 있음
문제 상황
- Server 1에 배포되어 있는 두대의 웹 애플리케이션이 다음날이 되면 DB Connection이 끊겨있는 현상 발생
- 개발 환경이기에 개발자가 WAS 재구동으로 일시적인 문제 해결은 되었지만 운영 환경이 문제
에러
예기치 못한 에러로 프로세스가 종료가 된다면, 두대의 WAS에서 주기적인 DB Polling을 하는 DB가 종료되었으면 되었지 WAS 두대 모두가 종료될 것이라는 생각은 하지 못했었다. 유사 케이스에서도 이런 일은 발생한 적이 없었기 때문에 더욱 이해가 가지 않았다.
com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure
com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: The last packet successfully received from the server was 39,196,147 milliseconds ago.
The last packet sent successfully to the server was 39,196,154 milliseconds ago. is longer than the server configured value of 'wait_timeout'.
You should consider either expiring and/or testing connection validity before use in your application, increasing the server configured values for client timeouts, or using the Connector/J connection property 'autoReconnect=true' to avoid this problem.
점검
이전 유사 케이스와 현재 상황이 어떻게 다른지, 왜 이런 현상이 발생하는지 알아야했다.
- 3.x → 4.x 전자정부프레임워크 변경
- 외장 톰캣 → 내장 톰캣 변경
- 전체적으로 리팩토링된 소스
- 1.4 → 2.9.0 commons-dbcp2 의 버전
- Spring Boot 2.7.0 사용
이전과 달라진 사항들이 있다면 위와 같은데, 컴퓨터는 거짓말을 하지 않으니 사람의 손을 탄 소스를 먼저 확인해보았다. 아무래도 리팩토링 된 소스 때문이지 않을까 싶었지만 테스트 결과 Polling 로직에는 이상이 없었다.
톰캣, 프레임워크 때문일까?
그랬다면 컴파일 또는 구동시 문제가 발생했을 것이기 때문에 제외됐다.
그렇담 무엇이 문제일까?
변경된 이력에 문제가 없다면 프로젝트 설정과 DB 설정 탓을 해볼 차례다.
가정과 시행착오
1. WAS에서 예상했던 DB Polling이 지속되지 않음
1-1. 리팩토링 된 Polling 로직이 문제일 거라 추측
- 브라우저에서 웹 애플리케이션 실행 후 브라우저 종료
- 10 - 18시까지 Polling 로그 확인됨
- 결과 : 약 22시 이후부터 DB Connection Pool 연결 실패
1-2. 테스트용 스케줄러의 성공 가능성 추측
- 기존 Polling 로직 실행
- 1번과 별개로 Connection용 쿼리 Spring Scheduler 등록 및 실행
- 결과 : 약 22시 이후부터 DB Connection Pool 연결 실패
2. DB 서버 측에서 커넥션을 강제 종료
2-1. MariaDB에 기본 설정되어 있는 wait_timeout 수정
- DB에서 wait_timeout 설정값 확인
- show variables like '%timeout%';
- 고객사 DB라 설정 변경 불가
- 결과 : 가능하다 해도 근본적인 해결이 아니므로 최악의 방법이라 판단하여 설정 보류
3. DBCP 버전 문제
3-1. 기존 사용했던 Commons DBCP 1.4 처럼 메모리 누수 버그 가능성
- 로그에 남은 메세지는 Connection 관련 메세지
- OutOfMemoryError 에러 메세지는 확인되지 않음
- 결과 : 메모리 누수를 해결한 2.x 버전에서 고민할 문제가 아니라고 판단
해결 : DBCP 커넥션 풀 설정
ChatGPT에게 아직 낯가리던 때라 구글 선생님께서 알려주신 마지막 방법이였다.
이걸로도 실패하면 팀장님의 바짓가랑이라도 붙잡고 도와달라고 해야하는 상황이였다.
1. 에러 로그에서 안내된 autoReconnect=true 설정
- xml에 autoReconnection 설정
- 재기동 후 다음날 확인
- 결과 : 실패
2. testWhileIdle와 validationQuery 설정
- xml에 testWhileIdle와 validationQuery 설정
- 재기동 후 다음날 확인
- 결과 : 실패
spring:
datasource:
driverclassname:
url:
username:
password:
testWhileIdle: true
validationQuery: SELECT 1
오픈날까진 여유롭다 하여도 설정 하나 바꾸고 검증할 때마다 하루의 시간이 소요되다보니 ‘이번에는 꼭 되게해주세요🙏’라며 개발신에게 기도하게 되었다. 하지만 계속되는 실패로 놓치고 있는 새로운 가정은 없는지 점검했던 항목들을 다시 살펴보았다.
3. 이전과 달라진 프레임워크 버전
3.x → 4.x 전자정부프레임워크를 변경했다(!!!)
이 둘의 가장 큰 차이점은 Spring에서 Spring Boot를 사용함으로써 스프링 설정 방법이 달라졌다는 건데,
DBCP 설정을 yml 파일에만 작성하고 @Configration 에서 읽어올 수 있도록 추가하지 않은 것이다.
마치 3.x의 xml 처럼하고 있었다(....)
바로 자바 설정 파일을 찾아 testWhileIdle와 validationQuery 설정을 추가해줬고,
다음날 두대의 WAS 모두 DB Connection이 살아있음을 확인할 수 있었다.
testWhileIdle와 validationQuery 설정
- @Configuration에 testWhileIdle와 validationQuery 설정
- 재기동 후 다음날 확인
- 결과 : 성공(!)
@Configuration
public class EgovDatasource {
@Value("${spring.datasource.driverclassname}")
private String driverClassName;
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String userName;
@Value("${spring.datasource.password}")
private String password;
@Value("${spring.datasource.testWhileIdle}")
private boolean testWhileIdle;
@Value("${spring.datasource.validationQuery}")
private String validationQuery;
@Bean(name = "dataSource")
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(url);
dataSource.setUsername(userName);
dataSource.setPassword(password);
// Connection 테스트 및 풀 관련 설정
dataSource.setTestWhileIdle(testWhileIdle);
dataSource.setValidationQuery(validationQuery);
return dataSource;
}
}
마치며
- 익숙한 환경이라도 변경된 사항이 있다면 Human Side Effect를 체크하자
- 성공, 실패, 예외에 대한 설정값을 추가하자(기존엔 DB연결 설정만 존재하였음)
찝찝한 궁금증들
- 정말 Polling에는 문제가 없었던게 맞았을까? 다른 프로젝트들도 한번쯤 이런 이슈가 발생해야 하지 않았을까? 왜 이 프로젝트에서만 문제가 발생했을까? DBCP 설정을 추가해서 해결하는게 근본적인 해결이 맞을까?
- @Configuration 에 추가하지 않은 것이 문제였더라면 권장하지 않는 방법이라고 들었지만 autoReconnection 설정도 되긴 했을까?