가상스레드에서 DB 커넥션풀 사용시의 문제점

스프링에서 가장 많이 사용되는 Hikari를 사용해서 테스트 해보자
테스트 결과는 아래와 같다.
처참하다.

문제의 원인

기존에 톰캣의 HTTP 커넥터 풀은 최대 스레드 수가 제한되어 있어 DB커넥션 사용에 있어서 적절한 웨이팅 시간 유지가 가능했다.
하지만, 가상스레드를 사용함으로 인해, HTTP 요청을 어마어마하게 받다보니, DB커넥션을 풀에서 받으려 기다리다 기다리다..
결국 지쳐서 위와 같은 결과를 발생시킨다.
 

문제의 해결

결과적으로 백프레셔가 필요한데,
이걸 자바에서 동시성에 제공되는 세마포어를 사용하여 해결해 보자.

    private lateinit var semaphore: Semaphore

    private var maxPoolSize: Int = DEFAULT_MAX_POOL_SIZE

    @PostConstruct
    fun initSemaphore() {
        if (readWriteDataSource.maximumPoolSize > 0) {
            maxPoolSize = readWriteDataSource.maximumPoolSize
        }
        semaphore = Semaphore(maxPoolSize, true)
    }

    @Pointcut("execution(* com.zaxxer.hikari..HikariDataSource.getConnection(..))")
    fun hikariDataSource() = Unit

    @Around("hikariDataSource()")
    fun around(joinPoint: ProceedingJoinPoint): Any? {
        try {
            semaphore.acquire()
            return joinPoint.proceed()
        } finally {
            semaphore.release()
        }
    }

 

결과

결과는 매우 좋다. 에러율 0%.
실제 요청에 있어서 응답시간은 매우 늦긴하지만, 오류를 딜리버리 하지 않는다는게 핵심

이대로 적용?

이걸 실제로 사용하기에는 어렵다. 단순히 커넥션을 얻는 부분만 래핑이 되어 있기에,
커넥션을 얻은 후 처리 로직이 길어질 경우 문제가 생길 수 있다.(하지만 무난한 적용..)

@Pointcut("within(@org.springframework.transaction.annotation.Transactional *)")

이걸로 설정하려면 좀 더 괜찮으려나?
아니다. 위 방법 또한 데드락 지점이 생긴다.
(남은 자원이 0일때 해당 어노테이션을 사용하는 셀프인보케이션이 아닌 지점에서 모두가 동시에 요청을 한다면, 서로 들고 있는 자원은 릴리즈 되지 않고, 그저 서로 릴리즈 하기만을 기다리는 미친 상황..)
 
그냥 AOP 코드가 변경이 되야 하는데,
트랜잭션 할당 여부 확인하고, 스레드 확인해서 최초 요청에 대한 부분 기억해 두었다가, 필요하지 않은 경우에는 세마포어 할당을 안받도록 하면 된다. 
말이 쉽지 생각보다 복잡..게다가 전파설정이 REQUIRES_NEW 라면? 새로운 커넥션도 할당 받어야 하는 상황까지 고려해야 한다.

안 비밀

테스트한 API는 db에서 sleep을 하는 것과 UNIQUE INDEX를 사용하는 쿼리를 대상으로 테스트 해보았다.
sleep이 아닌 일반 쿼리는 pool size를 100으로 했을때, Throughput이 50정도 나오긴 했다. 
RDB를 사용한다면 성능적인 차이보다는 비용적인 부분에서 이점이 생기긴 할듯!
결정적으로 mysql jdbc의 connectionImpl 클래스를 보면 synchronized 천지라서 스레드 피닝이 발생한다는 점!

+ Recent posts