NodeJS + Express 의 잔여 메모리와 응답시간
작년 초 지인의 소개로 모 인디게임의 서버 프로그램을 개발하였습니다.
서버의 주 역할은 주기적으로 (짧게는 몇 초부터 길게는 5분) 사용자의 게임 데이터를 서버로 전송해 운영팀이 게임 상황을 파악할 수 있게 해주거나 사용자 차단, 일일 보상 체크 등의 간단한 역할만 수행하고 있습니다.
처음에는 사용자의 수가 적었기 때문에 10 rps (초당 요청 수) 정 도로 아무런 문제가 없었지만 최근 몇 달간 사용자가 급증하기 시작하면서 평균적으로 40rps 를 유지하고 있으며 피크타임 또는 공격을 받을 때는 80 rps까지 올라가는 상황이 벌어지고 있습니다.
이와 동시에 응답시간이 초기에는 1 ~ 2s 미만으로 쾌적한 모습을 보여주었지만 요즘 들어 자주 9~ 15s까지 올라가는 모습을 보고 원인을 파악하고 해결해야 할 필요성이 느껴져서 일주일간의 데이터를 분석하기로 했습니다.
처음에는 피크시간대의 요청 증가로 인해 발생하는 현상이라고 생각했지만, 결과는 전혀 달랐습니다.
피크 시간대에는 1개의 서버 인스턴스가 하나 더 추가되는 것을 고려 하더라도 평소와 성능 차이는 크지 않았으며 오히려 피크 이외의 시간대에 성능 저하가 발생하는 경우가 더 많았습니다.
이상하게 생각되어 해당 시간대의 데이터를 집중적으로 분석하던 중 사용자 증가로 메모리 사용량 초과가 지속해서 발생하였고 PM2의 cluster 수를 2에서 1로 조정하게 되었는데 그 순간 서버의 성능이 개선되었고 성능 저하의 원인을 메모리 사용량을 중점으로 찾아보았습니다.
관찰 결과 PM2 클러스터의 수를 1로 지정했을 때 최초 실행 시 서버의 메모리 사용량은 약 250MB이며 특정 시점부터 메모리 사용량이 증가하면서 384MB까지 증가하였고 이 구간에서 메모리 사용량이 증가할수록 응답시간도 증가하는 것을 확인할 수 있었습니다.
따라서 성능 저하의 문제는 메모리와 관계가 있다는 결론을 내릴 수 있었고 다음과 같은 가능성을 고려하여 자료를 찾아보았습니다.
어디선가 메모리 누수가 진행 중이다. 서버의 메모리가 부족하다 (최대 512MB) 비효율적인 로직이 존재한다.
이 중 2번은 현재 1개의 서버 인스턴스 당 1개의 cluster만 돌아가고 있으며 메모리 사용량 모니터링 결과 384MB에 도달하면 cluster instance를 재시작하게 설정한 상황에서도 처리량에는 큰 문제가 없었기 때문에(재시작될 때 해당 인스턴스가 처리 중인 요청은 누락되긴 합니다.) 제외하였으며
3번은 로직이 복잡하지 않고 최근 다음과 같은 작업이 게임 쪽에서 진행 중이라 제외하였습니다.
사용하지 않는 API 제거 out of dated 된 모듈, DB 등의 마이그레이션 새 스펙에 맞게 서버 및 클라이언트 재개발
결국 중점적으로 찾아보게 된 건 1번 메모리 누수 문제였고 다음과 같은 내용을 알게 되었습니다.
Node.js 는 64bit에서는 기본으로 1.4GB를 메모리 한계로 잡는다. 그래서 메모리가 1.4GB 이하인 환경에서는 메모리 제한이 필요하다. V8의 GC 역시 메인 스레드에서 진행되며 사양보다 과도한 요청을 처리하느라 GC가 끼어들 틈이 없다. 잘못된 요청으로 인한 오류가 제대로 처리되지 못하고 구천을 떠돌고 있다.
이 중 2번의 경우에는 순간적으로 80rps로 급상승할 때도 모니터링 결과 GC는 꾸준히 수행 중에 있었으며 메인 스레드의 event loop는 평소 10% 미만의 사용률을 보이고 있었습니다. 따라서 1번과 3번의 해결책을 시도해보았습니다.
1번의 해결책
node --max-old-space-size=512 server.js
PM2의 경우 다음과 같이 설정하면 된다고 합니다.
{ "apps": [ { "node_args": "--max_old_space_size=512" } ] }
3번의 해결책
app.use(function(err, req, res, next) { // handle error });
하나씩 적용해보면서 결과를 관찰하였는데 3번의 해결책의 경우 의외로 큰 성능 개선을 보여주었습니다. 흔히 툴키드라고 불리는 악성 사용자들의 비정상적인 요청이 처리하지 않은 오류를 발생시켰고 제대로 처리되지 않아 한동안 서버 내부에서 고아 상태가 되었던 것 같습니다. 이러한 부분은 다음 버전부터는 express-async-errors 모듈 등을 통해 전역적으로 오류를 처리하는 방향으로 결정하였습니다.
1번의 해결책의 경우에는 큰 효과는 못 보고 있는데 PM2에서 재시작하는 부분도 있어서 좀 더 지켜봐야 할 것 같습니다. 다만 메모리 사용량이 이전보다는 완만하게 증가하는 경향을 보이는데 해당 옵션 때문인지는 판단이 잘 안 되고 있습니다.
최종적으로 서버의 응답시간은 다음과 같이 변했습니다.
과거
Ratio Response Time MAX 30s 99% 13s 95% 7s 50% 0.7s
현재
Ratio Response Time MAX 17s 99% 5s 95% 2s 50% 0.2s
(개선된? 상태)
RPS는 약 10 정도 더 늘어났으며 피크시간대에도 더 나은 성능을 보여주기 시작했습니다.
다만 이 수치는 해당 기간 사용자 수가 20% 이상 증가하여 서버 인스턴스를 평균적으로 1개 정도 추가하였음을 고려하고 보시면 감사하겠습니다.
다음에 좀 더 성능 개선을 하면 어느 정도까지 성능개선이 될지 궁금하네요.
참고
Node.JS ( & pm2 ) Process Memory Limit
Production best practices: performance and reliability
Node.js 최적화, 메모리관리를 위한 flag
nodejs 메모리 누수
Static Memory Javascript with Object Pools
Memory Leaks in NodeJS | Quick Overview
from http://kyeonjung.tistory.com/29 by ccl(A) rewrite - 2020-03-07 11:20:15
댓글
댓글 쓰기