본문 바로가기
데이터베이스

Thread Pool과 Connection Pool은 어떤 관계가 있고 적당한 크기는 얼마일까?

by 나무후추통 2022. 9. 15.

 대부분 DBMS는 TCP/IP로 통신한다. WAS가 I/O 요청을 할 때마다 새로운 커넥션을 연다면 오버헤드가 발생하게 될 것이다. 이러한 오버헤드를 방지하기 위해서 DBMS는 미리 커넥션을 생성하는데 이것을 커넥션 풀이라고 한다. 풀 사이즈가 작다면 요청이 들어와도 대기하는 시간이 발생할 것이고 너무 많으면 한정된 컴퓨팅 자원을 낭비하게 될 것이다. 그렇다면 적절한 커넥션 수는 얼마일까?

 

더보기

 상용 DBMS는 커넥션 풀 기능을 내장있다. 오픈 소스로는 Apache dbcp, HikariCP 등이 있다. PostgreSQL에서는 DBMS에서 커넥션 풀을 관리하는 것보다 외부 머신에서 관리하는 것을 권장한다.

1. 스레드 풀과 커넥션 풀

그림 1. 커넥션 비용, (출처 : MySQL 8.0 공식문서)

 TCP는 연결을 생성할 때 3-way handshake를 시도한다. DB에 접근할 때마다 이러한 요청을 생성하는 것은 매우 비효율적이다. MySQL 공식 문서에서 Insert 문을 실행하는 예시를 통해서 필요한 비용을 나타낸 것이 있는데, 여기서 연결을 생성하는 비용이 3이나 필요하다.

 

 이 문제는 어떻게 해결할 수 있을까? 비슷한 사례로 스레드 풀이 있다. 스레드를 생성하는 비용은 비싸다. 요청된 작업마다 새로운 스레드를 생성한다면...

 

그림 2. 시스템 콜 간략한 그림

(1) 운영체제 시스템 콜을 호출한다.

(2) Application thread에서 Kernel Thread로 컨텍스트 스위칭이 발생한다.

(3) 운영체제가 스레드를 생성하고 Application Thread에게 돌려준다.

(4) new Thread와 운영체제가 만든 스레드가 1:1 맵핑이 되고 작업이 할당되어 처리된다.

(5) 작업이 끝나면 스레드의 상태가 terminated로 된다.

(6) 새로운 요청이 올 때마다 위 과정을 계속 반복.

 

 처리할 요청이 증가하면 새로운 스레드 생성도 같이 증가하게 된다. 이는 빈번한 컨텍스트 스위칭과 메모리 사용을 증가시킨다. 결국 전체 애플리케이션 성능 저하가 발생할 수 있다.

 

