HikariCP는 자체 자료구조 ConcurrentBag
public interface IConcurrentBagEntry
{
int STATE_NOT_IN_USE = 0;
int STATE_IN_USE = 1;
int STATE_REMOVED = -1;
int STATE_RESERVED = -2;
boolean compareAndSet(int expectState, int newState);
void setState(int newState);
int getState();
}
ConcurrentBag의 borrow 메소드를 통해 Connection을 얻을 수 있다.
커넥션을 ConcurrentBag에서 꺼내준다. 효율적인 동시성 제어(임계영역 진입 최소화)를 위해 쓰레드로컬을 이용하여 캐시된 BagEntry를 우선적으로 사용한다. 쓰레드로컬로부터 Connection 을 얻지 못할 경우 공유자원에서 사용 가능한 Connection을 확인하고, 사용가능한 자원이 없는 경우 handoffQueue를 활용하여 지정된 timeout 시간 동안 다른 쓰레드로 부터 Connection을 넘겨받기를 기다린다. 코드에서 효율적인 동시성제어를 위한 고민이 보이는 것 같다.
waiters수만큼 루프를 돌면서 handoffQueue가 꽉차거나 사용중이면 반환을 하며 ThreadLocal에서 관리하는 리스트에 커넥션을 반환하는 역할을 하는 메서드이다.
ProxyConnection은 close 호출 시 Connection을 풀에 반환하도록 오버라이딩 되어있다. 그래서 풀에서 받은 Connection 타입을 close 메소드를 호출 시 dynamic binding으로 ProxyConnection 의 close 메소드가 호출된다. 결국 DB와의 연결 해제가 아닌 Pool에 반환한다.
public final void close() throws SQLException {
this.closeStatements();
if (this.delegate != ProxyConnection.ClosedConnection.CLOSED_CONNECTION) {
this.leakTask.cancel();
try {
if (this.isCommitStateDirty && !this.isAutoCommit) {
this.delegate.rollback();
this.lastAccess = ClockSource.currentTime();
LOGGER.debug("{} - Executed rollback on connection {} due to dirty commit state on close().", this.poolEntry.getPoolName(), this.delegate);
}
if (this.dirtyBits != 0) {
this.poolEntry.resetConnectionState(this, this.dirtyBits);
this.lastAccess = ClockSource.currentTime();
}
this.delegate.clearWarnings();
} catch (SQLException var5) {
if (!this.poolEntry.isMarkedEvicted()) {
throw this.checkException(var5);
}
} finally {
this.delegate = ProxyConnection.ClosedConnection.CLOSED_CONNECTION;
this.poolEntry.recycle(this.lastAccess);
}
}
}
프로그램 운영 중 하우스키핑은 프로그램의 정상 동작을 위한 관리 및 보조를 의미한다. HikariPool의 HouseKeeping은 내부클래스로 구현되어있다.
private final class HouseKeeper implements Runnable
{
private volatile long previous = plusMillis(currentTime(), -housekeepingPeriodMs);
@Override
public void run()
{
try {
// refresh values in case they changed via MBean
connectionTimeout = config.getConnectionTimeout();
validationTimeout = config.getValidationTimeout();
leakTaskFactory.updateLeakDetectionThreshold(config.getLeakDetectionThreshold());
catalog = (config.getCatalog() != null && !config.getCatalog().equals(catalog)) ? config.getCatalog() : catalog;
final long idleTimeout = config.getIdleTimeout();
final long now = currentTime();
// Detect retrograde time, allowing +128ms as per NTP spec.
if (plusMillis(now, 128) < plusMillis(previous, housekeepingPeriodMs)) {
logger.warn("{} - Retrograde clock change detected (housekeeper delta={}), soft-evicting connections from pool.",
poolName, elapsedDisplayString(previous, now));
previous = now;
softEvictConnections();
return;
}
else if (now > plusMillis(previous, (3 * housekeepingPeriodMs) / 2)) {
// No point evicting for forward clock motion, this merely accelerates connection retirement anyway
logger.warn("{} - Thread starvation or clock leap detected (housekeeper delta={}).", poolName, elapsedDisplayString(previous, now));
}
previous = now;
String afterPrefix = "Pool ";
if (idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize()) {
logPoolState("Before cleanup ");
afterPrefix = "After cleanup ";
final List<PoolEntry> notInUse = connectionBag.values(STATE_NOT_IN_USE);
int toRemove = notInUse.size() - config.getMinimumIdle();
for (PoolEntry entry : notInUse) {
if (toRemove > 0 && elapsedMillis(entry.lastAccessed, now) > idleTimeout && connectionBag.reserve(entry)) {
closeConnection(entry, "(connection has passed idleTimeout)");
toRemove--;
}
}
}
logPoolState(afterPrefix);
fillPool(); // Try to maintain minimum connections
}
catch (Exception e) {
logger.error("Unexpected exception in housekeeping task", e);
}
}
}
하이스키핑를 운영하기 위해 ScheduledExecutorService(corePoolSize=1)를 초기화 하여 사용한다. 하우스키퍼는 ScheduledThreadPoolExecutor
에 의해 30초마다 동작한다.
private ScheduledExecutorService initializeHouseKeepingExecutorService()
{
if (config.getScheduledExecutor() == null) {
final ThreadFactory threadFactory = Optional.ofNullable(config.getThreadFactory()).orElseGet(() -> new DefaultThreadFactory(poolName + " housekeeper", true));
final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, threadFactory, new ThreadPoolExecutor.DiscardPolicy());
executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
executor.setRemoveOnCancelPolicy(true);
return executor;
}
else {
return config.getScheduledExecutor();
}
}
HikariCP에서 하우스키핑은 일정 시간(idle timeout) 사용되지 않은 유휴 상태(idle)의 Connection을 풀에서 제거하는 역할을 한다.
if (idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize()) {
logPoolState("Before cleanup ");
afterPrefix = "After cleanup ";
final List<PoolEntry> notInUse = connectionBag.values(STATE_NOT_IN_USE);
int toRemove = notInUse.size() - config.getMinimumIdle();
for (PoolEntry entry : notInUse) {
if (toRemove > 0 && elapsedMillis(entry.lastAccessed, now) > idleTimeout && connectionBag.reserve(entry)) {
closeConnection(entry, "(connection has passed idleTimeout)");
toRemove--;
}
}
}
프로퍼티로 minimum idle과 maximum pool size를 설정해서 해당 기능을 테스트 해볼 수 있다.
spring:
datasource:
hikari:
minimum-idle: 10
maximum-pool-size: 100
기본 값은 minimum idle=10, maximum pool size=10 으로 프로퍼티 지정 없이 동작하면 하우스키핑은 동작하지 않는다. 실제 릴리즈 환경에서는 리소스를 효율적으로 활용하기 위해 프로퍼티 지정이 반드시 필요할 것 같다.
HikariPool 분석 중 ProxyLeakTaskFactory 클래스가 있어서 Hikari Pool에서 따로 연결에 대한 누수 관리를 해주는 부분인 것 같아 관심이 생겨 확인해봤다. getConnection 호출 시 ProxyConnection에 LeakTask를 전달해주는데, leakDetectionThreshold
(milliseconds, 기본값=0L) 만큼 반환이 안되면 로그가 발생한다. 정확하게는 누수라기보다 지연되는 Connection 검출하는 기능인 것 같다.
설정 이름 | 기본 값 | 설명 |
---|---|---|
keepalive-time | 0 (milliseconds) | 지속적인 연결을 유지하기 위해 서버에 핑 패킷 지정 값마다 전송한다. (기본값으로는 기능이 동작하지 않음) |
connection-test-query | “” | 해당 값을 지정하면 핑 패킷이 아닌 해당 쿼리로 검증한다. (추천하지 않음) |
max-lifetime | 1_800_000 (milliseconds) | 해당 시간 만큼 사용하지 Connection을 사용하지 않으면 종료한다. (MySQL의 wait_timeout보다 큰 값일 경우 문제가 될 수 있음) |
minimum-idle | 10 | 최소 유지 Connection 수 |
maximum-pool-size | 10 | Pool의 최대 Connection 수, 기본 설정을 사용할 경우 Pool의 유동성이 없음. |
idle-timeout | 600_000 (milliseconds) | Connection 유휴 타임아웃 |
public final class PingPacket implements ClientMessage {
/** default instance */
public static final PingPacket INSTANCE = new PingPacket();
@Override
public int encode(Writer writer, Context context) throws IOException {
writer.initPacket();
writer.writeByte(0x0e);
writer.flush();
return 1;
}
}