Sign In

Node.js

Sharp 라이브러리 사용 시 메모리 이슈 해결하기
최근 서비스 운영 중 이유를 알 수 없는 메모리 점유율 상승(Memory Leak) 현상이 발생했습니다. 로컬 환경에서는 괜찮은데, 유독 리눅스 기반의 운영 서버(특히 컨테이너 환경)에서만 메모리가 계속 치솟다가 프로세스가 죽어버리는 현상이었죠. 오늘은 이 문제를 해결하기 위해 적용했던 설정과 그 배경에 대해 정리해 보려 합니다. 1. 문제는 '메모리 파편화'와 '캐시' Sharp는 내부적으로 C++로 작성된 libvips 라이브러리를 사용합니다. 이 덕분에 굉장히 빠르지만, Node.js의 가비지 컬렉션(GC) 범위 밖에서 메모리를 관리하는 부분이 있어 주의가 필요합니다. 특히 glibc를 사용하는 리눅스 환경(Debian, Ubuntu 등)에서는 기본 메모리 할당자(allocator)가 멀티 스레드 환경에서 메모리를 반환하지 않고 붙들고 있는 '메모리 파편화(Fragmentation)' 이슈가 고질적으로 발생하곤 합니다. 2. 해결을 위한 두 가지 설정 공식 문서와 여러 트러블슈팅 사례를 참고하여 서비스에 아래 두 가지 설정을 적용했습니다. sharp.concurrency(1) Sharp는 기본적으로 CPU 코어 수에 맞춰 멀티 스레드로 동작합니다. 하지만 앞서 언급한 리눅스의 glibc 환경에서는 스레드가 많아질수록 메모리 파편화가 심해지는 경향이 있습니다 공식 문서에서도 jemalloc 같은 별도의 메모리 할당자를 사용하지 않는 리눅스 환경이라면, concurrency를 1로 설정하는 것을 권장하고 있습니다. 스레드 수를 제한함으로써 불필요한 메모리 경합과 점유를 막아주는 것이죠. sharp.cache(false) Sharp는 성능 향상을 위해 처리한 이미지 데이터를 메모리에 캐싱합니다. 하지만 트래픽이 몰리거나 큰 이미지를 반복해서 처리해야 하는 서버 환경에서는 이 캐시가 오히려 독이 되어 메모리 부족(OOM)을 유발할 수 있습니다. sharp.cache(false)를 통해 캐시 기능을 끄면, 매 작업마다 사용한 메모리를 즉시 해제하도록 유도할 수 있어 메모리 사용량을 훨씬 안정적으로 유지할 수 있습니다. 적용 결과 설정 적용 후, 일정하게 상승하던 메모리 그래프가 안정적인 수준에서 유지되는 것을 확인할 수 있었습니다. 물론 concurrency를 1로 줄였기에 CPU 작업 효율은 조금 떨어질 수 있지만, 서버가 메모리 부족으로 다운되는 것보다는 훨씬 안전한 선택이었습니다. 아래는 전/후의 메모리 사용률 그래프입니다. (서로 다른 x,y축 스케일의 캡처입니다. 그래프의 양상만 참고해주세요) Before
  • 1more