그림 3. 자바 스레드 풀, (출처 :https://stackoverflow.com/questions/65189805/java-thread-pool-task-execution-queueing)

 위와 같은 문제를 방지하기 위해서 정해진 개수의 스레드만 만들고, 몰려오는 요청은 큐에 넣고 대기시킨다. 생성된 스레드가 큐에 있는 작업을 가져가서 처리한다.

 

(1) 요청은 BlockingQueue에 등록이 된다.

(2) 등록된 작업을 처리할 때, thread가 없다면 스레드를 생성하고 작업을 할당한다. (위에 있는 kernel 스레드, 1:1 맵핑은 생략)

(2-1) 이 때, 전체 스레드 개수 n보다 더 만들지 않는다.

(2-2) 만약 스레드에서 예외가 발생할 경우 새로운 스레드를 만들고 해당 스레드는 퇴출된다.

(3) 각 생성된 스레드들이 loop를 돌면서 queue에 등록된 작업을 처리한다.

 

그림 4. jdk.internal.net.http.ConnectionPool 소스 코드 내부

 커넥션 풀도 동일하다. 미리 만들어 둔 커넥션 풀에서 찾아와 작업을 처리한다.

 

2. Thread와 Connection pool은 어떤 관계가 있는가?

 어느 하나의 요청이 DB I/O 작업을 보낸다고 가정하자. 그렇다면 아래와 같은 일이 발생한다고 유추할 수 있다.

 

(1) 요청을 담당하는 스레드가 할당된다.

(1-1) 모든 스레드가 일을 하고 있다면 요청은 queue에서 대기한다.

(2) 할당된 스레드는 커넥션 풀에서 커넥션을 가져와 쿼리를 보낸다.

(2-1) 만약 모든 커넥션이 고갈되었다면 queue에서 대기한다.

(3) 할당된 스레드가 작업을 처리하고 커넥션을 반환한다.

(4) 스레드는 다른 작업을 처리하거나 풀에 들어가서 다른 요청을 가져와 처리한다.

 

 웹 애플리케이션일 경우 단일 서버가 최대한 많은 요청을 처리하는 것이 좋을 것이다. 그렇다면 '많은 요청을 처리할 수 있도록 thread pool 크기를 늘리는 것이 좋을까?'

 

 스레드 풀은 많은 요청이 몰려 올 때, 빈번한 컨텍스트 스위칭을 줄이기 위한 방법이다. 근데 '더 많은 요청을 처리하기' 위해서 '더 큰 스레드 풀 크기'라는 해결책은 결국 스레드 풀 사용 목적을 무시하는 것도 다름이 없다.

 

 그렇다면 '커넥션 풀을 얻기 위한 대기 시간을 줄이기' 위해서 '커넥션 풀 크기를 늘리면 좋을까?'

 

 이것 또한 문제가 있다. 일단 어느 정도의 요청이 DB I/O를 필요할 지 예상하기 어렵다. 또한 커넥션 풀도 결국 메모리에 할당된다. 항상 쓰이지 않는 커넥션 개수가 많을수록 한정된 컴퓨팅 자원을 낭비한다고 할 수 있다.

 

3. 그렇다면 적절한 수는 얼마인가?

 허무하게도 여기부터는 답이 없다고 한다. 테스트를 통해서 적절한 수를 찾는 수 밖에 없다.

3-1. 알려진 적절한 커넥션 풀 수 - 1

 PostgreSQL에서 권장하는 커넥션 풀 크기는 ((core_count * 2) + effective_spindle_count)이다. 왜냐하면 DB 안에서도 I/O가 발생한다면 커넥션에 할당된 스레드(DB 내부에 있는 스레드)가 놀 수 있기 때문에 그동안 다른 일을 처리하도록 (core 수 * 2)를 권장하는 것으로 보인다.

 

그림 5. 하드 디스크 사진, (출처 : http://data-recovery-tips.co.uk/hard-disk-work/)

 

 또한 effective_spindle_count는 하드 디스크에 있는 모터를 뜻한다. 하드 디스크는 회전하며 헤드가 블록을 인식하는데, (1개의 모터) = (처리할 수 있는 IO)라고 할 수 있다. 모터가 여러 개 혹은 여러 디스크로 조합이 된다면 그만큼의 IO를 더 처리할 수 있어서 추가된 변수라고 생각된다. 하지만 RAID, SSD에 대한 고려가 없는 것을 보아 참고만 해야 할 것 같다.

3-2. 알려진 적절한 커넥션 풀 수 - 2

 만약 하나의 요청 스레드가 여러 개의 커넥션을 사용하면 어떻게 될까? 이와 같은 경우에는 데드락이 발생할 수 가 있다. 즉, 작업 중인 스레드들이 1개 이상의 커넥션을 얻어서 작업을 해야 하는데 커넥션은 고갈이 되었고, 스레드는 새로운 커넥션을 바라는 상태이면서 현재 가지고 있는 커넥션은 놓질 않는 상태이다. 이 사례는 실제로 발생하여 '기술 블로그'에 자세히 정리가 되어 있다. HikariCP에서도 해당 이슈가 논의되었다. 위 문제 해결 방법을 수식화 하면 다음과 같다.

 

pool size = Tn * (Cm — 1) + 1 

Tn : 최대 스레드 수

Cm : 하나의 스레드에서 동시에 발생할 수 있는 커넥션 수

 

 위 공식을 간단하게 설명하자면 데드락이 발생할 커넥션 사이즈에 1개를 더 주는 것이다. 커넥션을 얻어 작업만 끝낸다면, 결국 가지고 있던 커넥션이 전부 반환될 것이다. 그렇다면 자연스럽게 데드락이 발생하지 않을 것이다. 하지만 '기술 블로그'에서는 위 공식을 최소한의 크기로 판단하고 더 많은 커넥션을 주도록 수정하였다.

 

4. 무엇을 기준으로 테스트를 진행해야 할까?

 이제 적절한 커넥션 풀과 스레드 풀을 테스트로 찾아야 한다는 것도 알게 되었다. 그렇다면 이제 테스트를 시작하고자 할 때, 무엇을 기준으로 테스트를 진행해야 할까? 

4-1. TPS

 TPS는 1초당 처리할 수 있는 트랜잭션을 뜻한다. 트랜잭션은 여러 개의 쿼리로 이루어져 있다. 즉, (하나의 요청) = (1개 이상의 쿼리)라고 할 수 있다. 만약 (총 쿼리 실행 시간) > (설정된 timeout 시간) 경우 요청이 실패되고 롤백될 것이다. 이 때는 쿼리를 개선하는 방법도 있을 것이고 커넥션 풀의 timeout 시간을 바꾸는 방법도 있을 것이다.

 

 또한 커넥션 풀 크기가 5인데 10개의 요청이 들어온다면 5개는 대기할 것이다. 하지만 (최대 대기 시간) < (요청 쿼리가 끝날 시간) 경우 timeout이 발생해서 예외가 발생할 것이다. 이 때는 커넥션 풀 크기를 늘리거나 대기 시간을 변경하는 방법이 있을 것이다.

 

 TPS는 애플리케이션 성능을 판단할 때 기준이 된다. 그리고 이 기준으로 적절한 성능의 서버를 선택해 비용 절감이 가능하다. TPS에 영향을 주는 요소는 다양하지만 커넥션 풀 사이즈로도 개선을 이룰 수가 있다.

 

참고 자료

https://d2.naver.com/helloworld/5102792

https://dev.mysql.com/doc/refman/8.0/en/insert-optimization.html

https://wiki.postgresql.org/wiki/Number_Of_Database_Connections

https://blog.christopherschultz.net/2009/03/16/properly-handling-pooled-jdbc-connections/

https://techblog.woowahan.com/2663/

https://kwahome.medium.com/database-connections-less-is-more-86c406b6fad

https://github.com/brettwooldridge/HikariCP/issues/442#issuecomment-146096704