Open KIMSEI1124 opened 1 year ago
DB Connection Pool 에 대해 알아보기에 앞서, 간단히 DB Connection 에 대해 정리하고자 한다. Connection
은 DB에 연결하는 객체를 의미하며, SQL문을 실행할 수 있는 Statement 객체를 생성하는 기능을 제공한다.
private DBUtil util = DBUtil.getUtil();
@Override
public Product select(String num) throws SQLException {
Product prod = null;
String sql = "select * from product where num=?";
try(Connection con = util.getConnection();
PreparedStatement ps = con.prepareStatement(sql);){
ps.setString(1, num);
try(ResultSet rs = ps.executeQuery()){
if(rs.next()) {
prod = new Product(num,rs.getString(2),rs.getInt("price"));
}
}
}
return prod;
}
이 때, 사용자가 요청을 할 때마다 JDBC Driver 를 로드하고 Connection 객체를 받아오는 것은 귀찮을 뿐더러 매우 비효율적이다. Connection을 획득하는 과정은 아래의 순서로 진행되어 비용이 많이 드는 작업이기 때문이다.
이런 비효율적인 과정을 해결하기 위해 나온 개념이 DB Connection Pool
이다.
이전 #7 이슈에서
Thread Pool
이 Thread를 재사용하여 성능과 리소스 관리에 도움을 주는 개념이라고 설명했는데DB Connection Pool
도 동일한 개념이라고 생각하면 된다.
웹 컨테이너(WAS)가 실행되면서 connection
객체를 미리 pool에 생성해둔다. 클라이언트는 pool에서 Connection
객체를 가져다쓰고 반환하면 된다. connection
을 생성하는데 드는 요청마다 연결 시간이 소비되지 않는다는 점이 가장 큰 장점이다.
하지만 connection
은 한정되어 있기 때문에, 동시 접속자가 많은 경우 클라이언트는 connection
이 반환될 때까지 대기 상태로 기다려야 한다는 단점이 존재한다. 대기 상태에 관해서는 아래에서 설명할 예정이다.
대표적인 DBCP 프레임워크는 아래와 같다.
이 중 가장 많이 사용되는 HikariCP 를 중점적으로 알아보고, 다른 프레임워크는 간단히 언급만 하려고 한다.
Fast, simple, reliable, HikariCP is a "zero-overhead" production ready JDBC connection pool. At roughly 130Kb, the library is very light.
HikariCP
는 Spring에 기본적으로 내장되어 있는 JDBC DBCP 이다. Spring 은 HikariCP
를 사용할 수 있는 상황이라면 항상 HikariCP
를 선택한다. 그 이유는, HikariCP
가 바이트코드 수준까지 최적화되어 있고 Collection
프레임워크를 영리하게 사용하여 성능이 압도적으로 우수하기 때문이다.
참고로, HikariCP 를 사용할 수 없는 경우 Tomcat DBCP -> Apache Commons DBCP -> Oracle UCP 순으로 선택해서 사용한다고 한다.
HikariCp
는 내부적으로 ConcurrentBag
이라는 구조체를 이용해 connection
을 관리한다. 이는 여러 스레드가 객체를 저장할 수 있으며, 스레드로부터 안전한 컬렉션 클래스 중 하나이다. getConnection()
을 호출하면 내부적으로 ConcurrentBag.borrow()
를 통해 사용 가능한(=idle) connection
을 리턴한다.
또한, HikariCP
는 Thread가 이전에 사용했던 connection
이 존재하는지 확인하고 이를 우선적으로 반환한다는 특징이 있다.
(각 DBCP 프레임워크 별 Thread 에게 connection
을 반환하는 방식에는 약간씩 차이가 존재한다. )
만약, 가능한 connection
이 존재하지 않으면 Thread 는 HandOffQueue 에서 대기를 하게 된다. 다른 Thread가 connection
을 반납할 때까지 대기하지만 일정 시간(=TimeOut) 까지 대기하다가 시간이 만료되면 예외를 던진다. 참고로 default TimeOut 시간은 30초이다.
Thread는 connection
을 다 사용한 후 Connection.close()
를 통해 다시 객체를 Pool에 반납한다.
(정상적으로 transaction이 commit 되거나 에러 등으로 rollback이 호출되면 Connection.close()
이 호출됨 )
해당 메서드를 호출하면 내부적으로는 ConcurrentBag.requite()
이 실행되어 connection
이 반납된다. 반납된 connection
은 HandOffQueue에 대기하는 Thread에게 제공되고 대기하는 Thread가 없다면 ConcurrentBag
로 관리된다.
워낙 구현이 복잡하게 되어 있어서, 전체 코드가 궁금한 사람은 GitHub를 참고하면 좋을 것 같고 여기서는 몇가지 눈에 띄는 사항들만 정리하고자 한다. https://github.com/brettwooldridge/HikariCP
public final class HikariPool extends PoolBase implements HikariPoolMXBean, IBagStateListener
{
private final Logger logger = LoggerFactory.getLogger(HikariPool.class);
public static final int POOL_NORMAL = 0;
public static final int POOL_SUSPENDED = 1;
public static final int POOL_SHUTDOWN = 2;
public volatile int poolState;
private final long aliveBypassWindowMs = Long.getLong("com.zaxxer.hikari.aliveBypassWindowMs", MILLISECONDS.toMillis(500));
private final long housekeepingPeriodMs = Long.getLong("com.zaxxer.hikari.housekeeping.periodMs", SECONDS.toMillis(30));
private static final String EVICTED_CONNECTION_MESSAGE = "(connection was evicted)";
private static final String DEAD_CONNECTION_MESSAGE = "(connection is dead)";
private final PoolEntryCreator poolEntryCreator = new PoolEntryCreator();
private final PoolEntryCreator postFillPoolEntryCreator = new PoolEntryCreator("After adding ");
// Thread 가 pool을 이용하는 주체이므로 ThreadPool을 사용
private final ThreadPoolExecutor addConnectionExecutor;
private final ThreadPoolExecutor closeConnectionExecutor;
// 내부적으로 ConcurrentBag 사용
private final ConcurrentBag<PoolEntry> connectionBag;
private final ProxyLeakTaskFactory leakTaskFactory;
private final SuspendResumeLock suspendResumeLock;
private final ScheduledExecutorService houseKeepingExecutorService;
private ScheduledFuture<?> houseKeeperTask;
/**
* Construct a HikariPool with the specified configuration.
*
* @param config a HikariConfig instance
*/
public HikariPool(final HikariConfig config)
{
super(config);
this.connectionBag = new ConcurrentBag<>(this);
this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK;
this.houseKeepingExecutorService = initializeHouseKeepingExecutorService();
checkFailFast();
if (config.getMetricsTrackerFactory() != null) {
setMetricsTrackerFactory(config.getMetricsTrackerFactory());
}
else {
setMetricRegistry(config.getMetricRegistry());
}
setHealthCheckRegistry(config.getHealthCheckRegistry());
handleMBeans(this, true);
ThreadFactory threadFactory = config.getThreadFactory();
final int maxPoolSize = config.getMaximumPoolSize();
// BlockingQueue를 사용하여 관리
LinkedBlockingQueue<Runnable> addConnectionQueue = new LinkedBlockingQueue<>(maxPoolSize);
this.addConnectionExecutor = createThreadPoolExecutor(addConnectionQueue, poolName + " connection adder", threadFactory, new CustomDiscardPolicy());
this.closeConnectionExecutor = createThreadPoolExecutor(maxPoolSize, poolName + " connection closer", threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
this.leakTaskFactory = new ProxyLeakTaskFactory(config.getLeakDetectionThreshold(), houseKeepingExecutorService);
this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);
if (Boolean.getBoolean("com.zaxxer.hikari.blockUntilFilled") && config.getInitializationFailTimeout() > 1) {
addConnectionExecutor.setMaximumPoolSize(Math.min(16, Runtime.getRuntime().availableProcessors()));
addConnectionExecutor.setCorePoolSize(Math.min(16, Runtime.getRuntime().availableProcessors()));
final long startTime = currentTime();
while (elapsedMillis(startTime) < config.getInitializationFailTimeout() && getTotalConnections() < config.getMinimumIdle()) {
quietlySleep(MILLISECONDS.toMillis(100));
}
addConnectionExecutor.setCorePoolSize(1);
addConnectionExecutor.setMaximumPoolSize(1);
}
}
/**
* Get a connection from the pool, or timeout after connectionTimeout milliseconds.
*
* @return a java.sql.Connection instance
* @throws SQLException thrown if a timeout occurs trying to obtain a connection
*/
public Connection getConnection() throws SQLException
{
return getConnection(connectionTimeout);
}
/**
* Get a connection from the pool, or timeout after the specified number of milliseconds.
*
* @param hardTimeout the maximum time to wait for a connection from the pool
* @return a java.sql.Connection instance
* @throws SQLException thrown if a timeout occurs trying to obtain a connection
*/
public Connection getConnection(final long hardTimeout) throws SQLException
{
suspendResumeLock.acquire();
final var startTime = currentTime();
// TimeOut 이 존재하고, 이를 넘어가면 오류를 발생
try {
var timeout = hardTimeout;
do {
var poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
if (poolEntry == null) {
break; // We timed out... break and throw exception
}
final var now = currentTime();
if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > aliveBypassWindowMs && isConnectionDead(poolEntry.connection))) {
closeConnection(poolEntry, poolEntry.isMarkedEvicted() ? EVICTED_CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE);
timeout = hardTimeout - elapsedMillis(startTime);
}
else {
metricsTracker.recordBorrowStats(poolEntry, startTime);
return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry));
}
} while (timeout > 0L);
metricsTracker.recordBorrowTimeoutStats(startTime);
throw createTimeoutException(startTime);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new SQLException(poolName + " - Interrupted during connection acquisition", e);
}
finally {
suspendResumeLock.release();
}
}
// Thread의 작업을 종료시키는 shutdown 메소드에서 close() 구현
public synchronized void shutdown() throws InterruptedException
{
try {
poolState = POOL_SHUTDOWN;
if (addConnectionExecutor == null) { // pool never started
return;
}
logPoolState("Before shutdown ");
if (houseKeeperTask != null) {
houseKeeperTask.cancel(false);
houseKeeperTask = null;
}
softEvictConnections();
addConnectionExecutor.shutdown();
if (!addConnectionExecutor.awaitTermination(getLoginTimeout(), SECONDS)) {
logger.warn("Timed-out waiting for add connection executor to shutdown");
}
destroyHouseKeepingExecutorService();
connectionBag.close();
final var assassinExecutor = createThreadPoolExecutor(config.getMaximumPoolSize(), poolName + " connection assassinator",
config.getThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
try {
final var start = currentTime();
do {
abortActiveConnections(assassinExecutor);
softEvictConnections();
} while (getTotalConnections() > 0 && elapsedMillis(start) < SECONDS.toMillis(10));
}
finally {
assassinExecutor.shutdown();
if (!assassinExecutor.awaitTermination(10L, SECONDS)) {
logger.warn("Timed-out waiting for connection assassin to shutdown");
}
}
shutdownNetworkTimeoutExecutor();
closeConnectionExecutor.shutdown();
if (!closeConnectionExecutor.awaitTermination(10L, SECONDS)) {
logger.warn("Timed-out waiting for close connection executor to shutdown");
}
}
finally {
logPoolState("After shutdown ");
handleMBeans(this, false);
metricsTracker.close();
}
}
}
속성명 | 설명 |
---|---|
autoCommit | pool에서 반환된 connection의 기본 자동 커밋 동작 제어 (default = true) |
connectionTimeout | 클라이언트가 pool로 부터 연결을 기다리는 최대 시간 (default = 30000 ms) |
idleTimeout | Connection Pool에서 쉬고 있는 커넥션을 유지하는 시간 (default = 60000 ms) |
keepaliveTime | 데이터베이스 또는 네트워크 인프라에 의해 시간 초과되는 것을 방지하기 위해 connection 유지를 위한 연결을 얼마나 자주 시도할 것인지 제어 (default = 0) |
maxLifetime | Connection Pool에서 살아 있을 수 있는 커넥션의 최대 수명 시간 (default = 1800000 ms) |
connectionTestQuery | 데이터베이스 연결이 활성 상태인지 확인하기 위해 pool 에서 연결이 제공되기 직전에 실행되는 쿼리 (default = none) |
minimumIdle | 커넥션이 일을 하지 않아도 해당 사이즈만큼은 커넥션을 유지하도록 |
maximumPoolSize | Connection Pool에 유지시킬 수 있는 최대 커넥션 수 (default = 10) |
pollName | Connection Pool의 사용자 정의 이름 (default = auto-generated) |
Thread 가 서로의 DB Connection 이 반납되기만을 무한정 대기하는 상황
DBCP 를 사용할 때는 교착상태(deadlock)
을 주의해야 한다. Pool Size를 충분하게 설정하지 못하는 경우 Deadlock
이 발생할 수 있다.
Repository.save()
의 insert query를 실행하기 위해 Transaction 시작 Connection
을 가져옴Repository.save()
를 위해서 Connection
이 더 필요하다면 ?
Connection
이 없으므로 handOffQueue에서 TimeOut 동안 기다림Connection
은 본인이 사용하고 있으므로, 본인이 반납하지 않는 이상 사용 가능한 Connection
이 없음hikari-pool-1 - Connection is not available, request timed out after 30000ms
에러 발생Rollback
Rollback
되며 Root Transaction 이 rollbackOnly=true
가 되며 Root Transaction 또한 Rollback
됨 Rollback
되었으므로 Root Transaction 의 Connection
은 다시 pool 에 반납됨 위와 같은 상태를 방지하기 위해서는 적절한 Pool Size를 지정해야 한다. HikariCP
공식 문서에서 제공하는 적절한 커넥션 풀 사이즈 공식은 아래와 같다.
connection=((core_count∗2)+effective_spindle_count)
core_count : cpu 의 코어 수 effective_spindle_count : DB 서버가 동시 관리할 수 있는 IO 개수
또한, deadlock
이 발생하지 않을 DBCP 최소 사이즈도 아래와 같이 제안하고 있다.
Tn ∗(Cm −1)+1
Tn : WAS 의 전체 thread 개수 Cm : thread가 작업을 수행하기 위해 동시에 필요한 connection의 개수
thread 최대 개수가 10개고, 동시에 필요한 connection
개수가 3개라고 가정하고 위의 공식을 적용해보자.
위의 공식에 대입하면 10 * (3-1) + 1 = 21 의 결과가 나온다.
만약, 10개의 thread가 2개씩 connection
을 할당받으면 1개의 connection
pool에 남게 된다.
따라서 최소 하나의 thread는 무조건 connection
을 3개 확보하고 작업을 처리한 뒤 connection
을 반납할 수 있게 되어 deadlock
이 발생하지 않는다.
위의 공식은 단순히 참고용일 뿐, 애플리케이션에 따라 추가적인 기준이 필요하다. 이때, connection
또한 객체이기 때문에 메모리 공간을 차지하므로 적절한 크기를 찾아 설정해야 한다.
Apache 에서 제공하는 대표적인 DBCP 프레임워크이다. 아래와 같이 4개의 속성이 대표적으로 존재한다.
속성명 | 설명 |
---|---|
initialSize | BasicDataSource 클래스 생성 후 최초로 getConnection() 메서드를 호출할 때 커넥션 풀에 채워 넣을 커넥션 개수 |
maxActive | 동시에 사용할 수 있는 최대 커넥션 개수 (default : 8) |
maxIdle | 커넥션 풀에 반납할 때 최대로 유지할 수 있는 커넥션 개수 (default : 8) |
minIdle | 최소한으로 유지할 커넥션 개수 (default : 0) |
가장 중요한 성능 요소는 maxActive
이다. 이 값은 DBMS의 설정과 애플리케이션 서버의 개수, Apache, Tomcat 에서 동시에 처리할 수 있는 사용자 수등을 고려해서 설정해야 한다. 또한, Commons DBCP
에서는 DBMS에 로그인을 시도하고 있는 커넥션도 사용 중인 것으로 간주한다. 따라서 로그인을 시도하고 있는 상태에서 무한으로 대기한다면 새로운 요청을 처리하지 못하게 된다. 이를 막기 위해 loginTimeOut
속성과 같은 JDBC 드라이버별 타임아웃 속성을 설정하는 것이 좋다.
tomcat 에 내장되어 사용되고 있는 프레임워크이다. Apache Commons DBCP를 바탕으로 만들어져 있으며, Spring 2.0 하위 버전의 기본 DBCP이다.
https://velog.io/@mooh2jj/%EC%BB%A4%EB%84%A5%EC%85%98-%ED%92%80Connection-pool%EC%9D%80-%EC%99%9C-%EC%93%B0%EB%8A%94%EA%B0%80 https://steady-coding.tistory.com/564 https://hudi.blog/dbcp-and-hikaricp/ https://data-make.tistory.com/764 https://techblog.woowahan.com/2664/
👍 문제
12장에서
DB Connection Pool
이라는 키워드가 나왔습니다. 저는 Spring Framework 를 사용할 때 자주본 키워드인데HikariCP
를 사용하는 것만 알고 있을 뿐 동작 방식에 대해서 자세히 알지 못하고 있습니다! 그래서DB Connection Pool
의 종류와 동작 방식에 대해서 설명해주시면 좋을 것 같습니다!✈️ 선정 배경
키워드와 간단한 설명만 등장하였는데 자세히 알아보면 좋을 것 같고
Spring Framework
에서도 자주 확인할 수 있는 키워드여서 한번 자세히 공부하면 좋을 것 같아서 선정하였습니다!📺 관련 챕터 및 레퍼런스
🐳 비고