백엔드/Spring Boot

Hikari CP 모니터링하기

infitry 2024. 6. 6. 20:00
반응형

상황에 따른 Hikari CP의 커넥션 풀 상태가 궁금해졌습니다.

@Transactional 어노테이션에 따라 active 가 되고 언제 다시 idle로 돌아가는지 알아보기 위해 모니터링을 위한 준비를 했습니다.

다음과 같은 클래스 구조에서 확인해 보겠습니다.

다음과 같이 테스트 합니다.

1. 먼저 facade 클래스의 메서드가 실행되면 Thread.sleep으로 많은 일을 처리했다고 가정합니다.

2. @Transactional 어노테이션이 걸려있는 서비스의 메서드를 호출합니다.

 

각 과정 사이에 HikariCP 로그를 찍어 Connection pool 상태를 확인합니다.

자세한 소스코드는 다음 github를 참고해 주세요.

 

먼저 HikariCP를 모니터링하기 위해 application.yml 에 다음 설정을 추가합니다.

 

소스코드

application.yml

spring:
  datasource:
    hikari:
      register-mbeans: true
      pool-name: hikari

 

HikariPoolMXBean을 사용하기 위해 다음과 같이 빈을 등록해 줍니다.

package com.infitry.laboratory.config;

import com.zaxxer.hikari.HikariPoolMXBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.management.JMX;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;

@Configuration
public class HikariConfig {
    @Bean
    public HikariPoolMXBean hikariPoolMXBean() throws MalformedObjectNameException {
        MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
        ObjectName objectName = new ObjectName("com.zaxxer.hikari:type=Pool (hikari)");
        return JMX.newMBeanProxy(mBeanServer, objectName, HikariPoolMXBean.class);
    }
}

 

위 그림의 실제 비즈니스 로직이 동작하는 Facade 클래스입니다.

 

ConnectionPoolFacade.java

package com.infitry.laboratory.service.transaction.connectionpool;

import com.zaxxer.hikari.HikariPoolMXBean;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class ConnectionPoolFacade {
    private final ConnectionPoolService connectionPoolService;
    private final HikariPoolMXBean hikariPoolMXBean;
    private static final int LATENCY = 3000;

    public void doSomethingBefore() throws Exception {
        printCurrentConnectionPool("1");
        sleep();
        printCurrentConnectionPool("2");
        connectionPoolService.whenGetConnectionInTransactional();
        printCurrentConnectionPool("3");
    }

    public void doSomethingAfter() throws Exception {
        printCurrentConnectionPool("1");
        connectionPoolService.whenGetConnectionInTransactional();
        printCurrentConnectionPool("2");
        sleep();
        printCurrentConnectionPool("3");
    }

    private static void sleep() throws InterruptedException {
        log.info("start sleep!!!");
        Thread.sleep(LATENCY);
        log.info("end sleep!!");
    }

    private void printCurrentConnectionPool(String id) {
        log.info("{}, (total={}, active={}, idle={}, waiting={})", id,
                hikariPoolMXBean.getTotalConnections(), hikariPoolMXBean.getActiveConnections(),
                hikariPoolMXBean.getIdleConnections(), hikariPoolMXBean.getThreadsAwaitingConnection());
    }
}

 

ConnectionPoolService.java

package com.infitry.laboratory.service.transaction.connectionpool;

import com.infitry.laboratory.persistence.jpa.MemberRepository;
import com.zaxxer.hikari.HikariPoolMXBean;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ConnectionPoolService {
    private final MemberRepository memberRepository;
    private final HikariPoolMXBean hikariPoolMXBean;

    @Transactional
    public void whenGetConnectionInTransactional() throws Exception {
        printCurrentConnectionPool();
        memberRepository.findAll();
        Thread.sleep(2000);
    }

    private void printCurrentConnectionPool() {
        log.info("in transaction, (total={}, active={}, idle={}, waiting={})",
                hikariPoolMXBean.getTotalConnections(), hikariPoolMXBean.getActiveConnections(),
                hikariPoolMXBean.getIdleConnections(), hikariPoolMXBean.getThreadsAwaitingConnection());
    }
}

 

테스트

먼저 doSomethingBefore()를 실행해 보면?

 

1. 처음엔 idle connection 이 5개입니다.
2. sleep 이후 에도 마찬가지로 active connection 이 없습니다.

3. 트랜잭션이 실행되는 순간 idle connection 이 4개, active connection이 1개가 됩니다.

4. 트랜잭션이 종료된 후 idle connection 이 다시 5개, active connection이 1개가 됩니다.

 

doSomethingAfter() 도 마찬가지로 실행됩니다.

 

정리

다음 결과로 트랜잭션 범위에 대한 중요성을 깨달을 수 있습니다.

트랜잭션 범위를 넓게 잡고 트랜잭션 범위 안에서 시간이 오래 걸리는 작업이 있다면 ex) API 호출

hikari CP의 connection을 사용하여 반환하지 않습니다.

결국 커넥션이 모두 소진되어 waiting 이 늘어 갈 것이고 오랜 시간 커넥션을 얻지 못한 요청은 오류가 발생할 것입니다.

많은 사용자의 요청을 처리하기 위해서는 트랜잭션의 범위를 설정하는데 항상 주의가 필요한 것 같습니다.

반응형