커넥션 테스트와 유효성 검사
데이터베이스 커넥션 풀에서 가장 중요한 책임 중 하나는 애플리케이션에 항상 유효한 커넥션을 제공하는 것입니다. 네트워크 문제, 데이터베이스 재시작, 자원 제한 등 다양한 이유로 커넥션이 무효화될 수 있는 환경에서 이는 결코 쉬운 일이 아닙니다.
커넥션 유효성 검사의 중요성
유효하지 않은 커넥션이 애플리케이션에 전달되면 다음과 같은 심각한 문제가 발생할 수 있습니다:
런타임 예외
애플리케이션이 연결이 끊긴 커넥션을 사용하려 할 때 예외가 발생하여 사용자 작업이 실패하고 오류 메시지가 표시됩니다.
타임아웃 지연
일부 경우에는 연결이 끊긴 커넥션을 사용할 때 즉시 예외가 발생하지 않고 네트워크 타임아웃까지 대기하게 되어 애플리케이션이 장시간 응답하지 않을 수 있습니다.
연쇄적 오류
한 커넥션의 문제가 여러 사용자 요청에 영향을 미칠 수 있으며, 이는 시스템 전체의 성능 저하로 이어질 수 있습니다.
HikariCP의 커넥션 검증 전략
HikariCP는 다양한 상황에서 커넥션 상태를 검증하기 위해 여러 전략을 제공합니다:
커넥션 테스트 시점
대여 전 검증 (Connection Validation on Borrow)
유휴 시간 검증 (Idle Connection Validation)
주기적 검증 (Background Validation)
외부 트리거 검증 (Externally Triggered Validation)
이러한 각 검증 시점은 HikariCP 설정을 통해 구성할 수 있습니다.
구성 옵션
HikariCP는 커넥션 유효성 검사와 관련된 다양한 설정 옵션을 제공합니다:
public class HikariConfig {
// 커넥션 대여 시 검증 활성화 여부
private boolean connectionTestOnBorrow = false;
// 커넥션 반환 시 검증 활성화 여부
private boolean connectionTestOnReturn = false;
// 유휴 커넥션 검증 활성화 여부
private boolean connectionTestWhileIdle = true;
// 유휴 커넥션 검증 주기 (밀리초)
private long validationInterval = TimeUnit.SECONDS.toMillis(30);
// 커넥션 테스트 쿼리
private String connectionTestQuery;
// 커넥션 상태 검사 방법 (JDBC4, JDBC42, QUERY)
private String connectionValidationMethod = "JDBC4";
// 검증 타임아웃 (밀리초)
private long validationTimeout = TimeUnit.SECONDS.toMillis(5);
// 유휴 커넥션 제거 활성화 여부
private boolean removeAbandoned = false;
// 유휴 커넥션으로 간주되기 전 시간 (초)
private long removeAbandonedTimeout = TimeUnit.SECONDS.toSeconds(60);
}
검증 메서드
HikariCP는 커넥션 상태를 검사하기 위한 세 가지 주요 방법을 지원합니다:
1. JDBC4 isValid() 메서드
JDBC 4.0 이상에서는 Connection 인터페이스에 isValid(int timeout)
메서드가 추가되었습니다. 이 메서드는 드라이버 수준에서 커넥션의 유효성을 가장 효율적으로 검사할 수 있습니다:
private boolean isConnectionAlive(final Connection connection) {
try {
if (connectionValidationMethod.equalsIgnoreCase("JDBC4")) {
return connection.isValid((int) TimeUnit.MILLISECONDS.toSeconds(validationTimeout));
}
// 다른 검증 방법 처리
}
catch (SQLException e) {
return false;
}
}
2. 커스텀 테스트 쿼리
데이터베이스 드라이버가 JDBC4 isValid()
메서드를 올바르게 구현하지 않은 경우, 사용자 정의 테스트 쿼리를 사용하여 커넥션을 검증할 수 있습니다:
private boolean isConnectionAlive(final Connection connection) {
try {
if (connectionValidationMethod.equalsIgnoreCase("QUERY") && connectionTestQuery != null) {
try (Statement statement = connection.createStatement()) {
if (statement.execute(connectionTestQuery)) {
try (ResultSet rs = statement.getResultSet()) {
return rs.next(); // 결과 집합이 있으면 커넥션이 활성 상태
}
}
}
}
// 다른 검증 방법 처리
}
catch (SQLException e) {
return false;
}
}
3. 메타데이터 메서드 호출
일부 데이터베이스 드라이버에서는 메타데이터 메서드를 호출하여 커넥션 상태를 확인할 수 있습니다:
private boolean isConnectionAlive(final Connection connection) {
try {
if (connectionValidationMethod.equalsIgnoreCase("METADATA")) {
connection.getMetaData(); // 메타데이터 액세스가 가능하면 커넥션이 활성 상태
return true;
}
// 다른 검증 방법 처리
}
catch (SQLException e) {
return false;
}
}
커넥션 대여 시 검증
HikariCP는 애플리케이션에 커넥션을 제공하기 전에 해당 커넥션이 여전히 유효한지 확인할 수 있습니다:
public Connection getConnection() throws SQLException {
final PoolEntry poolEntry = connectionBag.borrow(connectionTimeout, MILLISECONDS);
if (poolEntry == null) {
throw new SQLTransientConnectionException("Connection is not available, timeout after " + connectionTimeout + "ms");
}
final Connection connection = poolEntry.connection;
// 대여 전 검증이 활성화된 경우 수행
if (connectionTestOnBorrow) {
if (!isConnectionAlive(connection)) {
closeConnection(poolEntry, "Connection failed validation check on borrow");
// 재귀적으로 다른 커넥션 요청
return getConnection();
}
}
return connection;
}
백그라운드 커넥션 검증
HikariCP는 별도의 스레드를 사용하여 백그라운드에서 유휴 커넥션을 주기적으로 검사할 수 있습니다:
private class HouseKeeper implements Runnable {
private volatile long lastValidationTime;
@Override
public void run() {
try {
// 마지막 검증 이후 충분한 시간이 지났는지 확인
final long now = currentTime();
if (now - lastValidationTime > validationInterval) {
lastValidationTime = now;
// 유휴 커넥션 검증
validateIdleConnections();
}
}
catch (Exception e) {
logger.error("Unexpected exception in housekeeping task", e);
}
}
/**
* 유휴 커넥션을 검증하고 필요시 교체합니다.
*/
private void validateIdleConnections() {
final List<PoolEntry> idleEntries = connectionBag.getIdleEntries();
for (PoolEntry entry : idleEntries) {
if (!isConnectionAlive(entry.connection)) {
// 문제가 있는 커넥션 폐기
closeConnection(entry, "Connection failed validation check during idle check");
// 필요시 새 커넥션으로 교체
if (getTotalConnections() < getMinimumIdle()) {
addConnection();
}
}
}
}
}
커넥션 누수 감지와 회수
애플리케이션이 커넥션을 명시적으로 닫지 않는 경우, HikariCP는 이를 감지하고 필요한 경우 회수할 수 있습니다:
private class LeakDetector implements Runnable {
@Override
public void run() {
final long now = currentTime();
final long leakThreshold = removeAbandonedTimeout * 1000L;
if (removeAbandoned) {
final List<PoolEntry> inUseEntries = connectionBag.getInUseEntries();
for (PoolEntry entry : inUseEntries) {
if (entry.lastBorrowTime > 0 && now - entry.lastBorrowTime > leakThreshold) {
// 누수로 간주되는 커넥션 기록
logger.warn("Connection has been in use for {} ms (threshold: {}), considered leaked",
now - entry.lastBorrowTime, leakThreshold);
if (entry.leakTask != null) {
// 누수 스택 트레이스 로깅
entry.leakTask.cancel();
logger.warn("Stack trace where the connection was borrowed:", entry.leakTask.getStackTrace());
}
// 강제로 커넥션 회수
closeConnection(entry, "Connection leaked and reclaimed");
}
}
}
}
}
데이터베이스 리셋 감지 및 복구
네트워크 중단이나 데이터베이스 재시작 같은 이벤트는 풀의 모든 커넥션을 한 번에 무효화할 수 있습니다. HikariCP는 이러한 상황을 감지하고 풀을 효율적으로 복구하는 메커니즘을 갖추고 있습니다:
private void checkFailFast() {
if (failFastEnabled) {
final long startTime = currentTime();
try {
// 새 커넥션 생성 시도
final Connection connection = dataSource.getConnection();
connection.close();
}
catch (SQLException e) {
// 모든 커넥션 무효화
logger.warn("Database connectivity failure detected, marking all connections as broken");
evictAllConnections("Database appears to be down");
// 백오프 정책에 따라 재시도 일정 설정
scheduleHouseKeepingWithBackoff();
throw new SQLTransientConnectionException("Database connectivity failure detected", e);
}
}
}
private void evictAllConnections(String reason) {
// 모든 커넥션을 무효화하고 풀 상태 리셋
connectionBag.close();
softEvictConnections(connectionBag.values(), reason);
// 새 커넥션백 생성
connectionBag = new ConcurrentBag<>(this);
}
알림 메커니즘
HikariCP는 커넥션 문제 발생 시 관리자에게 알리기 위한 다양한 메커니즘을 제공합니다:
로깅
문제가 있는 커넥션은 자세한 정보와 함께 로그에 기록됩니다:
private void handleBrokenConnection(PoolEntry poolEntry, String reason, Throwable cause) {
final String connectionId = poolEntry.connection.toString();
logger.warn("Connection {} marked as broken: {}", connectionId, reason, cause);
metrics.incrementBrokenConnectionCount();
closeConnection(poolEntry, reason);
}
JMX 모니터링
HikariCP는 JMX를 통해 풀 상태와 관련된 다양한 메트릭을 노출합니다:
public class HikariPool implements HikariPoolMXBean {
@Override
public int getActiveConnections() {
return connectionBag.getActiveCount();
}
@Override
public int getIdleConnections() {
return connectionBag.getIdleCount();
}
@Override
public int getTotalConnections() {
return connectionBag.getTotalCount();
}
@Override
public int getThreadsAwaitingConnection() {
return connectionBag.getWaitingThreadCount();
}
@Override
public int getConnectionTimeoutsPerSecond() {
return (int) metrics.getConnectionTimeoutRate();
}
}
헬스체크 인터페이스
HikariCP는 풀 상태를 외부 모니터링 시스템에 노출하기 위한 헬스체크 인터페이스를 제공할 수 있습니다:
public class HikariPoolHealthCheck implements HealthCheck {
private final HikariDataSource dataSource;
@Override
public Result check() {
final int active = dataSource.getHikariPoolMXBean().getActiveConnections();
final int max = dataSource.getMaximumPoolSize();
// 풀이 거의 포화 상태인 경우 경고
if (active > max * 0.9) {
return Result.unhealthy("Connection pool near capacity: %d of %d connections in use", active, max);
}
// 대기 스레드가 많은 경우 경고
final int waiting = dataSource.getHikariPoolMXBean().getThreadsAwaitingConnection();
if (waiting > 10) {
return Result.unhealthy("%d threads waiting for connections", waiting);
}
return Result.healthy("Pool state OK: %d active, %d idle, %d waiting",
active, dataSource.getHikariPoolMXBean().getIdleConnections(), waiting);
}
}
벤더별 최적화
HikariCP는 다양한 데이터베이스 벤더에 맞게 커넥션 검증 전략을 최적화합니다:
MySQL 최적화
private void configureValidationForMySql(HikariConfig config) {
// MySQL은 JDBC4 isValid()를 효율적으로 구현함
config.setConnectionValidationMethod("JDBC4");
// 기본 검증 쿼리 설정 (isValid()가 작동하지 않는 경우)
if (config.getConnectionTestQuery() == null) {
config.setConnectionTestQuery("SELECT 1");
}
// MySQL 서버 장애 감지를 위한 설정
if (config.getDataSourceProperties().getProperty("socketTimeout") == null) {
config.addDataSourceProperty("socketTimeout", String.valueOf(validationTimeout));
}
}
PostgreSQL 최적화
private void configureValidationForPostgreSQL(HikariConfig config) {
// PostgreSQL은 JDBC4 isValid()를 효율적으로 구현함
config.setConnectionValidationMethod("JDBC4");
// 기본 검증 쿼리 설정
if (config.getConnectionTestQuery() == null) {
config.setConnectionTestQuery("SELECT 1");
}
// Statement 타임아웃 설정
if (config.getDataSourceProperties().getProperty("socketTimeout") == null) {
config.addDataSourceProperty("socketTimeout", String.valueOf(validationTimeout));
}
}
Oracle 최적화
private void configureValidationForOracle(HikariConfig config) {
// Oracle 드라이버는 isValid()가 느릴 수 있으므로 간단한 쿼리 사용
config.setConnectionValidationMethod("QUERY");
// 기본 검증 쿼리 설정
if (config.getConnectionTestQuery() == null) {
config.setConnectionTestQuery("SELECT 1 FROM DUAL");
}
// Fast Connection Failover 활성화
if (config.getDataSourceProperties().getProperty("oracle.net.CONNECT_TIMEOUT") == null) {
config.addDataSourceProperty("oracle.net.CONNECT_TIMEOUT", String.valueOf(validationTimeout));
}
}
고급 검증 기법
HikariCP는 일반적인 검증 이외에도 몇 가지 고급 검증 기법을 제공합니다:
풀 자가 건전성 검사
HikariCP는 자체적으로 풀의 건전성을 주기적으로 진단하고 필요한 조치를 취합니다:
private void performPoolHealthCheck() {
final int totalConnections = getTotalConnections();
final int idleConnections = getIdleConnections();
final int activeConnections = getActiveConnections();
// 연결 수가 예상과 다른 경우 조사
if (totalConnections != activeConnections + idleConnections) {
logger.warn("Inconsistent pool state detected: total={}, active={}, idle={}",
totalConnections, activeConnections, idleConnections);
// 복구 조치 수행
recoverPoolState();
}
// 최소 유휴 커넥션 유지
if (idleConnections < config.getMinimumIdle() && totalConnections < config.getMaximumPoolSize()) {
addConnections(Math.min(config.getMinimumIdle() - idleConnections,
config.getMaximumPoolSize() - totalConnections));
}
}
커넥션 수명 주기 관리
HikariCP는 커넥션의 수명 주기를 관리하여 오래된 커넥션을 자동으로 교체합니다:
private void manageConnectionLifecycle() {
final long now = currentTime();
final List<PoolEntry> allEntries = connectionBag.values();
for (PoolEntry entry : allEntries) {
final long connectionAge = now - entry.createTime;
// 최대 수명 초과 여부 확인
if (connectionAge > config.getMaxLifetime()) {
// 커넥션이 사용 중이 아닐 때만 교체
if (connectionBag.reserve(entry)) {
closeConnection(entry, "Connection reached maximum lifetime");
if (getTotalConnections() < config.getMinimumIdle()) {
addConnection();
}
}
}
}
}
점진적 커넥션 교체
데이터베이스 재시작 같은 이벤트 이후 모든 커넥션을 한 번에 재생성하면 성능에 영향을 줄 수 있습니다. HikariCP는 커넥션을 점진적으로 교체하여 이러한 문제를 방지합니다:
private void softEvictConnections(Collection<PoolEntry> connections, String reason) {
for (PoolEntry poolEntry : connections) {
// 소프트 제거 플래그 설정
poolEntry.markEvicted();
// 비활성 커넥션은 즉시 제거
if (connectionBag.reserve(poolEntry)) {
closeConnection(poolEntry, reason);
}
}
}
HikariCP의 커넥션 테스트와 유효성 검사 메커니즘은 고성능 커넥션 풀링의 또 다른 중요한 측면을 보여줍니다. 단순히 빠른 커넥션 관리를 넘어 신뢰성과 안정성이 실제 운영 환경에서 얼마나 중요한지 강조합니다
Last updated