-
Notifications
You must be signed in to change notification settings - Fork 3
HikariCP 설정 최적화
커넥션 풀은 데이터베이스와 연결된 커넥션을 미리 만들어 놓고 이를 pool로 관리하는 것입니다. 즉, 필요할 때마다 풀의 커넥션을 꺼내 사용하고 반환하는 기법입니다.
이처럼 미리 만들어 놓은 커넥션을 이용하면 Connection에 필요한 비용을 줄일 수 있기 때문에 DB에 빠르게 접근할 수 있습니다. 또한 커넥션 풀을 사용하면 커넥션 수를 제한할 수도 있어서 과도한 접속으로 인한 서버 자원 고갈을 방지할 수 있습니다.
SpringBoot를 사용해본 사람이라면 한 번쯤은 이름을 들어보셨을텐데요. HikariCP는 가벼운 용량과 빠른 속도를 가지는 JDBC 커넥션 풀 프레임워크입니다. SpringBoot는 커넥션 풀 관리를 위해 기본적으로 HikariCP를 사용합니다.
커넥션 풀은 위의 사진처럼 동작합니다. 만약 커넥션 풀의 크기가 너무 작다면 커넥션을 획득하기 위해 대기하고 있는 스레드가 많아지게 될겁니다. 이런 문제는 커넥션 풀의 크기를 늘려주면 해결할 수 있습니다. 그렇다면 커넥션 풀의 크기는 어느정도인게 좋을까요?
언뜻 생각해보면 커넥션을 많이 가지고 있을수록 성능이 좋아질 것 같습니다. 진짜로 커넥션 풀이 크면 클수록 성능이 좋아질까요? 결론부터 말하자면 그렇지 않습니다. Connection을 사용하는 주체인 스레드의 개수보다 커넥션 풀의 크기가 크다면 사용되지 않고 남는 커넥션이 생겨 메모리의 낭비가 발생하게 됩니다.
MySQL의 공식 레퍼런스에서는 600여 명의 유저를 대응하는데 15~20개의 커넥션 풀만으로도 충분하다고 언급하고 있습니다. MySQL은 최대 연결 수를 무제한으로 설정한 뒤 부하 테스트를 진행하면서 최적화된 값을 찾는 것을 추천하기도 합니다.
커넥션 풀 구성은 실수를 정말 많이 하는 부분이라고 합니다. 커넥션 풀을 구성할 때 이해해야 할 몇가지 원칙이 있습니다.
단일 코어는 한 번에 하나의 스레드만 실행할 수 있으며 운영 체제(OS)가 컨텍스트를 전환하면 해당 코어가 다른 스레드의 코드를 실행하는 등의 방식으로 실행됩니다.
간단한 예를 들어보겠습니다. 하나의 CPU 리소스가 주어졌을 때 A와 B를 순차적으로 실행하는 것보다 타임 슬라이싱을 통해 A와 B를 동시에 실행하는 것이 더 빠르다는 것은 당연한 이야기입니다.
우리는 멀티 스레드 환경에서 CPU가 수십, 수백개의 스레드를 동시에 실행할 수 있다고 알고는 있지만, 실제로는 CPU의 속도가 엄청나게 빠르기 때문에 동시에 실행되는 것처럼 느끼는 것 뿐입니다. 실제 CPU 코어는 한 번에 하나의 스레드의 작업만 처리할 수 있습니다.
그렇기 때문에 스레드 수가 CPU 코어 수를 초과하면 더 많은 스레드를 추가해야합니다. 이 과정에서 스레드의 Stack 영역 데이터를 로드하는 등 추가적인 작업이 필요하기 때문에 컨텍스트 스위칭(Context Switching)으로 인한 오버헤드가 발생할 확률도 있습니다.
실제로 HikariCP에서는 적절한 커넥션 풀 설정을 위해 커넥션 풀 공식을 제공해주고 있습니다. 여러 상황을 대비하여 다양한 공식을 제공하는데, 크게 2가지 공식으로 나뉩니다.
아래 공식은 PostgreSQL에서 만들어진 공식이지만, 대부분의 데이터베이스에 적용이 가능합니다.
Pool Size = (core_count * 2) + effective_spindle_count)
- core_count : 현재 사용하는 서버의 CPU Core 개수
- effective_spindle_conut : 하드 디스크 개수
위 공식에 따르면 하나의 하드 디스크가 있는 작은 4코어 8스레드의 서버는 다음과 같은 값을 사용하게 됩니다.
9 = (4 * 2) + 1
스프링에서 제공하는 기본 커넥션 풀 사이즈는 10입니다. 그에 비해 HikariCP에서 제공하는 추천 커넥션 풀 크기는 서버의 사양에 따라 낮아질 수도 높아질 수도 있습니다. HikariCP에서는 위 스펙으로 초당 6000회의 요청을 간단하게 처리할 수 있을거라고 장담한다고 합니다. 뒤에서 간단한 테스트를 진행해보겠습니다.
여러명의 사용자가 동시에 요청했을 때는 데드락이 발생할 확률이 존재합니다. 물론 비즈니스상 데드락이 발생할 확률이 매우 낮은 서비스도 있겠지만요. HikariCP에서는 교착 상태를 피하기 위한 공식도 제공합니다.
Pool Size = Tn x (Cm - 1) + 1
- Tn : 최대 스레드 수
- Cm : 단일 스레드가 보유하는 최대 동시 연결 수
예를 들어, 세 개의 스레드(Tn=3)이 존재하면, 각 스레드는 일부 작업(Cm=4)을 수행하기 위해 4개의 연결이 필요합니다. 위 공식을 적용했을 때 교착 상태가 발생하지 않도록 하는 데 필요한 커넥션 풀 사이즈는 다음과 같습니다.
Pool Size = 3 x (4 - 1) + 1 = 10
아마 @Transaction을 사용하는 환경이라면 현재 트랜잭션에서 이미 커넥션을 보유하고 있는 스레드에 동일한 커넥션을 반환하는 데 필요한 커넥션 수를 크게 줄일 수 있습니다.
이는 반드시 교착 상태, 즉 데드락을 피하기 위한 공식입니다. 최적의 커넥션 풀 사이즈가 아니라는 것이죠. 커넥션 풀 사이즈를 늘리면 이런 상황에서 데드락을 완화할 수는 있지만, 커넥션 풀을 늘리기 전에 애플리케이션 수준에서 수행할 수 있는 작업을 먼저 진행하는게 좋습니다.
기본 커넥션 풀 사이즈(10)으로 VUser 200 기준 부하테스트 실행시 TPS 140에서 애플리케이션이 커넥션을 기다리는 시간이 길어졌습니다.
그런데 여기서 고려해야할 점이 있습니다. 저렇게 병목 현상이 생기는 이유가 WAS의 스레드 풀 때문인지, 아니면 데이터베이스의 커넥션 풀 문제인지를 판단해야합니다.
여러가지 방법이 있을 수도 있겠지만, 해당 지표만으로 스레드 풀이 부족한건지 데이터베이스의 커넥션 풀이 부족한건지 알 수는 없다고 판단했습니다. 그래서 가장 고전적인 방법으로 스레드 풀과 데이터베이스 커넥션 풀 모두 값을 조정해서 측정해보았지만, WAS 값을 변경했을 때 유의미한 변화는 생기지 않았습니다.
기본 커넥션 풀 사이즈(10)일 때 VUser 200 기준 TPS 140에서 병목 현상이 발생했었습니다. DBCP 5로 변경 후 부하테스트를 다시 진행했을 때 얻을 수 있는 결과는 다음과 같았습니다.
DBCP만 줄였을 뿐인데 평균 TPS가 약 40정도 증가했습니다. 실제로 HikariCP에서 제공하는 DBCP 공식이 꽤나 유의미한 결과를 도출시켰습니다.