SSARTEL-10th / JPTS_bookstudy

"개발자가 반드시 알아야 할 자바 성능 튜닝 이야기" 완전 정복
7 stars 0 forks source link

Batch와 Fetch의 성능과 적절한 크기는? #13

Open ChoiSeEun opened 9 months ago

ChoiSeEun commented 9 months ago

👍 문제

p.234-235 에는 executeBatch()setFetchSize() 메서드를 적절하게 사용하면 성능에 도움이 된다고 언급되어 있습니다.

이 두 메서드가 어떤 환경 (개발 환경, 데이터 개수 등) 에서 성능에 유의미한 차이를 줄 수 있는지 정리해보면 좋을 것 같습니다. 또한,실제 개발 환경에서는 어떤 기준으로 Batch 및 Fetch 사이즈를 구하는지도 정리해보면 좋을 것 같습니다.

✈️ 선정 배경

평소에는 대량의 데이터를 다룰 일이 많이 없다보니, 늘 지나치고 넘어갔는데 교재에 언급이 되어 있어서 궁금증이 생겼습니다. 워낙 상황에 따라 다른 부분이기 때문에 사례들을 봐두면 이후에도 도움이 될 것 같아 선정했습니다.

📺 관련 챕터 및 레퍼런스

Story12. DB를 사용하면서 발생 가능한 문제점

🐳 비고

명확한 기준이 없는 만큼 참고할 수 있을정도로만 가볍게 정리하면 좋을 것 같습니다.

Yg-Hong commented 9 months ago

서론

Batch, 다들 알겠지만 Batch는 데이터를 실시간으로 처리하는 것이 아닌, 일괄적으로 모아서 처리하는 작업을 의미한다. 예컨데 하루동안 쌓인 데이터를 단 건으로 쿼리를 쓰는 것이 아닌 모아두었다 한꺼번에 처리하는 작업이다.

Fetch, Fetch는 기본적으로 조회의 의미를 가지고 있다. 넓게 보면 두가지 의미가 있는데 node에서는 특정 url로부터 data를 조회 및 넘겨받는 실시간 데이터 작업을 의미하거나 죄회 쿼리를 통해 data를 넘겨받는 과정을 의미한다. 우리가 다룰 의미는 후자이다.

본론

개발 환경 이야기를 먼저 해보자. Batch나 Fetch를 이용하여 여러 개의 데이터를 동시에 다루는 작업은 치명적인 약점이 있다. 동시간대에 다른 작업이 수행되고 있다면 쿼리 응답속도가 자연스럽게 느려지게 된다.

이런 경우는 업무량이 많은 시간대를 피해서 대규모 백업, 배치 작업이 실행되도록 SQL 스케줄을 조정해주는 것이 가장 간단한 방법일 것이다. 대표적인 예시가 은행 정산작업이다.

만약 반드시 동시간대에 실행되어야 하는 작업이라면 SQL server cpu 설정의 “max degree of parallelism(최대 병렬 처리 수준)”을 server cpu 갯수를 고려하여 재설정할 수 있다. 오버헤드가 많이 소비되는 작업을 한다할지라도 해당 작업이 모든 CPU를 점유하지 않도록 설정할 수 있다. 해당 sql를 튜닝하여 성능을 끌어올리는 것도 방법이 될 것이다.

데이터 갯수 이야기로 넘어와보자. 안타깝게도 직접 코드로 실습해보질 못해서 적절한 블로그 포스팅을 요약하여 서술하였다.

단순 반복 insert와 executeBatch()를 사용한 insert를 비교해보자.

아래는 단순 반복 insert의 슈도 코드이다.

```java for(VO v : getList) { test.setIndex(v.getIndex()); test.setItem(v.getItem()); insert(test); } ```

리스트에서 데이터를 하나씩 가져와 한 행씩 저장시키는 구조이다. 적은 양의 데이터는 상관없겠지만 15,000개의 데이터를 저장한 결과 193초(3분 13초)가 경과하였다. 1,000,000개의 데이터의 경우 22분 4초가 소요되었다.

