대용량 데이터 다운로드시 oom 방지 가이드

웹서비스를 하다보면 데이터를 조회하고, 그데이터를 일괄 다운로드하는 니즈가 생긴다.(특히 어드민)
이런경우 서버 성능에따라 oom(out of memory)이 발생할 수 있다.
자주 생기는 상황으로는 대량의 데이터를 조회한 다음, 이를 엑셀파일로 내려받는경우가 있다.
이 경우를 예시로 원인과 해결법을 알아보고자 한다.

원인

  1. db에서 데이터를 가지고와서 객체로 들고있는 단계에서 OOM
  2. workbook을 생성해 데이터체우다가 OOM
  3. 메모리에 담고있는것을 전송하는 단계까지 메모리를 계속 holding! - OOM 확률 up!

개선포인트

A. db에서 한번에 가지고오는 데이터 크기(fetch size)를 조절한다
B. db에서 가지고온 데이터를 통체로 들고있지 않고 나누어 처리한다.
C. 엑셀에 내용을 체우다가 일정 사이즈 이상 되면 Disk에 쓰도록 한다 (기존에도 되어있음)
D. 임시파일은 바로바로 삭제하도록한다 (삭제를 명시적으로 하지 않으면 tomcat 내려갈때 삭제됨. 강제종료시는 삭제 안될 가능성)
E. 다운로드 중복클릭 방지

해결법

(1) -> (A),(B)
(2),(3) -> (C)
부가적인 개선사항 : (D), (E)

요점

개선포인트에서 밝힌것처럼, 메모리를 효율적으로 사용하기 위한 방법들을 적용한다.

  1. db에서 데이터를 가지고오는 부분 (read)
  2. 저장하는 부분 (write)

read 와 write가 전체데이터가아닌 부분에 적용되어야하고
read된 데이터를 write 하고나서, 다음데이터를 처리하는 방식으로 진행해야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public SXSSFWorkbook createDownloadFile(R request) {
Page<T> result;
SXSSFWorkbook workbook = initializer.initialize(flushSize);
Pageable pageable = PageRequest.of(0, defaultPageable.getPageSize());

try {
do {
result = reader.read(request, pageable);
writer.write(workbook, result.getContent());
pageable = pageable.next();
} while (result.hasNext());
} catch (Exception e) {
workbook.dispose();
throw e;
}

return workbook;
}

아래부터는 실제 적용 예시를 소개한다.

예시

환경

  • spring
  • mybatis
  • poi
  • apache
  • tomcat

fetch size를 조절 & result handler 사용하는방법

mybatis 를 쓰는경우 ResultHandler 를 쓸 수 있음.
이 ResultHandler 안에서 apache poi 를 활용하여 다운로드 하도록 설정

Service

1
2
3
4
5
List<Object> getObjects(Object object) {
YourResultHandler resultHandler = new YourResultHandler();
yourMapper.getObjects(object, resultHandler);
return resultHandler.getResults();
}

Dao

1
2
3
public interface YourDao {
void getObjects(@Param("param1") Object object, ResultHandler handler);
}

mapper.xml

fetch size 가 작을수록 OOM 발생가능성은 낮아지지만, 성능하락이 있음.
조회되는 데이터의 크기와,쿼리의 속도를 고려해서 fetchSize를 고려해야한다.

1
2
3
<select id="SELECT_TABLE" parameterType="Object" fetchSize="100" resultType="List">
SELECT * FROM TABLE WHERE NAME = #{object.value}
</select>

apache poi 사용

엑셀 파일 데이터 생성시 임시 file 을 생성하는 방식임

1
Workbook wb = new SXSSFWorkbook(1000);

이후 임시 파일을 바로 삭제

1
((SXSSFWorkbook)wb).dispose();

excel file을 생성하지 않고 response로 바로 전송 (?)

엑셀파일을 만든다움 클라이언트에 전송하게되면 파일크기만큼 메모리를 사용한다.

+ 추가적으로 연속클릭을 방지하도록 작성하기 (jquery.fileDownload)

1
2
3
4
5
6
7
8
public void download(HttpServletResponse response, SXSSFWorkbook sxssfWorkbook) {
String fileName = "data" + new SimpleDateFormat("yyyyMMdd").format(new Date()) + ".xlsx";

response.setHeader("Set-Cookie", "fileDownload=true; path=/");
response.setHeader("Content-Disposition", String.format("attachment; filename=\""+fileName+"\""));

sxssfWorkbook.write(response.getOutputStream());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div>
<input type=button id="insert" value="insert">
<input type=button id="download" value="download">
<input type=button id="download2" value="download2">
</div>

<!-- progressbar -->
<div title="Data Download" id="preparing-file-modal" style="display: none;">
<div id="progressbar" style="width: 100%; height: 22px; margin-top: 20px;"></div>
</div>

<!-- error -->
<div title="Error" id="error-modal" style="display: none;">
<p>생성실패.</p>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$("#download2").on("click", function () {
var $preparingFileModal = $("#preparing-file-modal");
$preparingFileModal.dialog({modal: true});
$("#progressbar").progressbar({value: false});
$.fileDownload("/data/test/download2.json", {
successCallback: function (url) {
$preparingFileModal.dialog('close');
}, failCallback: function (responseHtml, url) {
$preparingFileModal.dialog('close');
$("#error-modal").dialog({modal: true});
}
});
return false;
});

실험

실험방법

  1. 테스트 페이지 작성
    • insert : 데이터넣기
    • download : 기존로직
    • download2 : 개선로직


2. data insert
- 약 40만건 데이터 수동으로 넣음
-

  • before / after 비교 할 수 있는 test api 2개 작성
  1. 각버튼을 연속으로 5회씩 눌러 메모리 변화테스트 (테스트를위해 중복클릭 허용)
  2. 메모리 변화 모니터링은 VisualVM

기타정보

Object size : 246M
excel file size : 43M

비교

memory

before

after

비교

  • 개선전
    • 2번째 GC구간을 보면 메모리가 감소하지 않는 영역이있다. (약1.8G)
    • 요청이 몰리거나 데이터가 더 커지면 충분히 OOM이 발생 할 수 있다.
  • 개선후
    • GC를 하는구간을 보면 메모리를 거의 사용하지 않는것을 확인 할 수 있다.
    • 심지어 GC하기전에도 1.5G이상 메모리가 올라가지않는다.

temp files

before

  • 다운로드 후 파일이 계속 남아있음
  • GC를 하면 사라짐
  • 강제종료시 계속 남아있음.

after

  • 다운로드 후 바로 삭제됨.

참고자료

http://dymn.tistory.com/20
http://dreamsea77.tistory.com/221
https://offbyone.tistory.com/70
http://jaycee-p.tistory.com/12
http://www.docjar.com/html/api/org/apache/poi/xssf/usermodel/examples/BigGridDemo.java.html

(보충할 부분이나 잘못된부분이 있다면 연락주시기 바랍니다.)

Comments