docker와 pm2로 서버 관리하기

3 minute read

집에서 쉬고 있었는데 갑자기 카톡이 울렸다.

??? : 인규님 지금 서버 안되요??

나 : ???? 그럴리 없는데요??????

식겁한 마음으로 서둘러 서버에 접속해 보았더니 무슨 일 때문인지 서버가 내려가 있었다.

이런 일이 한두번이 아니었고 서버가 종료되더라도 복구될 수 있는 기능이 필요하다고 생각하여 이를 도와주는 pm2를 사용하기로 했다. 심지어 cluster mode를 이용해서 Scalability한 서버를 구성할 수 있어서 이 또한 적용해보기로 하였다.

이번에는 이 과정에서 내가 배우고, 오류를 만났고 어떻게 고민하고 해결했는지 기록해보려고한다.

PM2

우선 pm2에 관해 간단하게 설명하자면

  • Node.js는 기본적으로 싱글 스레드(thread)이다. 또한 Node.js 애플리케이션은 단일 CPU 코어에서 실행되기 때문에 CPU의 멀티코어 시스템을 사용하기 힘들다. 예를 들어 현재 내 컴퓨터의 사양이 8코어이고 하이퍼스레딩을 지원해서 최대 16개 코어를 사용 할 수 있는데, Node.js 애플리케이션은 모든 코어를 사용해 최대 성능을 내지 못하고 오직 한 개의 코어만 사용하고 나머지 자원을 놀고 있게 된다. PM2는 이런 문제를 해결하기 위해 클러스터(Cluster) 모드을 통해 단일 프로세스를 멀티 프로세스(Worker)로 늘릴 수 있는 방법을 제공한다. 또한 각각의 프로세스들이 예상치 못한 오류로 종료된다면 자동으로 재시작하게 해줄 수 있고, 이를 간단한 설정파일로 custom할 수 게해준다.

  • 물론 Node.js의 Cluster Module을 이용해서 직접 멀티프로세싱을 제어할 수 있지만 이는 번거롭고, 또한 PM2에서도 내부적으로 Node.js의 Cluster Module을 사용하기 때문에 반드시 직접 Cluster를 제어해야하는 경우가 아니라면 PM2를 사용하는게 좋아보인다.

pm2의 기본적인 설치 및 사용법은 공식 문서나 다른 블로그에 잘 설명이 되있기 때문에 여기서는 스킵하겠다.

pm2가 설치되어 있고, 설정 파일 (ecosystem.config.js 이나 pm2.yaml)이 작성되어 있다고 가정하고 진행하겠다.


Dockfile 작성하기


docker container 위에서 서버를 실행시킬 것이기 때문에 컨테이너 내부에서 pm2로 관리해줘야한다.

  • Dockfile
# Step 1
## base image for Step 1: Node 10
FROM node:10 AS builder

WORKDIR /app
## 프로젝트의 모든 파일을 WORKDIR(/app)로 복사한다 .dockerignore빼고
COPY . .
## Nest.js project를 build 한다
RUN npm install
RUN npm run build

# Step 2
## base image for Step 2: Node 10-alpine(light weight)
FROM node:10-alpine
WORKDIR /app
## Step 1의 builder에서 build된 프로젝트를 가져온다
COPY --from=builder /app ./
## pm2 설치
RUN npm install -g pm2
## application 실행
EXPOSE 3000

CMD ["npm", "run", "start:prod"]

여타 다른 dockerfile들과 큰 차이가 없다. step1 에서 빌드를 진행하고 step2에서 빌드된 파일들을 실행시키는 것이다.

여기서 CMD [“npm”, “run”, “start:prod”] 이 부분이 서버를 실행시키는 스크립트인데 pakage.json에 해당 내용이 정의되어 있다.

  • package.json
"start:prod": "cross-env NODE_ENV=prod pm2 start ecosystem.config.js",

package.json에서 해당부분만 가져온 것이다. cross-env라는 모듈을 이용해서 환경변수를 주입했고, 그 이후 pm2 start 명령으로 pm2를 실행 시켰다.

