커넥션 풀(DBCP)란?
- 웹 컨테이너(WAS)가 실행되면서 DB와 미리 connection(연결)을 해놓은 객체들을 pool에 저장해두었다가, 클라이언트 요청이 오면 connection을 빌려주고, 처리가 끝나면 다시 connection을 반납받아 pool에 저장하는 방식
JDBC : Java Data Base Connectivity. 한글로 번역하면, 자바 데이터 베이스 연결, 정의는 DataBase와 연결하기 위한 Java Interface이다.
DBCP : DataBase Connection Pool. 역시 간단하게 정리하면 DataBase와 Connection을 맺고 있는 객체를 관리하기 위한 Connection Pool
- DBCP 직접 사용 VS JNDI : https://itstein.tistory.com/2
Connection Pool 사용 이유
- 자바에서 db에 직접 연결해서 처리하는 경우(JDBC) 드라이버를 로드하고 커넥션 객체를 받아와야 한다.
- 그러면 매번 사용자가 요청할 때마다 드라이버를 로드하고 커넥션 객체를 생성하여 연결하고 종료하기 때문에 매우 비효율적이다.
- 이러한 문제를 해결하기 위해 커넥션풀을 사용한다.
커넥션 풀 특징
- 웹 컨테이너(WAS)가 실행되면서 connection 객체를 미리 pool에 생성해 둡니다.
- HTTP 요청에 따라 pool에서 connection객체를 가져다 쓰고 반환한다.
- 이와 같은 방식으로 물리적인 데이터베이스 connection(연결) 부하를 줄이고 연결 관리 한다.
- pool에 미리 connection이 생성되어 있기 때문에 connection을 생성하는 데 드는 요정 마다 연결 시간이 소비되지 않는다.
- 커넥션을 계속해서 재사용하기 때문에 생성되는 커넥션 수를 제한적으로 설정함
DB Connection 구조
- 2Tier - 클라이언트로서의 자바 프로그램(JSP)이 직접 데이터베이스 서버로 접근하여 데이터를 액세스하는 구조
- 3Tier - 자바 프로그램과 데이터베이스 서버 중간에 미들웨어 층을 두어, 그 미들웨어 층에게 비즈니스 로직 구현부터 트랜잭션 처리, 리소스 관리 등을 전부 맡기는 구조이다.
JDBC(Java Database Connectivity)
- 자바 언어로 다양한 종류의 관계형 데이터베이스에 접속하고 SQL문을 수행하여 처리하고자 할 때 사용되는 표준 SQl 인터페이스 API
- 원래라면 DB마다 연결 방식과 통신 규격이 따로 있기 때문에 프로그램을 DB와 연결한다면, 해당 DB와 관련된 기술적 내용을 배우고 DB가 변경될 시 많은 변경 사항이 존재한다.
- 각 DBMS에 맞는 JDBC를 받아주게 되면 쉽게 DBMS를 변경할 수 있게 된다.
- 즉, DBMS 종류(MySQL, MsSQL, Oracle 등)에 상관 없이 하나의 JDBC API를 사용해서 데이터베이스 작업을 처리할 수 있게 된다. JDBC API를 사용하는 애플리케이션의 개략적인 구조는 다음과 같다.
• 자바 애플리케이션에서 데이터베이스에 접근하기 위해서는 JDBC API를 이용해서 데이터베이스에 접근하고, JDBC API는 JDBC 드라이버를 거쳐 데이터베이스와 통신을 한다.
• 각각의 DBMS는 자신에게 알맞은 JDBC 드라이버를 제공하고 있다.
동시 접속자가 많을 경우
- 동시 접속 할 경우 POOL에서 미리 생성된 connection을 제공하고 없을 경우는 사용자는 connection이 반환될 때까지 번호순대로 대기상태로 기다린다.
- 여기서 WAS에서 커넥션풀을 크게 설정하면 메모리 소모가 큰 대신 많은 사용자가 대기시간이 줄어들고, 반대로 커넥션 풀을 적게 설정하면 그 만큼 대기시간이 길어진다.
커넥션풀 종류
SpringBoot 2.0 이전에는 tomcat-jdbc 를 사용하다가 2.0이후 부터는 HikariCP 를 기본옵션으로 채택 하고있다.
commons-dbcp
- Apache Commons
- 아파치에서 제공해주는 대표적 Connecrion Pool 라이브러리
tomcat-jdbc-pool
- tomcat
- tomcat에 내장되어 사용되고 있다
- Apache Commons DBCP 라이브러리를 바탕으로 만들어짐
HikariCP
maxLifeTime(커넥션이 커넥션 풀에 최대로 머무를 수 있는 시간)의 값은 mysql의 wait_timeout 보다 몇초정도 짧게 설정한다. (뒤에서 자세히 설명)
- maxLifeTime을 mysql의 wait_timeout보다 짧게 설정한다면, mysql이 회수하기전에 커넥션 풀이 커넥션을 새롭게 맺으므로 별다른 이슈가 없다.
- maxLifeTime의 기본값은 30분, mysql의 wait_timeout 기본값은 8시간이다.
- maxLifeTime으로 커넥션이 끊기고 새롭게 커넥션을 맺는 대상은 active하지 않은 커넥션들 대상이다. (따라서 계속 active 상태의 커넥션이라면 대상이 되지 않는다.)
- maxLifeTime에 모든 커넥션들이 내려가고 새롭게 맺지 않는다. 순차적으로 이뤄진다.
- connection pool size를 thread 개수보다 넉넉히 가져가준다. (Hikari CP 데드락 이슈)
- minimumIdle의 기본 값은 maximumPoolSize이므로, idleTimeout을 설정해주지 않는 이상 따로 손보지 않아도 된다.
- leakDetectionThreashold는 커넥션이 설정 시간보다 길게 잡고 있다면 누수로 판단하고 WARN 로그를 출력한다.
maximum-pool-size: 최대 pool size (defailt 10)
connection-timeout: (말 그대로)
connection-init-sql: SELECT 1
validation-timeout: 2000
minimum-idle: 연결 풀에서 HikariCP가 유지 관리하는 최소 유휴 연결 수
idle-timeout: 연결을위한 최대 유휴 시간
max-lifetime: 닫힌 후 pool 에있는 connection의 최대 수명 (ms)입니다.
auto-commit: auto commit 여부 (default true)
커넥션 풀의 크기와 성능
- Hikari CP의 공식 문서에 의하면, 1 connections = ((core_count) * 2) + effective_spindle_count) 로 정의하고 있다.
- core_count는 현재 사용하는 서버 환경에서의 CPU 개수를 의미한다.
- core_count * 2 를 하는 이유는 Context Switching 및 Disk I/O와 관련이 있다.
- Context Switching으로 인한 오버헤드를 고려하더라도 데이터베이스에서 Disk I/O(혹은 DRAM이 처리하는 속도)보다 CPU 속도가 월등히 빠르다.
- 그러므로, Thread가 Disk와 같은 작업에서 블로킹되는 시간에 다른 Thread의 작업을 처리할 수 있는 여유가 생기고, 여유 정도에 따라 멀티 스레드 작업을 수행할 수 있게 된다.
- Hikari CP가 제시한 공식에서는 계수를 2로 선정하여 Thread 개수를 지정하였다.
- 즉 코어수에 가까울수록 = 더 적은 수의 스레드가 더 많은 스레드보다 더 나은성능을 발휘
- blocking이 있을경우에만 더 많은 스레드가 유리함
- core_count * 2 를 하는 이유는 Context Switching 및 Disk I/O와 관련이 있다.
- effective_spindle_count는 기본적으로 DB 서버가 관리할 수 있는 동시 I/O 요청 수이다.
- 하드 디스크 하나는 spindle 하나를 갖는다.
- 디스크가 16개 있는 경우, 시스템은 동시에 16개의 I/O 요청을 처리할 수 있다.
Connection을 사용하는 주체인 Thread의 개수보다 커넥션 풀의 크기가 크다면 사용되지 않고 남는 커넥션이 생겨 메모리의 낭비가 발생하게 된다.MySQL의공식레퍼런스에서는 600여 명의 유저를 대응하는데 15~20개의 커넥션 풀만으로도 충분하다고 언급하고 있다. MySQL은 최대 연결 수를 무제한으로 설정한 뒤 부하 테스트를 진행하면서 최적화된 값을 찾는 것을 추천한다.우아한 형제들 테크 블로그에서는 다음과 같은 공식을 추천하고 있다.
- Tn = 전체 Thread의 개수
- Cm = 하나의 Task에서 동시에 필요한 Connection 수(단일 스레드가 보유하는 최대 동시 연결 수)
- -1 : 마지막 connection이 필요한 sub Transaction에 대해
- +1 : connection 1개가 마지막 sub transaction을 해결할 수 있게 해준다.
PoolSize = Tn × ( Cm - 1 ) + ( Tn / 2 ) ← 기본 basic을 배민에서 확장한 방식, basic을 그대로 따라가기 보다는 배민과 같이 실제 서비스에서 +알파로 성능테스트를 통해 최적의 값을 찾아야한다.
- 여유있게 커넥션풀을 설정하여 성능 높임 & 교착상태방지
- thread count : 16
- simultaneous connection count : 2
- pool size : 16 * ( 2 – 1 ) + (16 / 2) = 24
- Dead lock을 피할수 있는 pool 갯수 + a
- 장기 실행 트랜잭션과 매우 짧은 트랜잭션이 혼합된 시스템은 일반적으로 모든 연결 풀로 조정하기 가장 어렵다. 이러한 경우 두 개의 풀 인스턴스를 만드는 것이 잘 작동할 수 있다(예: 하나는 장기 실행 작업용, 다른 하나는 "실시간" 쿼리용).
- 주로 장기 실행 트랜잭션이 있는 시스템에는 한 번에 특정 수의 작업만 실행하도록 허용하는 작업 실행 대기열과 같이 필요한 연결 수에 대한 "외부" 제약 조건이 있는 경우가 많다. 이러한 경우 작업 대기열 크기는 풀과 일치하도록 "적절한 크기"여야 한다(반대보다는).
- 10개의 연결 풀 만으로도 6000TPS로 간단한 쿼리를 실행하는 3000명의 프런트 엔드 사용자를 쉽게 처리할 수 있다.
- 부하 테스트를 실행하면 연결 풀을 줄일수록 TPS 속도가 떨어지기 시작하고 프런트 엔드 응답 시간이 증가하기 시작하는 것을 볼 수 있다.
dead lock 확인
- HikariCP의 Maximum Pool Size을 1로 설정한 다음 1건씩 Query를 실행해 본다. 만약 정상적으로 실행되지 않고, connection timeout과 같은 에러가 발생한다면 Dead lock 발생 가능성이 있는 코드입니다.
- Nested Transaction을 사용하지 않는다!
- why? → 보이지 않는 dead lock을 유발할 수 있습니다.