기본 콘텐츠로 건너뛰기

NodeJS + Express 의 잔여 메모리와 응답시간

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

댓글

이 블로그의 인기 게시물

[Mac OS X] 데스크탑 응용프로그램 개발시작

[Mac OS X] 데스크탑 응용프로그램 개발시작 현재 회사에서 다음 프로젝트로 Mac OS X를 개발해야 하는 일정이 생겨 난생 처음 데스크탑 응용프로그램을 개발하기 공부를 시작했고 여러가지 플랫폼이 있다는것을 발견했다. 어떤 플랫폼을 사용해야할 지 먼저 고민해보자. 무엇으로 개발할것인가? Xcode & Swfit or Objective-C Mac OS X 는 objective-c를 기반으로 만들어졌다. apple에서 제공하는 Xcode와 objective-c 혹은 Swift로 응용 프로그램을 만들 수 있다. Electron Electron 프레임워크를 사용하면 javascript, HTML, CSS를 사용하여 크로스 플랫폼 데스크탑 응용 프로그램을 만들 수 있다. react-native-desktop Qt 프레임 워크를 기반으로하는 크로스 플랫폼이다. react-native 와 javascript로 응용 프로그램을 만들 수 있다. Qt는 컴퓨터 프로그래밍에서 GUI 프로그램 개발에 널리 쓰이는 크로스 플랫폼 프레임워크. 각 플랫폼의 장단점은 무엇인가? Xcode & Swfit or Objective-C - 장점 모든 OSX API에 직접 접근할 수 있다. Xcode를 통해 App Store에 쉽게 배포할 수 있다. - 단점 Windows 전용 응용 프로그램을 다시 개발해야한다. Swift언어를 모른다면 새로 공부해야 한다. Electron - 장점 웹스택을 가지고 빠르게 만들 수 있다. 다양한 플랫폼을 지원한다. - 단점 Native언어로 개발할때 보다 성능이 떨어질 이슈가있다. OS X API를 사용하는데 복잡하다. *API에 대한 JavaScript 래퍼가 없으면 자체 래퍼를 작성하지 않는 한 프로젝트에서 사용할 수 없습니다. React-native-desktop - 장점 현재 개발스택과 완전히 동일하므로 더 빠르게 개발 할 수 있다. 다양한 플랫폼을 지원한다. - 단점 N...

AWS CI/CD 파이프라인에 Slack 알람 적용(Lambda, CloudWatch Events 연동)

AWS CI/CD 파이프라인에 Slack 알람 적용(Lambda, CloudWatch Events 연동) 이번 글에서는 구축된 CI/CD 파이프라인과 Slack 알람을 연동하는 방법에 대해서 알아볼 것이다. CI/CD 파이프라인 시작/종료(성공/실패) 시 Amazon CloudWatch Events에서 해당 이벤트를 감지한 후 AWS Lambda로 트리거 신호를 보내면 AWS Lambda에서 Slack채널로 알람을 보내는 프로세스다. 글의 순서는 다음과 같다. Amazon CloudWatch Events, AWS Lambda란? 실습 전 준비사항 Slack Webhook 생성 AWS Lambda 함수 생성 AWS CloudWatch Events 생성 테스트 위와 같은 방식으로 구축을 하면 최종 프로세스는 다음 그림과 같다 (우리가 이 글에서 진행하는 내용은 빨간색으로 표시된 부분) 1. Amazon CloudWatch Events, AWS Lambda란? 1-1. Amazon CloudWatch Events란? Amazon CloudWatch Event(혹은 AWS CloudWatch Events)는 AWS 상태 변경 등을 감시할 수 있는 기능이다. 만약 사용자가 만든 규칙에 맞는 이벤트가 발생하게 되면 해당 이벤트가 사정에 정의된 규칙과 일치할 경우 하나 이상의 대상 작업을 호출한다. 이벤트 유형에 따라 알람을 보내거나, 이벤트 정보를 캡쳐하거나, 교정작업을 수행하거나, 이벤트를 시작하거나, 기타 작업을 수행할 수 있다. 이벤트가 발생할 경우 대상 작업은 AWS Lambda함수, AWS Kinesis 스트림, AWS SQS, Amazon SNS 등이 있다. 사용되는 사례로는 1) 이벤트가 발생하면 Lambda함수를 사용하여 Slack채널로 알림을 전달하거나, 2) AWS 상태 이벤트가 발생하면 Lambda 및 CloudWatch Events를 사용하여 Amazon SNS로 사용자 지정 텍스트 또는 SMS 알림을 보내는 등...

[2020-angstromCTF] web - A peculiar query write-up

[2020-angstromCTF] web - A peculiar query write-up English write-up UI seems like below. If you click the 'the source' link, you can get back-end source code. const express = require("express"); const rateLimit = require("express-rate-limit"); const app = express(); const { Pool, Client } = require("pg"); const port = process.env.PORT || 9090; const path = require("path"); const client = new Client({ user: process.env.DBUSER, host: process.env.DBHOST, database: process.env.DBNAME, password: process.env.DBPASS, port: process.env.DBPORT }); async function query(q) { const ret = await client.query(`SELECT name FROM Criminals WHERE name ILIKE '${q}%';`); return ret; } app.set("view engine", "ejs"); app.use(express.static("public")); app.get("/src", (req, res) => { res.sendFile(path.join(__dirname, "index.js")); }); app.get("/", async (req, res) => { if (req.query...