ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • (성능테스트) Hikari pool connection 데드락 해결
    프로젝트/BLUE DELIVERY 2021. 8. 23. 18:22

     

    안녕하세요.

    최근 진행중인 프로젝트에서 최근 주문 API를 구현하고, JMeter를 이용해 부하 테스트를 진행한 적이 있습니다.

    적은 트래픽에서는 문제가 없었는데, 부하를 일으키니 Hikari pool connection timeout 발생으로 요청을 처리하지 못하는 문제를 겪은 적이 있습니다.

     

    이번 글에서는 이 문제를 발견하고 원인을 찾은 후 해결하는 과정까지를 정리해보려고 합니다.

     

    문제점

    주문 API를 구현하고 간단하게 테스트 해보았을 때는 아무 문제가 없었습니다.

    그러나 JMeter를 이용해 부하테스트를 진행하면 대부분의 요청이 실패하는 문제가 생겼습니다.

     

    임시 조치

    콘솔창에 찍힌 에러 로그를 확인하니, HikariPool 에서 connection을 얻을 수 없다고 합니다.

    hikari pool 의 사이즈를 200까지 늘려주었더니, 에러가 말끔히 사라졌습니다.

    하지만 이런식으로 무작정 자원을 늘려 해결하는건 어딘가 찝찝합니다..

    대충 상황을 넘기기 보단, 근본적인 원인을 알고싶었습니다.

     

    원인 파악

    APM을 통해 trace를 확인해보니,

    요청이 성공한 경우는 다음과 같이 select/update hibernate_sequence 에서 대부분의 시간을 점유하고 있었습니다.

    실패의 경우 대부분이 이렇게 hibernate_sequence 조회 직전에 끊겨있었습니다.

     

    에러를 확인해보니 (아까 확인한 로그와 마찬가지로) JDBC 커넥션을 얻지 못해 timeout이 발생하고 있었습니다.

    지금까지 정황을 보았을 때, hibernate_sequence 테이블과 관련이 있는게 분명해보입니다.

     

    hibernate_sequence 테이블

    프로젝트에서 Spring-Data-JPA와 구현체로 Hibernate 5.4 버전, DB에 MySQL을 사용하고 있습니다.

    JPA에서 자동 키 생성 전략으로 AUTO, IDENTITY, SEQUENCE, TABLE 을 제공하고 있습니다.

    • _IDENTITY_는 기본 키 생성을 JPA에서 하지 않고, 데이터베이스에 위임하는 방법입니다. MySQL의 경우 auto_increment가 적용됩니다.
    • _SEQUENCE_는 시퀀스는 데이터베이스에서 유일한 값을 생성해내는 데이터베이스 오브젝트입니다. 이 전략의 경우 데이터베이스에서 시퀀스 기능을 제공해야 합니다. MySQL의 경우 지원하지 않습니다.
    • _TABLE_은 시퀀스 기능을 흉내낸 전략으로, 내부 동작이 비슷하지만 시퀀스 대신 테이블을 사용합니다.
    • AUTO 는 현재 사용하고 있는 데이터베이스에 따라 자동으로 선택하는 전략입니다.

    그리고 저는 다음과 같이 주문관련 도메인에서 모두 default (=AUTO) 전략을 사용하고 있었습니다.

    public class Order {
        @Id @GeneratedValue
        private Long orderId;
    }
    
    public class OrderItem {
        @Id @GeneratedValue
        @Column(name = "ORDER_ITEM_ID")
        private Long id;
    }
    
    // OrderItemOptionGroup .. 
    
    // OrderItemOption ..

    AUTO 전략은 데이터베이스가 바뀌어도 최적의 전략을 선택해줄 것만 같습니다.

    Hibernate5 전에는 MySQL의 경우 _IDENTITY_가 기본 전략이었다고 하는데, Hibernate5 이상부터는 TABLE 전략이 기본으로 바뀌었습니다.

    그리고 이 TABLE 전략에서 사용하는 테이블이 바로 hibernate_sequence 입니다.

    그렇다면 이 TABLE 전략이 문제를 일으킨 걸까요?

    TABLE 전략을 사용함으로서 다음과 같은 쿼리가 발생하게 됩니다.

    select next_val as id_val from hibernate_sequence for update
    update hibernate_sequence set next_val= ? where next_val=?

    select ~ for update 를 통해 트랜잭션이 끝날 때 까지 다른 세션에서 레코드에 접근하지 못하도록 X락을 겁니다. 그리고 update 쿼리까지 마쳐야 트랜잭션이 끝나게 됩니다.

    우선 주문 API를 처리하는 그림을 먼저 보여드리겠습니다.

    (hikari cp에 따르면 default maximum-pool-size = 10 인데, 편의상 3개로 가정하겠습니다.)

    대략 요런 상황이 될텐데요.

     

    각 쓰레드들은 커넥션을 하나씩 가지고, 커넥션에서 작업이 끝나면 락을 풀것이고, 그럼 다른 커넥션들이 (좀 늦더라도) 차례가 오면 작업을 마칠 수 있을 것입니다.

     

    진짜 문제 상황

    그런데 문제는 TABLE 전략을 사용하면 hibernate_sequence에서 채번 및 업데이트를 위해 추가적인 커넥션을 필요로 한다는 점입니다.

    • 그런데 이미 각각의 쓰레드가 커넥션을 점유하고 있습니다.
    • 각각의 커넥션들은 select/update hibernate_sequence 작업이 끝나야 반납될 수 있기 때문에 반납되지 못합니다.

     

    이렇게 서로 타이밍과 자원상황이 안맞아서 데드락이 발생하게 되었습니다.

    추가적인 커넥션이 필요하다는 것은 https://techblog.woowahan.com/2663/ 를 보고 알게되었습니다.

     

    해결 방법

    TABLE 전략을 사용함으로써 생긴 문제이므로, 전략을 바꾸면 됩니다.

     

    굳이 TABLE 전략이 필요한 상황도 아니며, 성능적인 면에서 봤을 때도 위에서 봤듯이 쓰기-락을 걸고 매번 select/update를 해야하므로 딱히 이점이 있지 않습니다.

    여러 의견들을 찾아보아도 TABLE의 이러한 점때문에 MySQL 처럼 시퀀스를 지원하지 않는 DBMS를 사용하는 경우 IDENTITY 전략을 사용하는 것을 권장하고 있습니다.

    public class Order {
        @Id @GeneratedValue(strategy = GenerationType.IDENTITY) 
        private Long orderId;
    }
    // 다른 클래스는 생략

    이제 에러가 없습니다 짜잔

    만약 TABLE 전략을 필요로 하는 상황이라면 커넥션풀의 수를 계산하는 공식이 있습니다. 그것 역시 https://techblog.woowahan.com/2663/ 에서 확인하실 수 있습니다.

     

    후기

    이번이 사실상 처음으로 APM 모니터링과 부하테스트를 이용해 개선시킨 경험이었는데요. 그동안 와닿지 않았던 부분들이 실제로 데드락을 경험하니 와닿기 시작했습니다.

    공부를 하면서 항상 머리속으로는 '성능' 이라는 요소를 생각하고 있었지만 그렇게 신경쓰지 않았던 것 같습니다.

    오히려 공부는 했지만 '쿼리 하나 더 나가도... 설정 조금 달라도.. 성능이 그렇게 크게 차이가 나려나' 라는 생각도 했었는데요.

     

    이번 일을 겪으면서 '와. 생각없이 코딩한 대가가 이렇게 요청을 싹 다 무시하는 데드락으로 돌아올 수 있구나' 싶어 놀라웠습니다.

     

    이 외에도 불필요한 쿼리들이 나가는 것도 보이고 하는데, 이러한 것들이 예전과 달리 중요하게 와닿는 것 같아서.. 재밌습니다.

     

    🌝 감사합니다. 🌝

     

     

    참고

    https://techblog.woowahan.com/2663/

     

    댓글

Designed by Tistory.