Node.js 보안. 공홈의 Security Best Practices 문서 읽어보기
Security Best Practices 문서가 있어 읽어보았다. 신기한 내용만 추려보자면, Information Exposure through Timing Attacks (CWE-208) 값을 단순 비교(a === b)할 때, 문자열을 앞에서부터 한글자씩 비교하게 되면서 발생하는 시간차를 이용한다는 것 같다. 대체 뭘까 궁금해서 ChatGPT에게 설명을 요청해 받아낸 내용- - 서버가 클라이언트가 보낸 비밀(비밀번호, 토큰 등) 을 처리할 때 처리 시간이 입력값에 따라 달라지면 공격자가 여러 요청을 보내 응답시간을 통계적으로 비교하여 내부 비밀의 일부 정보를 유추할 수 있습니다. - 예: 문자열 단순 비교 (===)는 앞부분부터 비교를 하고 첫 불일치에서 바로 반환되므로 접두사가 맞을수록(비교가 길게 지속될수록) 응답 시간이 길어지는 경향이 있습니다. 공격자는 한 글자씩 맞춰가며(브루트포스/차례대로) 시간이 더 긴 입력을 고르면 정답에 가까운 문자임을 추측할 수 있습니다. - 특히 동일한 서버(혹은 같은 호스트)에서 타이밍 잡기가 쉬운 환경(공유 호스트, 로컬 네트워크, VM co-resident 등)에서는 성공 확률이 높습니다. - 또 한 가지 실수: 사용자가 없을 때와 있을 때 처리 시간을 달리하면(예: DB 조회 후 곧바로 실패 반환 vs 존재하면 해시 비교) 사용자 존재 여부 자체를 노출시킬 수 있습니다. 이를 이용한 정보 수집도 위험합니다. GPT가 제시해준 해결책 - 길이가 고정된(또는 비교 전에 고정 길이로 변환한) 바이너리 값들을 비교하고, crypto.timingSafeEqual을 사용해 비교한다. - 비밀번호는 평문이 아니라 KDF(scrypt, bcrypt, argon2 등) 로 처리한 고정 길이 해시(또는 키)를 저장하고 비교한다. - 사용자 존재 여부에 따라 처리 시간을 달리하지 않도록 — 사용자가 없을 때도 동일한 KDF 연산(더미 값)이나 timing-safe 비교를 수행해 시간 차를 줄인다. - 비교 전에 반드시 두 버퍼 길이가 같은지 확인(다르면 길이를 맞추거나 실패 처리) — timingSafeEqual은 길이가 다르면 예외가 날 수 있음. - 브랜칭(조건문)으로 비밀값을 분기하지 않도록 주의. Prototype Pollution Attacks (CWE-1321) javascript 내장 객체의 prototype 내용물을 바꿔치는 과정에 문제가 될 수 있었나보다. lodash에만 해도 CVE에 관련 취약점이 이미 몇 건 씩이나 보고되어 있다. 당연히 바뀔수있는 값 아닌가? 생각했는데, GPT가 아래와같은 사례일수 있다고 알려주었다. 공격자가 {"__proto__": {"isAdmin": true}}를 보내면 결과적으로 Object.prototype.isAdmin === true가 되고, 애플리케이션이 if (user.isAdmin) 같은 체크를 하면 공격자가 권한을 획득할 수 있습니다.
  • 1more
--max-old-space-size 옵션 관련
node.js의 —max-old-space-size 기본 값 확인 $ node > v8.getHeapStatistics() { total_heap_size: 6578176, total_heap_size_executable: 262144, total_physical_size: 6815744, total_available_size: 2193448312, used_heap_size: 5277544, heap_size_limit: 2197815296, malloced_memory: 163968, peak_malloced_memory: 172528, does_zap_garbage: 0, number_of_native_contexts: 2, number_of_detached_contexts: 0, total_global_handles_size: 8192, used_global_handles_size: 3040, external_memory: 2269138 } 기본값이 2GB인 것으로 확인함. 참고: https://stackoverflow.com/questions/48387040/how-do-i-determine-the-correct-max-old-space-size-for-node-js 수정 node.js 공식문서의 추천에 따르면, 서버 메모리 2GB 당 1536MB로 설정하라고 한다. pm2의 ecosystem 파일에 옵션을 추가하기위해, 파일을 아래와 같이 변경. { ... node_args: '--max-old-space-size=1536', ... }
  • 1more
PM2 필요했던 명령어들
환경변수 수정 후, 수정된 변수로 pm2를 다시 띄움 ** 주의할 점 ecosystem.config.js 내부에 설정값 (예를들면, "error_file" 값)을 수정한 후, 위 방법으로 재시작만 한다면 변경된 설정값이 적용되지 않는다. (모든 설정이 그런지, 로그 관련만 그런지는 모르겠다) 변경된 설정을 적용하려면, 기존 인스턴스를 정지&삭제 후 재시작해야 적용되었다. $ pm2 stop {{name}} $ pm2 delete {{name}} $ pm2 start ecosystem.config.js —only {{phase}} 적용된 환경변수 목록 조회 restart 카운트 초기화 pm2 프로세스를 죽였다 살리기(app 상태 유지)
  • 1more
AWS Opensearch Service에 bulk 작업에 timeout이 발생한다.....
Opensearch에 bulk요청을 할 때, timeout이 발생한다. 왜인지 알수 없어 주위에 물어보니, 보통 커넥터에 요청 데이터의 크기 제한이 있고(ES 클라이언트에선 http.max_content_length 옵션으로 설정하나보다), 그것보다 큰 데이터 요청을 보내면 타임아웃이 날 때가 있다고 한다. opensearch는 어떻게 되는지 찾아보니, 인스턴스 타입별 미리 정해진 네트워크 용량 제한이 있다고 한다. 지독한놈들.. es index작업이나 bulk에서 이유모를 timeout이 발생한다면, 사용중인 instance의 타입과 타입별 용량제한을 확인해보자.
  • 1more
Made with Slashpage