원인은 1. connection의 잦은 호출, 2. 쿼리 전후로 이루어지는 부가적인 잔업의 횟수로 예상된다.

DB에 1,000,000개의 데이터를 INSERT 하는 상황을 생각해보자. DB에 1개의 데이터를 넣을때 N, 쿼리 전후에 M이라는 값이 소모된다고 가정해보자. 만약 쿼리 하나에 1개의 튜플을 INSERT 하게 되면, 소모값은 (1,000,000 * N + 1,000,000 * M)이다. 하지만 쿼리 하나에 1,000개의 튜플을 INSERT 하게 되면, 소모값은 (1,000,000 * N + 1,000 * M)이다.

executeBatch()를 사용한 상황은 addBatch()를 이용하여 쿼리를 메모리에 올려놓고 executeBatch()를 호출할 때 쿼리를 전송하는 방식을 사용한다.

```java import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; public void insertCode() { PreparedStatement pstmt = null; Connection con = null; try { String sql = "Insert Into table(index, item) " + "Values(?,?)" ; Class.forName("org.postgresql.Driver"); con = DriverManager.getConnection("jdbc:mysql://127.0.0.0/dbname","id","pw"); pstmt = con.prepareStatement(sql); List<VO> getList = getList(); int count = 0; for (VO v : getList) { ++count; pstmt.setInt(1, v.getIndex()); pstmt.setString(2, v.getItem()); pstmt.addBatch(); pstmt.clearParameters(); if((count%1000) == 0){ pstmt.executeBatch(); pstmt.clearBatch(); con.commit(); } } pstmt.executeBatch(); con.commit(); } catch(Exception e){ con.rollback(); } finally{ if(pstmt != null) pstmt.close(); if(con != null) con.close(); } } ```

`addBatch()`를 통해 메모리에 적재된 쿼리문을 1000개 단위로 배치작업을 실행하는 코드이다. 저 범위를 설정하지 않는다면 메모리에 올라가는 쿼리문이 OutOfMemory 에러를 뿜어낼 가능성이 있으므로 반드시 배치 범위를 적용해주어야 한다. 배치 사이즈를 조절하는 이유와 직접적으로 연관된 것도 OutOfMemory 에러 이슈일 것으로 짐작된다.

위 방식은 15,000개 데이터를 기준으로 60초 가량 소요되었고, 1,000,000개 데이터를 기준으로 7분 32초 소요되었다.

  | 단건 insert | batch insert -- | -- | -- 15,000개 | 193초 | 60초 1,000,000개 | 22분 | 7분

한 번에 가져오는 데이터의 갯수를 지정하기 위해선 Cursor와 Paging, 두가지 방법을 사용할 수 있다. 책에서 서술된 setFetchSize()는 Database의 cursor를 활용하는 방식이다. Cursor란 일련의 데이터에 순차 접근할 때 검색 및 현재 위치를 포함하는 요소이다. Fetch도 batch와 마찬가지로 메모리 사용량을 고려하며 데이터의 갯수를 지정해서 가져와야 한다.

```java Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/?useCursorFetch=true", "...", "..."); Statement stmt = conn.createStatement("select ... from ..."); stmt.setFetchSize(fetchSize); // ... ```

fetch size에 따른 시간 측정 결과는 다음과 같다.

fetch size | time(ms) -- | -- Integer.MIN_VALUE | 682 5 | 12367 10 | 6401 50 | 1632 100 | 1115 500 | 606 1000 | 527 2500 | 508 5000 | 464

결론

결국 메모리 사용량과 시간 사이에서 절충안을 찾는것이 중요하다는 것을 알 수 있었다. 좀 더 구체적인 사이즈 지정의 기준을 알아내지 못해서 아쉽다…

출처

https://chobopark.tistory.com/306

https://heowc.dev/en/2019/02/09/using-mysql-jdbc-to-handle-large-table-1/

https://blog.naver.com/nv2921/150046095641

https://fruitdev.tistory.com/111