배경
- 업무 중에, 서버의 Disk Full 연락을 받았다.
- 기존 개발해둔 API에서 OOM (Out Of Memory)가 계속 발생하여, core-dump가 계속 생성되어서 Disk Full이 발생.
Free disk space < 3% on
해결과정
df, du 명령어 차이점과 차이 발생 시 해결방법
df와 du란 무엇인가? Linux에서 제공하는 두 가지 중요한 디스크 공간 확인 도구인 df와 du에 대해 알아보겠습니다. df 명령어는 리눅스 시스템 전체의 디스크 사용량을 확인할 수 있는 도구입니다
support.bespinglobal.com
리눅스 명령어 (df - 리눅스 시스템 전체의 (마운트 된) 디스크 사용량 , du - 특정 디렉토리 기준 디스크 사용량 확인) 사용하여 어느 디렉토리에서 공간을 많이 차지하는지 확인함.
문제가 되는 디렉토리는 core-dump가 생성되는 디렉토리였고 해당 디렉토리 제거함으로 해결함.
core-dump가 생기는 원인은 기존 개발되어있는 API를 다른 서비스에서 이용시 MongoDB OOM (Out Of Memory)가 발생하기 때문.
OOM이 발생한 원인은 API에서 로그정의서 History collection (로그정의서 수정 내역을 저장하는 colleciton) 을 긁어올 때 특정 로그정의서에 대해 데이터 크기가 너무 커서 발생.
API 수정하지 않으면 core-dump로 인한 Disk Full 문제가 계속 발생하기 때문에 이 부분 수정이 필요했음.
문제 분석
에러 로그
<--- Last few GCs ---> [34315:0x7f7f50008000] 109780 ms: Mark-sweep 1391.1 (1416.3) -> 1391.1 (1416.3) MB, 42.2 / 0.0 ms (average mu = 0.736, current mu = 0.007) last resort GC in old space requested [34315:0x7f7f50008000] 109820 ms: Mark-sweep 1391.1 (1416.3) -> 1391.1 (1416.3) MB, 39.4 / 0.0 ms (average mu = 0.583, current mu = 0.001) last resort GC in old space requested <--- JS stacktrace ---> ==== JS stack trace ========================================= 0: ExitFrame [pc: 0x109ce3267901] Security context: 0x177a86a1e6e9 <JSObject> 1: stringSlice(aka stringSlice) [0x177ad5c13571] [buffer.js:~589] [pc=0x109ce326a52f](this=0x177a05f826f1 <undefined>,buf=0x177a402b87f9 <Uint8Array map = 0x177a0edd60b1>,encoding=0x177a86a3ec99 <String[4]: utf8>,start=2814348,end=4192904) 2: toString [0x177a1a520b31] [buffer.js:~643] [pc=0x109ce3267baa](this=0x177a402b87f9 <Uint8Array map = 0x177a0e... FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory 1: 0x10003d035 node::Abort() [/Users/1004780/.nvm/versions/node/v10.16.3/bin/node] 2: 0x10003d23f node::OnFatalError(char const*, char const*) [/Users/1004780/.nvm/versions/node/v10.16.3/bin/node] 3: 0x1001b8e15 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/Users/1004780/.nvm/versions/node/v10.16.3/bin/node] 4: 0x100586d72 v8::internal::Heap::FatalProcessOutOfMemory(char const*) [/Users/1004780/.nvm/versions/node/v10.16.3/bin/node] 5: 0x100590274 v8::internal::Heap::AllocateRawWithRetryOrFail(int, v8::internal::AllocationSpace, v8::internal::AllocationAlignment) [/Users/1004780/.nvm/versions/node/v10.16.3/bin/node] 6: 0x100562064 v8::internal::Factory::NewRawTwoByteString(int, v8::internal::PretenureFlag) [/Users/1004780/.nvm/versions/node/v10.16.3/bin/node] 7: 0x100561ca9 v8::internal::Factory::NewStringFromUtf8(v8::internal::Vector<char const>, v8::internal::PretenureFlag) [/Users/1004780/.nvm/versions/node/v10.16.3/bin/node] 8: 0x1001db1b8 v8::String::NewFromUtf8(v8::Isolate*, char const*, v8::NewStringType, int) [/Users/1004780/.nvm/versions/node/v10.16.3/bin/node] 9: 0x1000e8822 node::StringBytes::Encode(v8::Isolate*, char const*, unsigned long, node::encoding, v8::Local<v8::Value>*) [/Users/1004780/.nvm/versions/node/v10.16.3/bin/node] 10: 0x100056889 void node::Buffer::(anonymous namespace)::StringSlice<(node::encoding)1>(v8::FunctionCallbackInfo<v8::Value> const&) [/Users/1004780/.nvm/versions/node/v10.16.3/bin/node] 11: 0x109ce3267901
문제가 되는 로직
- 특정 로그정의서의 History (수정 이력) find로 전체 다 불러와서 createdAt 필드를 비교해가며 가장 최신 수정이력을 찾아내는 것.
- find시에 OOM이 발생.
https://node-js.tistory.com/40
MongoDB에서 효율적으로 페이징 처리하기(pagination)
MongoDB Pagination MongoDB에서 페이징을 처리하는 방법은 여러 가지가 있습니다. 효율적으로 처리하기 위해 했던 고민들을 공유합니다. 개발 환경 Node.js(Express) mongoDB(Mongoose) ❌ Skip, Limit 처음 적용했
node-js.tistory.com
처음에는 페이징 (대량의 데이터를 한꺼번에 가져오는 대신, 페이지 단위로 데이터를 나누어 처리) 과 Projection (필요한 필드만 선택)을 이용함.
이 때, OOM 문제가 발생하지는 않았지만, API 응답 속도가 너무 느렸음

