인증메일 쿼리 속도 비교, 배치 대용량 Insert (갑자기 궁금해서 해본) - Chaedie/spring-board GitHub Wiki

findAll, existsBy 성능 비교

findAll 보다 exists가 훨씬 빨리 실행될줄 알았는데 아니더라.

데이터가 너무 적나 싶어서 1만건 넣고 돌렸더니 똑같았다.

  • findAll 25ms 가량
  • existBy 40ms 가량

아래에 있을 많은 과정을 겪고 다시 돌아와 20만건 정도의 데이터를 가진 상태에서 확인을 해보니 예상했던 결과가 나왔다.

  1. 데이터가 많아지니 예상대로 findAll이 100ms이상, existsBy가 50ms로 limit으로 1건만 찾고 끝내는 것이 빠르게나왔다. 이는 count와 exists로 비교해도 1건만 찾아도 true로 return되는 exists가 빠른것과 같은 원리다.
  2. 아래 Explain과 초는 MySQLWorkBench에서 생쿼리 날렸을 경우의 속도이다.

대용량 배치 INSERT

데이터가 너무 없어서 그런가 해서 dummy controller 만들어서 insert db 콜 100만건을 for문으로 돌렸다. 그리고 엄청난 DB콜을 보면서 깨달았다.

아 어제 본 "슬래쉬 토스뱅크 데이터베이스 설계 사상" 영상에서 for문으로 100만건의 DB콜을 하는게 아니라 Merge into 를 사용한 한방쿼리로 1회의 DB콜만으로 배치 대용량 처리를 했다고 한게 이 말이구나.. 깨달았다. ㅎㅎ

한방 쿼리 그거 어떻게 하는건데..?


100만건 DB콜 For문으로 돌리는 중

  • 1,000건 for문 돌릴 땐 1건당 1ms 걸림
  • 25,000건 되니 1건당 20ms 걸림
  • 50,000건 되니 1건당 30ms 걸림
  • 70,000건 되니 1건당 50ms 걸림
  • 80,000건 되니 1건당 60ms 걸림
  • 혹시 데이터가 많아져서 생기는 문제가 아닐 수 있으니 확인을 위해 일단 for문 중단
  • for문 중단 후 85,000건 부터 시작했지만 1건당 1ms 부터 시작함
  • connection pool의 문제인지?? 등등 다른 영향이 있는것 같다, 확인 필요
    • 커넥션 풀은 was-db간에 비용 중 connection 그 자체에 비용이 많이 들기 때문에 미리 connection을 생성해두고 커넥션을 할당해서 DB 처리를 하는 방식이다.
    • 그럼 지금 느려졌던 원인은 커넥션 풀의 문제는 아닌것 같다?
    • 궁금한 점이 for문으로 인서트 쿼리를 쭉쭉 날리는데 이걸 동기적으로 처리하고 있다. 근데 이걸 DBMS에서 자동으로 큐잉시키고 여러 프로세스나 스레드로 나눠서 작업을 진행하는 것인지??? 만약 이 가설이 맞다면 느려지는데 조금 이해는 된다. 최초 API 콜들은 메시지 큐가 비어있는 상태였고, 가면 갈수록 유휴 자원이 없어지고 큐에 차있던 작업들을 완성하는데 까지 시간이 걸리니까.
    • 그렇다면 for문을 통해 동기적으로 DB 콜을 날리는걸 비동기로 날리고 메시지 큐를 중간에 둬서 속도 느려지는걸 방지하겠다는 생각은 이미 DB가 알아서 해주고있는건가? 확인해봐야겠다.

@Transactional

검색하던 중 @Transactional으로 묶지 않은 경우 CP 누수가 발생하는 이슈가 있었다는 말에 @Transactional로 감싸보았다. 그리고 10만건 돌려보니 헐,, 순식간에 10만건이 바로 끝났다...

@GetMapping("/insertAuthMail")
@Transactional // 트랜잭셔널 하나로 해결이 되어 버린 이슈.. 
public String insert_100_000_AuthMail() {

    for (int i = 0; i < 100_000; i++) {
        emailAuthRepository.save(EmailAuth.builder()
            .userEmail(UUID.randomUUID().toString())
            .authCode(UUID.randomUUID().toString().substring(0, 8)).build());
    }
    return "done!";
}
  • 왜 그런지 이해가 안되는데 @Transactional과 Deadlock에 대해 찾아봐야겠다. 모르는게 많다. 아니 100중에 99가 새로 알아야할 내용들인 것 같다. ㅎㅎ
  • 일단 주워들은 이야기들을 생각해봤을 때 의심가는 부분은 단건 Insert 문만 진행했기에 Row Lock이 걸려버린것 같은데 Row Lock이 걸려서 문제였다는 로그는 어떻게 남기는지? 어디서 확인하는지?, 그리고 Row Lock이 정확히 뭔지, 언제 발생하는지, 그리고 MySQL에서의 Row Lock 에 대해 알아봐야겠다.

