FCM 알림 서버를 만드는 프로젝트에 참여하게 되었다.
맡게 될 서버는 FCM 알림만 담당하게 되고, 저사양 서버에서(프리티어) 작동해야되어서 가벼워야해서 노드로 구축하기로 했다.
그리고 나서 찾아보니 firebase는 Node.js를 가장 잘 지원하고 있었다.
https://firebase.google.com/docs/admin/setup?hl=ko
Docs에서 찾아보니 Node에서 지원하는 기능이 가장 많다!
서버를 노드로 구성하기로 하고 배포 방법을 결정해야했다.
전에 참여했던 프로젝트에서는 Nginx, Docker, Github Action으로 Blue-Green 무중단 배포를 구현했었다.
이번에는 새로운 방법을 구현해보고 싶기도 했고 도커로 서버를 띄우는 것보다 pm2로 서버를 띄우는 게 더 가볍게 서버를 운영할 수 있으므로 pm2로 배포를 해보기로 했다.
구현
https://engineering.linecorp.com/ko/blog/pm2-nodejs
PM2를 활용한 Node.js 무중단 서비스하기
이 글은 마이크로소프트웨어 393호에 기고된 글입니다. 자바스크립트는 가장 널리 사용되는 클라이언트 측 프로그래밍 언어이자 프론트엔드 웹 개발 언어 중 하나입니다. 그리고 Node.js는 Chrome의
engineering.linecorp.com
LINE의 테크 블로그를 참고해서 구현했고, 정말 간단했다.
(구현은 간단하지만 알아야하는 내용은 간단하지 않다..)
ecosystem.config.js
파일에 config를 작성하고 pm2 reload ecosystem.config.js
명령어를 입력해주면 된다.
reload 명령어는 실행 중인 프로세스를 하나씩 재시작하면서 무중단 배포를 할 수 있게해준다.
사실상 프리티어 환경은 1코어라서 프로세스가 1개 생성되고 reload를 한다고 해도 서비스가 중단될 수 밖에 없다.
하지만 서버 스펙만 업그레이드 되면 무중단배포가 가능한 방법이므로, 학습의 차원에서 시도해보았다!
다음과 같이 설정파일을 작성했다.
module.exports = {
apps: [
{
name: 'app',
script: 'npm',
args: 'start',
instances: -1,
exec_mode: 'cluster',
wait_ready: true,
listen_timeout: 30000,
kill_timeout: 5000,
watch: true,
ignore_watch: ['node_modules', 'logs'],
max_memory_restart: '900M',
},
],
};
그리고 코어가 많은 로컬에서 프로세스 4개로 무중단배포 테스트를 해보았고, 아래 gif에서처럼 서비스가 하나씩 재시작되었다.
무중단배포를 위한 pm2 옵션
pm2 config에는 다양한 옵션이 있고 다음 공식문서에서 자세히 볼 수 있다.
PM2 - Ecosystem File
Advanced process manager for production Node.js applications. Load balancer, logs facility, startup script, micro service management, at a glance.
pm2.keymetrics.io
무중단배포에 필요한 graceful start와 graceful shutdown을 pm2 옵션으로 제공한다.
Graceful Start
애플리케이션이 초기화되거나 시작될 때, 리소스를 안전하게 할당하고 초기화하는 과정을 수행하는 것.
앱 구동이 완료되지 않았는데 ready 이벤트를 보내고 기존 앱을 중단시키면 서비스 중단이 발생할 수 있다.
이를 방지하기 위해서 앱이 구동될 때까지 기다리고, 준비가 되었을 때 ready 이벤트를 보내서 위의 상황때문에 발생하는 서비스 중단을 예방할 수 있다.
wait_ready
앱이 준비가 되었을 때 마스터 프로세스에 전달할 'ready' 이벤트를 기다릴지 여부
listen_timeout
앱이 수신 대기하지 않을때 강제로 다시 로드하기 전까지의 시간
wait_ready를 true로 주어 ready 이벤트를 기다리게 하고 listen_timeout으로 몇 초를 기다릴지 설정할 수 있다.
Graceful Shutdown
애플리케이션이 종료될 때, 현재 진행 중인 작업을 완료하고 리소스를 정리하며, 사용자에게 서비스 중지를 알리는 등의 과정을 수행하는 것.
앱을 중단할 때, 해당 프로세스가 처리 중인 클라이언트 요청이 다 처리되지 못했는데 SIGKILL 시그널이 발생하여 앱이 중지되어버리면 클라이언트는 응답을 받을 수 없고 서비스 중단이 발생하게 된다. 이를 위해 SIGKILL 시그널을 보내기까지의 시간을 설정할 수 있다.
kill_timeout
SIGKILL 이벤트를 보내기 전까지 기다릴 시간
🖐️ 잠깐 유닉스 시스템 시그널?
Signal은 유닉스, 유닉스 계열, POSIX 호환 운영 체제에 쓰이는 제한된 형태의 프로세스 간 통신이다. 신호는 프로세스나 동일 프로세스 내의 특정 스레드로 전달되는 비동기식 통보이다. 이러한 신호들은 1970년대 벨 연구소를 통해 존재한 뒤로 최근의 시기에는 POSIX 표준에 정의되어 있다.
출처 : https://ko.wikipedia.org/wiki/%EC%9C%A0%EB%8B%89%EC%8A%A4_%EC%8B%A0%ED%98%B8
시그널의 종류를 터미널에서 볼 수 있다.
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
가장 익숙한 것은 'SIGINT'와 'SIGKILL'이다.
ctrl+c를 입력했을 때 발생하는게 SIGINT, kill -9 {process id} 할 때 발생하는게 'SIGKILL' 이다.
둘 다 프로세스를 종료시키는 명령어지만 약간의 차이가 있다.
SIGINT는 종료하기 전에 리소스를 해제하거나 파일을 닫는 등의 작업을 수행할 수 있지만, SIGKILL은 시그널을 받는 즉시 프로세스가 종료된다는 차이가 있다.
추가로 설정한 옵션
어디선가 예상치 못한 메모리 누수가 발생한다면 앱 구동시간이 오래될 수록 메모리 사용량이 증가하고 메모리 초과로 장애가 발생할 수 있다. (알고 싶지 않았어요!)
메모리 사용량이 마구마구 증가할 때 앱을 재시작할 수 있는 옵션을 추가해주었다.
max_memory_restart
메모리가 설정값 이상이 되면 앱을 재시작한다.
트러블슈팅 : pm2 keep restarting
pm2 config를 작성하며 어플리케이션이 계속해서 재시작되는 문제가 있었다.
위에 적어둔 옵션은 배포를 완성하고 난 후의 옵션이다.
서버에 어플리케이션을 올리고 실행되는 것을 확인했는데, 나중에 보면 서버가 죽어있었고 확인해보니 앱이 계속해서 무한으로 재시작되고 있었다.
로그를 찍어보아도 에러가 발생하는 게 없어서 당황스러웠다.
그러다 발견한 스택오버플로우!
https://stackoverflow.com/questions/37145546/node-js-pm2-keeps-restarting-almost-every-second
옵션을 제대로 읽지 않고 적용한 내 잘못이었다.
옵션 중 watch를 true로 주고 처음 세팅을 진행했다.
watch: true,
ignore_watch: ['node_modules'],
그리고 배포를 하고 서버 로그를 파일로 저장하는 로직을 추가했다.
watch 옵션은 프로젝트 경로의 파일이 변화하는 것을 watch 할 건지에 대한 옵션이다.
앱이 구동될 때 마다 로그파일이 작성되고 프로젝트 경로 아래의 로그 파일이 계속 생성되니 pm2 자체에서 변화를 감지하고 계속 앱을 재시작 시켰던 것이었다.
watch: true,
ignore_watch: ['node_modules', 'logs'],
watch에서 제외할 경로에 로그를 저장할 폴더를 지정해주고 나서 해결할 수 있었다.
교훈 : 사용하는 옵션을 제대로 알고 사용하자
'Projects' 카테고리의 다른 글
Swap 메모리의 힘은 대단했다 (0) | 2024.03.09 |
---|---|
게시글 추천 시스템 도입을 고려하며 조사한 내용 (0) | 2024.02.04 |
게시판 전체 글 랭킹 점수 데이터 업데이트 속도 77.72% 개선 (bulk update vs batch update) (1) | 2024.01.23 |
Querydsl을 이용한 커서 기반 페이지네이션 구현기 (0) | 2024.01.20 |
[코드실행기능 개발기 #3] 코드 실행 서버 부하테스트 (1) | 2023.12.17 |