적절한 ConnectionPool 설정의 중요성

점돌이·2023년 11월 13일
0

오류 발생

    @Transactional
    public void sellTicket() {
        try {
            boolean isLock = getLock();
            if(isLock){
                서비스_로직();//propagation.REQUIRING_NEW
            }
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            releaseLock();
        }
    }

오류가 발생한 코드는 위와 같다.
간단히 설명하면 Mysql NamedLock을 이용해 동시성을 제어하는 코드이다.

  1. 락을 시도한다
  2. 락을 얻었다면 서비스 로직을 실행한다.
  3. 락을 해제한다

스프링부트에서 아무런 설정을 하지않고 여러 스레드가 동시에 접근하는 해당 코드를 테스트해보면 다음과 같은 오류가 발생한다.

o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 0, SQLState: null
o.h.engine.jdbc.spi.SqlExceptionHelper : hikari-pool-1 – Connection is not available, request timed out after 30006ms.

시간이 초과되어 커넥션을 이용할 수 없다는 내용인데 이유를 파헤쳐 보겠다.

원인

Hikari CP 작동방식

먼저 Hikari CP가 어떤식으로 작동하는지부터 알아봐야한다.
대략 설명하면

  1. 풀에 이용가능한 커넥션이 있는지 확인
    1-1. 없다면 큐에서 일정시간 대기
  2. 전에 이용하던 커넥션이 있는지 확인
    2-1. 없다면 남은 커넥션과 연결
  3. 사용끝났으면 반납

이런 프로세스를 거친다.

오류나는 이유

오류나는 이유는 위에서 Hikari CP 작동 방식 1-1번에서 일정시간 대기하다가 타임아웃으로 오류가 난다.
그렇다면 왜 타임아웃 되도록 커넥션을 얻지 못한걸까?
그 이유는 바로 코드에 있다. 코드를 다시 한번 살펴보면

    @Transactional
    public void sellTicket() {
        try {
            //커넥션 필요(1)
            boolean isLock = getLock();
            if(isLock){
            	//커넥션 필요(2)
                서비스_로직();//propagation.REQUIRING_NEW
            }
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            releaseLock();
        }
    }

즉, 2개의 트랜잭션이 생기며 2개의 커넥션이 필요한 것이다.
그렇다면 SpringBoot의 기본설정을 알아보자.

public class HikariConfig implements HikariConfigMXBean{
	private static final long CONNECTION_TIMEOUT = SECONDS.toMillis(30); //30초
	private static final int DEFAULT_POOL_SIZE = 10; // 10개
}

Pool에는 10개의 커넥션과 30초의 타임아웃이 설정되어 있다.
실제상황과 다르겠지만 상황을 가정해보겠다.

  1. 10개의 스레드가 동시에 get_lock 시도(10/10)
  2. 커넥션을 점유 한상태로 이후 서비스_로직의 추가 커넥션 점유 시도 (20/10)
  3. 10개 스레드가 추가 커넥션을 얻으려 대기하다 타임아웃으로 예외 발생 이후 롤백

이런 상황이 연출 될 수 있다.
실제로 오류나는 대략적인 이유는 위와 같다.
커넥션을 얻기위해 교착상태로 대기하다 타임아웃으로 서비스_로직이 실행이 안되는것이다.
원인을 파악하니 해결방법이 눈에 보인다.
바로 Pool Size를 늘리면 된다.

Pool Size 설정하기

Pool Size를 무작정 늘리면 오류는 나지 않겠지만 커넥션을 만들어 놓는거 또한 자원을 소모하는 일이다.
따라서 적절한 사이즈를 설정해야하는데 어떤식으로 설정하는게 좋을까?
교착상태가 일어나지 않게 단 하나의 여유공간만 있으면 될거 같다는 생각이 든다.
단 하나의 공간으로 서브 트랜잭션 커넥션을 얻으면 되니말이다.

필요한 풀사이즈 = 스레드 수 * (하나의 작업에 필요한 커넥션 수 - 1) + 1(교착상태를 피하기 위한 여유 공간)

테스트 환경에서 30개의 스레드, 위 작업에 2개의 커넥션이 필요하므로

30 * (2 - 1) + 1 = 31

31개만 있으면 교착상태를 피할 수 있다고 한다.
확인해보자.

[pool size 변경전]

spring:
  datasource:
    hikari:
      maximum-pool-size: 10

업로드중..

에러가 발생한다.

[pool size 변경 후]

spring:
  datasource:
    hikari:
      maximum-pool-size: 31

업로드중..

오류없이 테스트를 통과 했다.

하지만 위의 설정은 어디까지나 데드락을 피하는 최소한의 크기다.
저말대로면 Pool Size를 1로 설정하면 모든 요청을 처리할 수 있다는 뜻이기도하다.
좀 더 여유롭게 설정해야 효율적으로 요청을 처리할 수 있을것이다.

배운점

솔직히 말하자면 커넥션풀이 교착상태에 빠질정도로 많은 요청을 처리해본적이 없다.
동시성 제어 키워드를 알게되었고 이를 통해 공부하다보니 분산락까지 닿았고 이후에 오류로 Hikari CP가 대략적으로 어떤식으로 작동하는지도 알게 되었다.
락을 거는 데이터소스와 서비스 로직을 수행하는 데이터소스를 왜 분리해야하는지 어떤 영향을 끼치는지 배우게 되었다.
아직은 얕지만 좀 더 코드를 짤때 많은 상황을 고려할 수 있는 인사이트를 얻었다.

참조

💡 pool size dead lock 참조 : https://techblog.woowahan.com/2664/

💡 mysql named lock 참조 : https://techblog.woowahan.com/2631/

profile
감사합니다.

1개의 댓글

comment-user-thumbnail
2023년 11월 13일

좋은 정보 감사합니다

답글 달기