담당자분과 논의해보고 createdAt 관련 필드를 빼도 상관없어서, 관련 로직을 제거해서 배포하긴했지만, 만약 사용해야 한다면 어떤 방식이 더 적절할 지 고민해보며 테스트했다.
페이징을 사용한다는 전제하에 Index를 추가해보자
사용하는 쿼리는 lid라는 필드를 조건절로 사용하기 때문에, lid에 대해 인덱스를 추가했다.
index를 추가하면 58초 -> 30초로 성능이 개선된다. (50프로 감소!)

하지만 API하나에 30초씩 태우는건 말도 안된다.
Projection으로 필드 하나만 가져오기 때문에 limit를 좀 더 높여봐도 괜찮다는 생각이 들었다. (메모리 사용량 체크하면서)
limit 100 -> 500 으로 증가시 30초 -> 15초로 성능이 개선된다. (50프로 또 감소!)

10초 안짝으로 끊을 수 있을 것 같다. 메모리 사용량도 200mb 언더로 계속 유지하고 있다.
limit 500 -> 1000으로 증가시 15초 -> 8초로 성능이 개선된다. (대략 50프로 또 감소!!)

limit를 1000 -> 1500으로 늘렸을 때는 메모리 사용량이 200mb를 넘어가기도 하고 7.5초로 성능 개선도 의미있지 않아서 적절 limit값을 1000으로 유지하기로했다.

메모리 사용량 체크 코드
server의 app.js에 다음과 같이 주기적으로 메모리 사용량을 체크함.
각 필드의 의미는 다음을 참고.
https://nodejs.org/api/process.html#processmemoryusage
Process | Node.js v22.8.0 Documentation
Process# Source Code: lib/process.js The process object provides information about, and control over, the current Node.js process. import process from 'node:process';const process = require('node:process');copy Process events# The process object is an inst
nodejs.org
setInterval(() => {
const memoryData = process.memoryUsage();
const memoryUsage = {
'Metric': [ 'RSS', 'Heap Total', 'Heap Used', 'External' ],
'Value': [
`${ (memoryData.rss / 1024 / 1024).toFixed(2) } MB`,
`${ (memoryData.heapTotal / 1024 / 1024).toFixed(2) } MB`,
`${ (memoryData.heapUsed / 1024 / 1024).toFixed(2) } MB`,
`${ (memoryData.external / 1024 / 1024).toFixed(2) } MB`
]
};
console.table(memoryUsage);
}, 1000);
'Programming > Backend' 카테고리의 다른 글
MongoDB에서 Index (B-tree) (0) | 2025.01.10 |
---|---|
Nest.js 도입 및 느낀점 (Express와 비교) (1) | 2023.01.29 |