ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Long Transaction을 감지하고 대응하는 방법
    JPA 2025. 2. 25. 01:04
    반응형

    개요

    Long Transaction으로 인해 DB에 부하가 발생하여 같은 DB를 활용하는 모든 서버들에 영향이 가는 상황이 발생하여 왜 그런 일이 일어나는지, 어떻게 하면 예방할 수 있을지 공부하며 기록해보고자 합니다.

     

     

    Long Transaction이란?

    Long Transaction은 트랜잭션이 열리고 긴 시간 동안 commit, rollback이 수행되지 않고 있는 데이터베이스 트랜잭션을 말합니다.

     

    데이터베이스에서 트랜잭션은 하나의 논리적인 작업 단위를 의미하며, 데이터의 일관성(Consistency)과 무결성(Integrity) 을 보장하는 중요한 역할을 합니다.

     

    긴 시간이라는 것은 상대적이긴 개념입니다.

     

    만약 평균 트랜잭션이 100ms 이내에 처리된다면, 1초만 넘어가도 상대적으로 Long Transaction으로 볼 수 있습니다.

     

    반면, 데이터 마이그레이션과 같은 작업에서는 몇 분이 걸려도 Long Transaction으로 간주되지 않을 수 있습니다.

     

    따라서 서비스의 특성에 맞도록 임계치를 잡아 Long Transaction으로 분류해야 합니다.

     

     

    Long Transaction 영향

    첫 번째로 특정 rows, 특정 table에 트랜잭션이 lock을 잡고 놓아주지 않는다면 동일 rows, 동일 table에 추가적으로 lock을 획득하고자 하는 다른 스레드가 기다리게 되고 이 것이 심하면 전체장애로 번질 수 있습니다.

     

    두 번째로 MySQL 공용 내부 자원을 잠식하여 전체 지연으로 이어지는 케이스들도 존재합니다.

    InnoDB 엔진은 MVCC를 활용하여 트랜잭션 격리성을 보장합니다.

    만약 Long Transaction이 오랫동안 커밋되지 않으면, 변경된 데이터의 이전 버전을 UNDO 로그에 저장해야 합니다.

    이로 인해 UNDO 로그가 커지면서 디스크 사용량이 급증하고, 성능 저하 발생 가능합니다.

     

    세 번째로 서버와 데이터베이스를 연동할 때 보통 Connection Pool을 활용하곤 하는데, 하나의 트랜잭션을 처리하는데 시간이 오래 걸리게 되면 요청이 증가함에 따라 Connection Pool이 고갈될 가능성이 큽니다.

     

    Long Transaction 감지와 대응

    데이터베이스에 문제가 생기면 서비스를 할 수 없어지므로 Long Transaction을 감지할 수 있어야 하고, Long Transaction이 발생했다면 해당 Transaction을 Kill 하는 등 대응할 수 있어야 합니다.

     

    어떻게 Long Transaction인 경우 예외를 발생시켜서 Kill 할 수 있을까요?

    Spring의 JPA를 활용중이라면 application.yml 설정으로 일괄적으로 세팅할 수 있습니다.

    spring:
      transaction:
        default-timeout: 5 # 모든 트랜잭션의 기본 타임아웃 (초 단위)

     

    발생하는 예외

    org.hibernate.TransactionException: transaction timeout expired
    	at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.determineRemainingTransactionTimeOutPeriod(JdbcCoordinatorImpl.java:262) ~[hibernate-core-6.2.2.Final.jar:6.2.2.Final]

    5초가 넘어가는 경우 transaction timeout expired라는 메시지와 함께 hibernate의 TransactionException이 발생합니다.

     

    만약 개별로 적용하고 싶다면 @Transactional 메서드에 값을 직접 주입하면 됩니다.

        @Transactional(timeout = 10)
        fun makeLongTransaction() {
            repeat(5000){
                testEntityRepository.save(TestEntity())
                sleep(100)
            }
        }

     

     

    하지만 실행중인 Transaction을 Kill 하는 것은 서비스에 어떤 사이드 이펙트를 가져올지 예측하기 어렵기 때문에 위험할 수 있습니다.

     

    이때는 감지를 해서 임계치를 넘는 경우 제어해 볼 수 있습니다.

     

    Hibernate Interceptor 기능을 활용해 볼 수 있습니다.

     

    트랜잭션이 시작되었을때, 시간을 저장해 두고 트랜잭션이 종료되었을 때 해당 시간이 지나간 경우 warn, error 로그등을 남겨두면 됩니다.

     

    class TransactionTimeInterceptor : Interceptor {
    
        private val transactionStartTimes = ConcurrentHashMap<Transaction, Long>()
    
        override fun afterTransactionBegin(tx: Transaction) {
            transactionStartTimes[tx] = System.currentTimeMillis()
        }
    
        override fun afterTransactionCompletion(tx: Transaction) {
            val startTime = transactionStartTimes.remove(tx) ?: return
            val duration = System.currentTimeMillis() - startTime
    
            when {
                duration > 10_000 -> logger.warn("⚠️ [SLOW TRANSACTION] 실행 시간: {}ms (10초 초과!)", duration)
                duration > 5_000 -> logger.warn("⚠️ [SLOW TRANSACTION] 실행 시간: {}ms (5초 초과!)", duration)
                duration > 3_000 -> logger.info("✅ [NOTICE] 실행 시간: {}ms (3초 초과)", duration)
                else -> logger.debug("✅ [OK] 실행 시간: {}ms", duration)
            }
        }
    }

     

    ConcurrentHashMap에 Transaction을 저장해 두고, 끝날 때 시간을 측정하여 특정 시간이 넘어가는 경우 노티를 주게 됩니다.

     

     

     

    참고자료

    https://gokhansengun.com/why-do-long-db-transactions-affect-performance/

     

     

    댓글

Designed by Tistory.