최근, 사이트로부터 오라클DB의 readTimeout이 적용되지않는다는 이슈를 받고 DataSource 생성소스를 분석했다.
결과적으로 패치 미적용으로 인한 해프닝으로 일단락되었지만, DataSource를 생성할때 ip, port, sid같은 기본적인 속성들 이외에 maxWaitMillis, timeBetweenEvictionRunsMillis 같은 속성들은 어떤 의미를 지니는지 궁금하여 리서치하다가 좋은 글을 발견하여 이번기회에 정리해놓기로 했다.
(참고로 readTimeout은 dbcp 라이브러리 고유설정이 아니며, JDBC 드라이버설정이다)
DBCP란?
데이터베이스를 연결할때는 기본적으로 효율을 위하여 커넥션 풀을 사용한다. 커넥션을 한번 맺고 끊는건 꽤나 많은 비용이 소모되기에 매순간마다 커넥션을 맺고 끊기보다는 미리 다수의 커넥션을 맺어놓은 풀안에서 재사용하기쉽게 자원을 사용하고 반납하는것이다. 커넥션풀 오픈소스 라이브러리로는 Commons DBCP, Tomcat-JDBC, BoneCp, Oracle UCP, Hikari CP 등이 있다. 이중에 가장 많이 사용되는것은 Hikari CP지만, 본 포스팅에서는 Commons DBCP를 알아본다.
Commons DBCP 버전 | JDK 버전 | JDBC 버전 |
Commons DBCP 2 | JDK 7 | JDBC 4.1 |
Commons DBCP 1.4 | JDK 6 | JDBC 4 |
Commons DBCP 1.3 | JDK 1.4~1.5 | JDBC 3 |
DBCP설정은 BasicDataSource 클래스로 설정가능하며, Spring을 사용한다면 bean설정도 가능하다.
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close"
p:driverClassName="${db.driverClassName }"
p:url="${db.url}"
p:username="${db.username}"
p:password="${db.password}"
p:maxTotal="${db.maxTotal}"
p:maxIdle="${db.maxIdle}"
p:maxWaitMillis="${db.maxWaitMills}""
/>
DBCP2로 넘어가면서 바뀐속성은 변경해주어야한다.
Commons DBCP 1.x | Commons DBCP 2.x |
maxActive | maxTotal |
maxWait | maxWaitMills |
removeAbandoned | 다음과 같이 세분화됐다.
|
커넥션의 풀의 저장구조
커넥션풀이 어떤 구조로 저장되는지 알면 커넥션개수를 이해하기 편하다. 커넥션생성은 Commons DBCP에서 만들어지고 commons-pool의 addObejct() 메서드로 커넥션 풀에 추가된다. 이때 커넥션풀은 내부적으로 현재시간을 담고있는 타임스템프와 커넥션 레퍼런스를 한쌍으로하는 ObjectTimestampPair라는 자료구조를 생성한다. 이 타임스탬프를 활용하여 커넥션 속성에 맞게 커넥션수를 조절한다.
커넥션 개수 관련 속성
속성 | 설명 |
initialSize | BasicDataSource 클래스 생성 후 최초로 getConnection() 메서드를 호출할 때 커넥션 풀에 채워 넣을 커넥션 개수 |
maxActive | 동시에 사용할 수 있는 최대 커넥션 개수(기본값: 8) |
maxIdle | 커넥션 풀에 반납할 때 최대로 유지될 수 있는 커넥션 개수(기본값: 8), 초과시 반납시켜버림 |
minIdle | 최소한으로 유지할 커넥션 개수(기본값: 0) |
위 그림은 최대 동시 8개의 커넥션을 사용할수있고 4개는 사용중이고, 4개는 대기중인 커넥션 풀의 상태이다.
커넥션 개수와 관련된 속성들은 다음 조건을 만족시켜야한다.
- maxActive >= initialSize
maxActive = 10이고 initialSize = 20이라고 가정하면 최초에 커넥션을 생성할 때 initialSize 값이 최대 커넥션 개수인 maxActive 값보다 커서 논리적으로 오류 - maxIdle >= minIdle
maxIdle < minIdle면, 최솟값이 최댓값보다 커서 논리적으로 오류 - maxActive = maxIdle
maxActive 값과 maxIdle 값이 같은 것이 바람직하다. maxActive = 10이고 maxIdle = 5일 시, 항상 커넥션을 동시에 5개는 사용하고 있는 상황에서 1개의 커넥션이 추가로 요청된다면 maxActive = 10이므로 1개의 추가 커넥션을 데이터베이스에 연결한 후 풀은 비즈니스 로직으로 커넥션을 전달한다. 이후 비즈니스 로직이 커넥션을 사용 후 풀에 반납할 경우, maxIdle=5에 영향을 받아 커넥션을 실제로 닫아버리므로, 일부 커넥션을 매번 생성했다 닫는 비용이 발생할 수 있다.
커넥션 대기시간 관련 속성
속성 | 설명 |
maxWait | 커넥션 풀 안의 커넥션이 고갈됐을 때 커넥션 반납을 대기하는 시간(밀리초) (기본값 : 무한정) |
최대 커넥션 수가 5개인 커넥션풀에 10개의 요청이 들어왔다면, 5개의 커넥션은 각각 요청을 처리하고 커넥션을 할당받지못한 5개의 커넥션은 커넥션이 반납될때 까지 기다리게된다. 이때 얼마의 시간을 기다릴지가 maxWait다. 단순히 커넥션 대기를 해결하기위해서는 maxActive값을 증가시키면되지만, 무조건 커넥션 개수를 크게 설정하는것은 자원을 낭비하는 일이므로 실제 부하에따라 최적값을 설정하는것이 중요하다.
유효성 검사 쿼리 설정
속성 | 설명 |
testOnBorrow | 커넥션 풀에서 커넥션을 얻어올 때 테스트 실행(기본값: true) |
testOnReturn | 커넥션 풀로 커넥션을 반환할 때 테스트 실행(기본값: false) |
testWhileIdle | Evictor 스레드가 실행될 때 (timeBetweenEvictionRunMillis > 0) 커넥션 풀 안에 있는 유휴 상태의 커넥션을 대상으로 테스트 실행(기본값: false) |
JDBC 커넥션의 유효성은 validationQuery 옵션에 설정된 쿼리를 실행해 확인할 수 있다. Commons DBCP 1.x에서는 위 세 가지 테스트 옵션으로 유효성을 검사한다. 유효성을 검사할 때는 validationQuery 옵션에 하나 이상의 결과를 반환하는 쿼리를 설정해야 한다. Commons DBCP 2.x에서는 validationQuery 옵션이 없을 때 Connection.isValid() 메서드를 호출해 유효성을 검사한다.
validationQuery 옵션에는 DBMS에 따라 다음과 같이 쿼리를 설정 권장. 실제 데이터를 조회하면 안된다.
- Oracle: select 1 from dual
- Microsoft SQL Server: select 1
- MySQL: select 1
- CUBRID: select 1 from db_root
검증에 지나치게 자원을 소모하지 않게 testOnBorrow 옵션과 testOnReturn 옵션은 false로 설정하고, 오랫동안 대기 상태였던 커넥션이 끊어지는 현상을 막게 testWhileIdle 옵션은 true로 설정하는 것을 추천한다.
커넥션이 장애가 있으면 해당 커넥션을 커넥션 풀에서 제외시켜버리기때문에 유효성 검사 쿼리를 실행하는것만으로도 예상치못한 오류상황에 대비할 수 있다.
Evictor 스레드 설정
속성 | 설명 |
timeBetweenEvictionRunsMillis | Evictor 스레드가 동작하는 간격. 기본값은 -1이며 Evictor 스레드의 실행이 비활성화돼 있다. |
numTestsPerEvictionRun | Evictor 스레드 동작 시 한 번에 검사할 커넥션의 개수 |
minEvictableIdleTimeMillis | Evictor 스레드 동작 시 커넥션의 유휴 시간을 확인해 설정 값 이상일 경우 커넥션을 제거한다(기본값: 30분) |
Evictor 스레드는 Commons DBCP 내부에서 커넥션 자원을 정리하는 구성 요소이며 별도의 스레드로 실행된다.
Evictor 스레드의 역할은 크게 3가지로 나눌 수 있다.
- 커넥션 풀 내의 유휴 상태의 커넥션 중에서 오랫동안 사용되지않은 커넥션을 추출해 해제한다. Evictor 스레드 실행 시 설정된 numTestsPerEvictionRun 속성값만큼 CursorableLinkedList의 ObjectTimestampPair를 확인한다. ObjectTimestampPair의 타임스탬프 값과 현재 시간의 타임스탬프 값의 차이가 minEvictableIdleTimeMillis 속성값을 초과하면 해당 커넥션을 제거한다. 커넥션 숫자를 적극적으로 줄여야 하는 상황이 아니라면 minEvictableIdleTimeMillis 속성값을 -1로 설정해서 해당 기능을 사용하지 않기를 권장한다.
- 커넥션에 대해서 추가로 유효성 검사를 수행해 문제가 있을 경우 해당 커넥션을 제거한다. testWhileIdle 옵션이 true로 설정됐을 때만 이 동작을 수행한다. 첫 번째 작업 시 minEvictableIdleTimeMillis 속성값을 초과하지 않은 커넥션에 대해서 추가로 유효성 검사를 수행하는 것이다
- 앞의 두 작업 이후 남아 있는 커넥션의 개수가 minIdle 속성값보다 작으면 minIdle 속성값만큼 커넥션을 생성해 유지한다.
예를 들어, testWhileIdle=true && timeBetweenEvictionRunMillis > 0이면 위의 3가지 역할을 다 수행하고고, testWhileIdle=false && timeBetweenEvictionRunMillis > 0이면 두 번째 동작은 수행하지 않는다.
Evictor 스레드는 동작 시에 커넥션 풀에 잠금(lock)을 걸고 동작하기 때문에 너무 자주 실행하면 서비스 실행에 부담을 줄 수 있다. 또한 numTestsPerEvictionRun 값을 크게 설정하면 Evictor 스레드가 검사해야 하는 커넥션 개수가 많아져 잠금 상태에 있는 시간이 길어지므로 역시 서비스 실행에 부담을 줄 수 있으므로 가장 효율적인 값으로 잘 설정해야한다.
IDC(internet data center) 정책에 따라서는 서버 간의 소켓 연결 후 정해진 시간 이상 아무런 패킷도 주고받지 않으면 연결을 종료한다. 이런 경우 timeBetweenEvictionRunsMillis 속성 등으로 의도하지 않게 연결이 끊어지는 것을 방어할 수 있다. 예를 들어 30분 동안 통신이 없을 때 연결이 끊어지는 정책으로 네트워크를 운영한다면, BasicDataSource가 풀링(pooling)하는 커넥션의 수가 30개라고 가정할 때 30분 안에 모든 커넥션에 유효성 검사 쿼리를 한 번씩은 실행하는 것이 바람직하다. Evictor 스레드가 5분에 한 번씩 실행되도록 설정했을 때 30분 동안 Evictor 스레드 실행 횟수는 6번이므로 매번 5개의 커넥션을 검사해야 전체 커넥션을 테스트할 수 있다. 30분 안에 5분마다 Evctor 스레드가 실행되면 6번 실행되지만 오차를 감안해 5번으로 가정하면 이때 설정해야 할 numTestsPerEvictionRun 값은 다음과 같이 구할 수 있다.
6 * numTestsPerEvictionRun > 30개
따라서 numTestsPerEvictionRun 속성값은 최소 6 이상이어야 한다.
statement pooling 관련 옵션
속성 | 설명 |
poolPreparedStatements |
statement pooling 기능을 사용할지 여부(기본값: false) |
maxOpenPreparedStatements | 1개의 커넥션당 미리 저장해놓을 풀 개수 |
statement pooling은 RDBMS 실행에 있어 비용이 많이 드는작업으므로 매번 RDBMS에 얻어오기 보다는, 자주 실행되는 Statement를 사전에 prepare하여 풀에 저장해놓고 가져오는 방식을 말한다.
statement pooling은 JDBC 3.0에 정의된 명세지만, 하위 버전도 Commons DBCP를 사용하고있다면 poolPreparedStatements옵션을 true로 하여 커넥션 풀 뿐만아니라 statement pool로도 사용가능하다. 해당 옵션은 50정도로 작게 설정해야하며, 너무 많은 옵션설정히 OOM발생하니 주의.
예를들어, 커넥션풀에 10개 커넥션이 있을때 maxOpenPreparedStatements 값이 50이라면 총 500개의 PreparedStatement가 캐시에 저장된다.
기본값을 그대로 쓰기 권장하는 옵션
속성 | 설명 |
removeAbandoned | 오랫동안 열려만 있고 Connection.close() 메서드가 호출되지 않는 커넥션을 임의로 닫는 기능(기본값: false) |
removeAbandonedTimeout | removeAbandoned 가 true일시, close() 메서드가 동작하는 간격 |
defaultAutoCommit | 자동 커밋 (기본값 : true) |
removeAbandoned 사용시 실행시간이 긴 쿼리의 커넥션을 닫아버리는 버그 발생. false로 사용하고, 오래 걸리는 쿼리는 JDBC Statement의 쿼리타임아웃 등의 다른 속성으로 제어하는 편이 바람직하다.
defaultAutoCommit를 false로 지정하면 커넥션을 커넥션 풀에서 꺼낼때 바로 setAutocommit(false) 메서드를 호출해서 트랜잭션을 시작한다. 트랜잭션 처리가 되어있지않은경우 쿼리가 제대로 반영되지않으므로 변경하지 않는것이 좋다.
DataSource 생성 시, 늘 궁금했지만 사용하지않아도 돼서 애써 모른척했던 옵션들에 관해 정리해보았다.
Commons dbcp의 동작방식도 알아보았으니 이번기회를 통해 잘 숙지해놓아야겠다.
참고
'IT > DB' 카테고리의 다른 글
오라클 계정내 모든 테이블 삭제하기 (0) | 2024.07.15 |
---|---|
ORACLE 정리 (0) | 2023.03.27 |
mysql 정리 (0) | 2023.03.27 |
mysql 도커 실행 (0) | 2022.01.24 |
mybatis $ # 차이 (0) | 2022.01.24 |
댓글