하지만…

이미지

????? exited with code 0 ??????

서버가 로딩되자마자 종료가 되버린 것이다.

혹시나 pm2가 종료되도 다시 살리겠지? 하고 확인해봤지만

이미지

아예 컨테이너 자체가 종료되버렸다.

알아보니 pm2를 컨테이너에서 사용할 수 있게 해주는 명령어는 pm2-runtime 이라고 한다.

"start:prod": "cross-env NODE_ENV=prod pm2-runtime start ecosystem.config.js",

실제로 pm2부분을 pm2-runtime으로만 바꿔주니 정상적으로 동작했다.

  • 하지만 왜….???

  • 문제는 해결되었지만 도대체 pm2와 pm2-runtime이 뭐가 다르길래, 컨테이너에서는 뭐가 다르 길래 다른 명령을 해줘야하는지 의문이었다.

그래서 열심히 구글링하고, 도커 공식문서 읽어보고 한 결과

A container needs a foreground process to stay alive.

이 문장으로 모든것이 해결되었다.

  • 컨테이너가 살아있으려면 forground process가 필요하다는 것이다.

    • foreground process : 쉘(shell)에서 해당 프로세스 실행을 명령한 후, 해당 프로세스 수행 종료까지 사용자가 다른 입력을 하지 못하는 프로세스
  • 즉, pm2는 background 프로세스이기 때문에 pm2가 실행될 때 demon형태로 실행된다. 따라서 컨테이너 내부에 foreground process가 없기 때문에 컨테이너가 종료되버리는 것이었다.

순간 docker -d 옵션으로 background로 실행하지 않나? 라고 생각했지만 다시 생각해보니

docker -d 옵션은 도커 컨테이너 자체를 background로 실행시켜 컨테이너 실행 중에 shell을 사용할 수 있게 되는 것이고, 위의 예는 컨테이너 내부에서 프로세스 자체가 background로 실행되는 것이기 때문에 다른 것이었다.

PM2 Cluster Module

위의 의문을 해결한 후 pm2의 cluster module을 이용해서 서버 인스턴스를 cpu개수만큼 늘리고 로드밸런싱하려고 시도했다.

사실 이 부분은 큰 문제 없이 진행되었다.

ecosystem.config.js 파일에서 instance를 0이나 max로 두고, exec_mode를 cluster로 설정하고 실행하면 pm2가 알아서 cpu개수만큼 인스턴스를 만들어 주고, 라운드 로빈(default)으로 로드 밸런싱 해준다.

이 과정에서 내가 의문이 들었던 것은 “docker container 내부에서 멀티 프로세싱을 이용해서 컴퓨터의 모든 자원을 사용할 수 있을까?” 였다.

답은 생각보다 간단했다.

  • docker run 명령을 사용할 때 cpu나 memory에 관한 제한을 두지 않는다면 default를 컴퓨터의 모든 자원을 쓸 수 있다고 한다.

실제로 docker inspect 명령으로 컨테이너의 상황을 살펴보면 memory와 CpuShares가 모두 기본값(0)으로 되어있다.

이미지

이렇게 기본값으로 세팅되어 있다면 컨테이너는 컴퓨터의 자원을 모두 사용할 수 있고, 이 값을 조정함으로써 cpu사용 비율을 늘릴 수 있다. 즉, OS의 스케쥴링에서 좀 더 오래 cpu를 소유하고 있을 수 있다는 것이다. 하지만 나는 서버 컴퓨터에 Node js 컨테이너 하나만 구동할 예정이니 cpu를 나눠쓸 프로세스를 고려하지 않아도 되서 기본값으로 두었다.

느낀점

  • docker를 그래도 몇번 사용해봐서 이제는 조금 안다라고 생각했었는데 이번 일이 있으면서 아직 한참 멀었다고 느꼈다…

  • 책 한권 사서 읽어봐야될것 같다.

Leave a comment