개요
NodeJS의 express 프레임워크를 통해 백엔드를 개발하고 배포하던 중에 pm2 통해 무중단 서비스를 운영하게 되었습니다.
pm2를 이용하더라도 pm2에 대한 원리를 모르면 우리가 말하는 완벽한 무중단 서비스를 적용하기 힘들다는 것을 알게 되었습니다.
pm2를 이용하여 무중단 서비스를 구축하기 위해서 어떤 작업을 express에서 추가적으로 해줘야 하는지 한번 알아보도록 하겠습니다.
중단 배포 vs 무중단 배포
중단 배포란, 앱을 일시적으로 미리 막고 앱을 잠시 중단한뒤에 수정 후 배포하는 것을 말합니다. 보통 사용자들이 많이 사용하지 않는 새벽 시간대를 이용하거나 미리 사용자들에게 공지를 하고 서비스를 잠시 중단하게 됩니다.
반대로 무중단 배포는 서비스를 멈추지 않고 앱을 배포하는 것을 말하는데요, 중단 배포의 경우 업데이트 동안은 서비스를 이용하지 못하지만 무중단 배포를 하게 되면 업데이트되는 동안에도 사용자가 서비스를 이용할 수 있습니다.
pm2와 pm2 클러스터
PM2는 Node.js 애플리케이션을 관리하고 배포하기 위한 프로세스 매니저입니다. 기본적으로 로드벨런서 기능이 포함되어 있습니다. PM2를 이용하면 앱을 항상 작동 상태로 유지하고, 시스템 중단 없이 앱을 바로 다시 로드할 수 있습니다. 또 PM2를 통해 우리는 애플리케이션을 안정성과 가용성을 유지하면서 애플리케이션을 실행 및 관리, 모니터링 등을 수행할 수 있습니다.
PM2에서는 기본적으로 Cluster 모드를 지원하는데요, 기본적으로 Node.js는 싱글 쓰레드 기반이기 때문에 cpu 코어를 하나밖에 사용하지 못합니다. 따라서 PM2 Cluster 모드를 이용하면 여러 개의 프로세스를 생성하여 CPU 코어를 효과적으로 활용할 수 있습니다.
예를 들면 CPU 코어가 4개인 서버를 이용한다면 Cluster를 4개를 띄워서 프로세스를 병렬적으로 실행하여 요청을 부하분산 시킴으로써 애플리케이션의 성능을 향상시킬수 있는 것이죠.
PM2 명령어 정리
기본적으로 PM2를 이용하면 무중단 배포를 이용하여 배포할수 있습니다.
간단한 PM2 명령어 몇개를 알아보면 다음과 같습니다.
설치
$ npm install pm2@latest -g
- pm2 설치
프로세스 관리
$ pm2 restart app_name
$ pm2 reload app_name
$ pm2 stop app_name
$ pm2 delete app_name
- restart: 프로세스를 종료하고 다시 시작
- reload: 프로세스를 종료하지않고 다시 시작
- stop: 프로세스를 종료
- delete: 작업 리스트에 올려진 모든 프로세스를 제거
모니터링
$ pm2 monit
- 실시간으로 로그를 확인
클러스터 모드
$ pm2 start app.js -i max
- 앱을 클러스터 모드로 동작
- core의 최대 개수만큼 cluster 실행
Ecosystem 파일
$ pm2 ecosystem
$ pm2 start echosystem.config.js
- pm2 환경 설정 파일인 ecosystem.config.js 을 생성
- start 명령어로 실행
module.exports = {
apps: [{
name: 'app',
script: './app.js',
instances: 0,
exec_mode: 'cluster',
}]
}
- name : 앱 이름
- script : 실행할 스크립트
- instances: 0 / max : 사용가능한 cpu 수만큼 인스턴스 생성, 또는 지정한 수만큼 인스턴스 생성
- exec_mode : cluster 모드로 실행 가능
추가적으로 아래와 같은 옵션들도 있습니다.
module.exports = {
apps: [{
...
...
wait_ready: true,
listen_timeout: 50000,
kill_timeout: 5000,
}]
}
- wait_ready: Reload 대기 이벤트 대신에 어플리케이션에서의 process.send('ready') 를 기다린다. (즉 ready 이벤트를 수동으로 발생시킨다.)
- listen_timeout : ready 이벤트를 기다릴 시간값(ms)
- kill_timeout : 최종 SIGKILL을 보내기 까지의 시간
위 옵션들은 아래에 더 자세히 한번 알아보도록 하겠습니다.
$ pm2 start ecosystem.config.js
- 환경 설정파일로 pm2 실행
PM2 reload를 통해 배포 시 앱이 일시적으로 멈추는 이유
코드 수정 이후 반영을 위해 pm2 reload를 통해 애플리케이션을 다시 재배포할 수 있는데요, 이때 가벼운 애플리케이션이면 문제가 되지 않지만 무거운 애플리케이션일 경우 배포과정에서 약간의 downtime이 존재하게 됩니다. 이때 접속한 유저들은 종종 에러메시지를 볼 수 있습니다.
즉 pm2 reload를 통해 앱이 재시작 되는 동안 클라이언트가 요청을 보낼 때 이러한 문제가 발생할 수 있습니다. PM2 reload를 통해 문제가 발생하는 지점을 각각의 PM2 상태값에 따라서 알아보면 아래와 같습니다.
ready 상태:
- nodejs 프로세스가 구동되고 앱이 요청을 받을수 있는 상태를 의미합니다.
- pm2 reload 후에 새로운 프로세스가 시작됩니다. 새로운 프로세스가 초기화되고 요청을 처리할 준비가 되기 전까지 연결이 끊길 수 있습니다. 이는 앱이 완전히 시작되기 전까지 클라이언트 요청을 처리할 수 없는 상태이기 때문입니다.
- ready 상태에 도달하면 클라이언트는 다시 서버에 연결할 수 있습니다.
- 해당 ready상태를 PM2로 전달하여 현재 프로세스의 준비상태를 확인할 수 있습니다.
SIGINT 신호:
- SIGINT 신호는 프로세스에게 프로그램 종료를 요청하는 신호입니다.
- pm2 reload는 SIGINT 신호를 프로세스에게 보내고, 프로세스는 종료 작업을 수행합니다.
- 프로세스 종료 작업 중에는 새로운 요청을 처리하지 않으며, 이미 처리 중인 요청도 완료하지 않을 수 있습니다.
- 이로 인해 일시적으로 연결이 끊어지는 상황이 발생할 수 있습니다.
SIGKILL 신호:
- SIGKILL 신호는 프로세스에게 강제 종료를 요청하는 신호입니다.
- pm2 reload 후에 일정 시간 동안 프로세스가 SIGINT 신호에 응답하지 않으면 SIGKILL 신호를 보낼 수 있습니다.
- SIGKILL 신호는 프로세스를 즉시 종료하므로, 연결이 갑작스럽게 끊어지는 상황이 발생할 수 있습니다.
프로세스 재시작 과정
- 새로운 App 실행
- 새로운 App에서 구동 완료시 PM2로 Ready 요청 보냄
- PM2에서 Old App으로 SIGINT 보냄
- 일정시간 1600ms 이후에도 종료되지 않으면 SIGKILL을 통해 프로세스 종료
- 위와 같은 형태로 모든 cluster에 적용되면 재배포 완료
문제 1 (앱 구동이 완료되기전에 ready를 너무 빨리 보내는 경우)
- 새로운 앱 구동이 완료되어 요청을 받을 준비가 되기 전에 너무 빨리 ready요청을 보내는 경우 문제 발생
- 이때 애플리케이션에서 요청받을 준비가 완료된 시점에 ready이벤트를 보내도록 설정 파일에 명시해야 함
문제 2 (클라이언트 요청을 처리하는 도중에 old App이 죽어버리는 경우)
- old App 이 종료되기전에 요청시간이 오래 걸리는 요청이 들어왔을 때 강제로 종료되는 문제
- SIGINT 요청이 들어왔을때 app.close() 명령으로 프로세스가 새로운 요청을 받는 것은 거절하고 기존 연결은 유지하도록 처리
- 예를 들어 SIGINT 시그널이 전달된 상태에서 사용자 요청을 받았고, 요청시간이 긴 사용자 요청이 들어왔다고 가정해 보면(예를 들어 5000ms 소요됨) 1600ms 이후에는 kill_timeout 설정에 따라 SIGKILL이 전달되어 앱이 죽는 문제 발생
- kill_timeout 설정을 충분히 줘서 SIGINT 요청을 받고 요청을 처리한뒤 old App이 종료되도록 설정할 수 있음.
- 이때 HTTP 1.1 Keep-Alive를 사용하고 있다면 요청이 처리된 후에도 기존 연결이 계속 유지되기 때문에 해당 연결을 끊어줄 수 있는 방법이 필요함.
좀 더 자세한 상태별 이유는 아래 잘 설명된 블로그 링크 공유드리니 참조해 주시면 좋을 것 같습니다.
PM2 reload 를 통해 우아한 배포 graceful reload 적용해 보기 (express)
앞서 설명드린 Pm2의 ready, SIGINT, SIGKILL 등의 설정들을 적용하기 위한 예제는 아래와 같습니다.
const express = require('express')
const app = express()
const port = 3000
let isDisableKeepAlive = false
app.use(function(req, res, next) {
// SIGINT가 넘어온 이후 응답헤더에 Connection : 'close' 설정
if (isDisableKeepAlive) {
res.set(‘Connection’, ‘close’)
}
next()
})
app.get('/', function(req, res) {
res.send('Hello World!')
})
app.listen(port, function() {
// 프로세스가 요청을 받을 준비가 되었을때 ready 플래그를 pm2로 전달
process.send(‘ready’)
console.log(`application is listening on port ${port}...`)
})
process.on(‘SIGINT’, function () {
isDisableKeepAlive = true
app.close(function () {
console.log(‘server closed’)
process.exit(0)
})
})
이후 pm2에서 현재 애플리케이션의 상황에 맞게 wait_ready, listen_timeout, kill_timeout 등을 echosystem.config.js에 설정해 주시고 PM2를 구동해주시면 됩니다.
마무리
pm2를 사용하기만 하면 자동으로 무중단 배포가 되는줄 알고 사용을 했는데, 생각보다 설정해주어야 하는 옵션들이 많아서 조금 공부하는데 고생을 했던 것 같습니다.
위와 같은 내용들을 잘 공부해보니 NodeJS 프레임워크와 pm2와 같은 프로세스 매니저를 좀 더 이해하는데 도움이 된 것 같습니다.
앞으로도 pm2 에 관련된 내용을 포스팅할 기회가 있으면 공유드리도록 하겠습니다.
감사합니다.
참고
'서버 인프라, 백엔드 > Nodejs, PM2' 카테고리의 다른 글
PM2 : logrotate 모듈을 이용하여 PM2 로그 용량 줄이기 (0) | 2024.01.08 |
---|---|
NestJS : Custom Validation 데코레이터로 유효성 검사 수행하기 (0) | 2023.12.10 |
pm2 : 1개의 cluster에서만 cronjob 수행하기 (instance_var 옵션) (0) | 2023.07.23 |
nodejs의 모듈 시스템 : export, import (0) | 2018.09.27 |