슬기로운 개발생활

대용량 엑셀 다운로드 OOM(Out Of Memory) 해결 과정

by coco3o
반응형

서버가 갑자기 먹통이 된 경험을 했다.
운영 중인 서버의 CPU 사용률이 급격히 상승하며 서버가 죽어버렸다.
서버를 즉시 재가동하고 원인을 확인한 결과, OOM(Out Of Memory)으로 인해 서버가 중단된 것이었다.
 

왜 OOM(Out Of Memory)이 발생했는가?

관리자 페이지에서 엑셀 다운로드를 하는데 약 N만 건 이상의 Row를 다운로드하는 과정에 OOM이 발생했다.
현재 운영 중인 서버는 1개의 WAS 안에 서비스, 관리자 등 여러 프로젝트가 함께 돌아가고 있는 상황이다.
여러 요청에 대한 처리와 N만 건 이상의 Row를 다운받는 요청이 겹치면서 과부하가 발생한 것으로 보인다.
하지만, N만 건 정도의 데이터를 처리하면서 OOM이 발생하는 것은 이상하다고 생각하여 원인을 찾아보았다.

원인 파악

현재 엑셀 다운로드를 위해 Apache POI 라이브러리의 XSSFWorkbook을 사용하고 있는데,
문제의 원인은 XSSFWorkbook에서 확인되었다.

XSSFWorkbook 메모리 누수

  • 메모리 효율성의 부족
    • XSSFWorkbook 는 엑셀 파일 전체가 만들어질때까지 데이터를 메모리에 상주시키는 방식이다.
    • 큰 데이터셋을 다룰 경우 상당한 양의 메모리를 소비하며, 서버의 메모리 용량에 따라 OOM(Out Of Memory) 에러가 발생할 수 있다.
  • 대용량 데이터 처리시 성능 저하 
    • 작은 데이터셋에서는 문제가 되지 않지만, 수만 또는 수백만 데이터의 대용량 파일을 처리할 때 처리 속도가 현저히 느려지며 성능이 크게 떨어진다.
  • 가비지 컬렉션 부담
    • 대용량 파일을 처리하는 동안, XSSFWorkbook은 많은 수의 임시 객체를 생성할 수 있다.
    • 이는 가비지 컬렉터에 부담을 주며, 시스템의 전체 성능에 영향을 줄 수 있다.

 

문제 해결 방법

1. 서버 메모리 증설하기 (돈으로 찍어 누르기💰)

사실 이 방법이 제일 쉬운 방법이다.
OOM이 발생한 가장 직관적인 이유는 메모리가 부족한 것이다.

현재 운영 중인 서버의 메모리는 8GB 밖에 되지 않는다...😂

서버의 메모리를 증설하여 스케일업 하면 해결될 일이다.
하지만, 데이터가 계속 증가할 것을 고려하면 메모리 증설만으로는 근본적인 해결책이 될 수 없다.
 

2. 서버 내 여러 프로젝트 분리하기

현재 운영 중인 서버는 하나의 톰캣 서버에 여러 프로젝트가 돌아가고 있는 One To Many 구조이다.
이 프로젝트들을 각각 독립된 서버로 분리하는 방법도 고려해 봤지만,
서버 비용과 들어갈 작업의 양이 많아 당장 진행하기엔 어려움이 있었다..
 

3. SXSSFWorkbook 사용

한정된 내부 자원 안에서 해결할 수 있는 방법이 없을까 고민하였고,
엑셀 다운로드를 지원하는 Apache POI 라이브러리 중 SXSSFWorkbook를 찾을 수 있었다.
 
SXSSFWorkbook은 지정한 Row만큼만 메모리에 로드하여 처리하다가 지정한 Row에 도달하면
임시 XML 파일로 디스크에 기록하고 메모리를 비워주는 스트리밍 방식이다.
즉, SXSSFWorkbook은 메모리에 있는 데이터를 디스크로 옮기면서 처리하기 때문에 메모리를 적게 먹어 대용량 데이터 처리에 매우 적합하다.

비록 디스크에 플러시한 Row는 다시 접근 할 수 없는 단점도 있지만, 현재 상황에서는 큰 문제가 되지 않았다.

너로 정했다!


아래와 같이 인자 값을 지정해주면 설정한 row 수만큼 작성 후 알아서 flush 해준다.🙂

// auto disk flush
// 메모리에 1000개 rows를 유지하고, 초과 rows는 disk에 auto-flush
SXSSFWorkbook wb = new SXSSFWorkbook(1000);

// 인자 값 없이 생성시 default 100 
SXSSFWorkbook wb = new SXSSFWorkbook();

// auto-flushing 끄기 (모든 rows memory 유지)
SXSSFWorkbook wb = new SXSSFWorkbook(-1);

아래와 같이 flushRows 메서드로 데이터를 수동으로 디스크로 옮길 수도 있다.

// manually control
if(rownum % 100 == 0) {
    // 마지막 100개 rows를 제외한 all rows flush
    ((SXSSFSheet)sh).flushRows(100); 
    
    // 모든 rows disk flush
    ((SXSSFSheet)sh).flushRows(); // is a shortcut for ((SXSSFSheet)sh).flushRows(0)
}
wb.write(stream);
wb.close();
wb.dispose(); // 디스크에 저장한 임시파일 삭제

 작업 완료 후엔 디스크에 저장된 임시파일을 삭제해줘야 한다.

 XSSFWorkbook을 SXSSFWorkbook 라이브러리로 사용하도록 변경했다.

개선 전후 비교하기

로컬에서 약 10만 건의 데이터를 추출하는 API를 10회 연속으로 호출하는 테스트를 진행했으며,
VisualVM을 사용해 메모리 변화를 모니터링했다.

AS-IS

 

개선 전에는 요청을 처리하다가 서버가 죽었고,

TO-BE

개선 후에는 서버가 안정적으로 작업을 처리했다.
메모리 사용량 그래프도 전과 비교하면 상당히 안정적이며 기존보다 약 80%정도 절감되었다. 😀
메모리 효율성이 향상되면서 다운로드 속도도 크게 개선됐으며,
10만 건의 데이터를 다운로드하는 데 77초에서 5초로 단축되어, 약 1400% 이상 성능이 개선되었다.🔥
 

라이브러리를 변경하면서 엑셀 다운로드 코드 베이스 구조의 문제점도 몇 가지 보여 나중에 전반적으로 개선 후 다시 블로그에 정리해야겠다.

반응형

블로그의 정보

슬기로운 개발생활

coco3o

활동하기