트랜잭셔널을 적어주면 속도가 개선 되는 이유를 찾았다.....

  • [JPA] INSERT 속도 최적화
  • 위 글을 보면 save 자체에 @Tranasactional이 붙어 있고, 1만건을 수행하면 1만건의 트랜잭션이 시작-종료 된다는 뜻이다. 그런데 1만건을 한건의 트랜잭션으로 묶어주면 1건의 트랜잭션만 시작-종료 되므로 속도 개선이 일어난다.
  • 근데 위 글에선 list에 담아서 saveAll을 통해 1건으로 해라고 되어있다.
  • 내 코드에선 그런 작업을 한적이 없는데... 왜 속도가 개선되었을까? 그건 아마 JPA의 영속성 컨텍스트와 flush 때문인듯하다.
    • flush는 영속성 컨텍스트의 변경 내용을 DB에 반영하는 작업이다.
    • 10만건의 save를 통해 영속성 컨텍스트를 변경 시킨다.
    • 그리고 단 한번의 트랜잭션만 커밋하기 때문에 Flush가 단 한 번만 동작한다.
    • 따라서 변경 감지, DB전송이 10만건 일어나는 경우 vs 쓰기 지연 으로 인해 1건만 변경감지, DB전송이 일어나는 경우를 비교하면 당연히 후자가 빠른것이다.... 찾았다.. ㅠㅠ

출처: 자바 ORM 표준 JPA 프로그래밍 - 김영한 저

Heap Dump

  • 매번 Flush를 하면서 dirty check(변경 감지)와 DB에 반영하는 작업 때문에 느려지는건 이해했다. 그런데 dirty check를 해야하는 영속성 컨텍스트의 사이즈가 계속적으로 커지기 때문에 점점 느려지는것 이라고 생각하면 될것같은데 그럼 영속성 컨텍스트의 사이즈는 어떻게 보는건가?
  • 서버 장애관련 글을 보다보니 VisualVM이라는 툴로 Heap Dump 를 볼 수 있다고 해서 단건 flush가 동작하도록 하고 heap size를 확인했다.
  • heap size와 느려지는 속도의 상관관계는 명확하진 않아보인다. 조금 아쉬운 결과.
  • 모니터에는 힙이 어떻게 되는지 볼 수 없지만, Heap Dump 버튼을 눌러서 힙덤프를 뜨고, 해당 시점에서의 영속성컨텍스트의 사이즈를 확인할 수 있었다..!! 1분만에 PersistenceContext의 사이즈가 늘어나는걸 볼수 있다. ㅋㅋㅋ
  • 이 사이즈가 늘어남에 따라 dirty check에 소요되는 시간이 비례하게 늘어나야하는게 가설인데....!!! 상관관계가 있을 것으로 에상된다.

Row Lock에 대해 알아보자

해볼 내용들

  • findAll로 list.size() > 0이 빠를까? existsBy가 빠를까?
    • 데이터가 많아지면 어떨까? -> 데이터 적을땐 이상하게 existsBy가 느렸지만 데이터가 많아지니 예상대로 existsBy가 빠르다
    • (인덱스) byUserEmailAndAuthCode 이니까 (userEmail, authCode)로 인덱스를 걸면 훨씬 빨라지나? -> 인덱스로 인한 select 성능은 크게 차이 없었다. 그것도 신기하네
  • @Transactional과 Dead Lock의 관계
    • Row Lock이 뭔지? 왜 언제 발생? 예방하는 방법?
    • Row Lock 등 발생 시 로깅하는 법, 또는 확인하는 법
  • Spring Batch 또는 DB 한방 쿼리로 Insert 하는 방법 알아보기
  • 메시지 큐를 활용하여 비동기적으로 DB Call을 날리면 어떻게 되나?
    • 도대체 메시지 큐는 어떻게 쓰는건가? (Redis || Kafka) - 이건 궁금해도 시간관계상 후순위로 미루는게 낫지않나?

레퍼런스