서버 운영에 있어서 성능과 안정성은 매우 중요한 요소들 중 하나입니다. 이 글에선 Express 서버를 이용할 때 여러 워커를 클러스터 구성을 이용해 서비스하고 NGINX를 이용해 이중화 구성을 하는 방법에 대해 알아보도록 합니다.
0. 사전 준비.
이 글에서 웹서버는 React와 Express를 사용해 배포한 서버를 사용할 예정입니다. 실제 배포는 Docker를 사용해 도커 이미지를 생성한 후 실행시키도록 할 예정입니다. 미리 NPM와 Docker를 준비합시다.
마지막으로 모든 작업 결과물을 한번에 구동시키기 위해 Docker compose를 사용할 예정이니 Docker compose도 준비해 주세요.
- 2019/02/14 - [JavaScript] - NodeJS 시작하기 - NodeJs 설치 및 NPM 설치
- 2019/05/23 - [Linux] - [Ubuntu] Docker 설치하기
1. NodeJS
NodeJS는 구글이 구글 크롬에 사용하려고 제작한 V8 오픈소스 자바스크립트 엔진을 기반으로 제작된 자바스크립트 런타임입니다. NodeJS는 다음과 같은 특징이 있습니다.
- 단일 스레드.
- 비동기 방식.
- 이벤트 루프를 사용.
- NPM.
NodeJS는 싱글 스레드이기 때문에 하나의 CPU를 여럿이 나눠 갖는 건 비효율적입니다. 따라서 CPU 숫자에 맞춰서 서버를 띄워보겠습니다.
2. 웹서버 생성.
우선 프론트를 준비합니다. 다음 명령어로 react app을 생성해 주세요.
$ npx create-reac-app my-react
이 앱을 Express 서버로 서비스할 예정입니다. 폴더로 들어가서 다음 패키지를 설치하세요.
$ cd my-react
$ npm i express express-favicon
이제 express 서버를 실행할 코드를 작성해야 합니다. server 폴더에 index.js파일을 만드신 후 다음과 같이 코딩해 주세요.
//// ./server/index.js
const express = require('express');
const favicon = require('express-favicon');
const path = require('path');
const app = express();
const port = 3000;
const server = app.listen(port, () => {
console.log(`Server is listening on port ${server.address().port}.`);
});
app.use(favicon(path.join(__dirname, '../public/favicon.ico')));
app.use(express.static(__dirname));
app.use(express.static(path.join(__dirname, '../build')));
app.get('/ping', (req, res) => {
return res.send('pong');
});
app.get('/*', (req, res) => {
return res.sendFile(path.join(__dirname, '../build', 'index.html'));
});
이제 기본으로 생성된 react를 빌드해 줍시다.
$ npm run build
여기까지 수행하셨으면 폴더 구조가 다음과 같을 겁니다.
우리가 작성한 express서버가 react app을 잘 서빙하는지 확인해 봅시다. localhost:3000으로 이동해 react 앱을 확인해 보세요. 그리고 localhost:3000/ping으로 이동해 pong 메시지를 잘 수신하는지도 확인합시다.
3. 클러스터 구성.
앞서 설명한 대로 NodeJS는 단일 스레드입니다. 이제 우린 서버의 CPU개수에 맞춰서 여러 워커를 띄어서 서버를 운영해 보도록 하겠습니다.
여러 워커가 돌아갈 때 워커가 동작하는 서버를 구분하기 위해 uuid 패키지를 설치합니다.
$ npm i uuid
그리고./server/index.js를 다음과 같이 수정해주세요.
//// ./server/index.js
const cluster = require('cluster');
const os = require('os');
const uuid = require('uuid');
const port = 3000;
const instance_id = uuid.v4();
//// Create worker.
const cpu_count = os.cpus().length;
const worker_count = cpu_count / 2;
//// If master, create workers and revive dead worker.
if(cluster.isMaster){
console.log(`Server ID: ${instance_id}`);
console.log(`Number of server's CPU: ${cpu_count}`);
console.log(`Number of workers to create: ${worker_count}`);
console.log(`Now create total ${worker_count} workers ...`);
//// Message listener
const workerMsgListener = (msg) => {
const worker_id = msg.worker_id;
//// Send master's id.
if(msg.cmd === 'MASTER_ID'){
cluster.workers[worker_id].send({cmd: 'MASTER_ID', master_id: instance_id});
}
}
//// Create workers
for(var i = 0; i < worker_count; i ++){
const worker = cluster.fork();
console.log(`Worker is created. [${i +1}/${worker_count}]`);
worker.on('message', workerMsgListener);
}
//// Worker is now online.
cluster.on('online', (worker) => { console.log(`Worker is now online: ${worker.process.pid}`); });
//// Re-create dead worker.
cluster.on('exit', (deadWorker) => {
console.log(`Worker is dead: ${deadWorker.process.pid}`);
const worker = cluster.fork();
console.log(`New worker is created.`);
worker.on('message', workerMsgListener);
});
}
//// If worker, run servers.
else if(cluster.isWorker){
const express = require('express');
const favicon = require('express-favicon');
const path = require('path');
const app = express();
const worker_id = cluster.worker.id;
const server = app.listen(port, () => { console.log(`Server is listening on port ${server.address().port}.`); });
let master_id = "";
//// Request master's id to master.
process.send({worker_id: worker_id, cmd: 'MASTER_ID'});
process.on('message', (msg) => {
if(msg.cmd === 'MASTER_ID'){
master_id = msg.master_id;
}
});
app.use(favicon(path.join(__dirname, '../public/favicon.ico')));
app.use(express.static(__dirname));
app.use(express.static(path.join(__dirname, '../build')));
app.get('/ping', (req, res) => { return res.send('pong'); });
app.get('/where', (req, res) => { return res.send(`Running server: ${master_id} \n Running worker: ${worker_id}`); });
app.get('/kill', (req, res) => { cluster.worker.kill(); return res.send(`Called worker killer.`);})
app.get('/*', (req, res) => { return res.sendFile(path.join(__dirname, '../build', 'index.html')); });
}
소스가 길어졌으니 주석을 확인하면서 차근차근 코딩해 주세요. 간단히 설명해드리자면 다음과 같습니다.
- 우리는 한 서버의 CPU개수의 절반만큼의 워커를 생성할 겁니다.
- 만약 현재 생성된 클러스터가 마스터라면 워커 클러스터를 생성하고 관리하는 역할을 수행합니다.
- 마스터는 미리 정해진 개수만큼 워커를 생성하고 만약 죽은 워커가 발견된 경우 새 워커를 생성시켜 항상 일정 개수의 워커가 서버에서 동작할 수 있도록 합니다.
- 워커는 express 서버를 구동합니다.
- 워커에는 어느 서버에서 수행되고 있는지 확인할 수 있는 기능과 현재 워커를 죽일 수 있는 기능이 추가되었습니다.
코딩이 끝났다면 다시 서버를 실행시켜 보도록 합시다.
$ node server
콘솔에서 다음과 같은 메시지를 확인할 수 있습니다.
이제 localhost:3000/where로 이동해 보세요. 현재 우리가 접속한 서버와 워커의 번호를 확인할 수 있습니다. 여기서 localhost:3000/kill로 이동하면 워커를 죽일 수 있으며 이때 마스터는 죽은 워커를 확인 해 새 워커를 생성합니다.
이후 다시 localhost:3000/where로 이동하면 워커의 번호가 변경된 것을 확인할 수 있습니다.
4. Docker를 이용한 서버 구동
이제 우리가 작성한 서버를 도커를 이용해 배포해봅시다. 간단히 배포하기 위해 Dockerfile을 사용해 배포할 예정입니다.
Dockerfile 파일을 생성 한 뒤 다음과 같이 작성해 주세요.
# ./Dockerfile
FROM node:slim
# app 폴더 생성.
RUN mkdir -p /app
# 작업 폴더를 app폴더로 지정.
WORKDIR /app
# dockerfile과 같은 경로의 파일들을 app폴더로 복사
ADD ./ /app
# 패키지 파일 설치.
RUN npm install
# 환경을 배포 환경으로 변경.
ENV NODE_ENV=production
#빌드 수행
RUN npm run build
ENV HOST=0.0.0.0 PORT=3000
EXPOSE ${PORT}
#서버 실행
CMD ["node", "server"]
아주 간단한 Dockerfile을 작성했습니다. 주석을 참고하시면 모두 이해하실 수 있을 겁니다. 이제 다음 명령어를 통해 작성한 Dockerfile을 이용해 도커 이미지를 생성합니다.
$ docker build . -t my-react:0.0.1
도커 이미지가 정상적으로 생성된 것을 확인할 수 있습니다. 다음 명령어를 통해 직접 실행시켜 주세요.
$ docker run -itd -p 8080:3000 my-react:0.0.1
이제 localhost:8080로 이동하면 모든 기능을 정상적으로 사용할 수 있습니다.
'Programming > JavaScript' 카테고리의 다른 글
[VueJS Tutorial] 외부 API를 이용한 날씨 앱 만들기. (0) | 2020.08.13 |
---|---|
[Passport] Passport를 이용한 Node.js에서의 로컬 인증. (0) | 2020.08.03 |
Storybook (0) | 2020.06.02 |
Express에서 GraphQL 사용하기: express-graphql (0) | 2020.04.04 |
NodeJs - PostgreSql 연동하기. (0) | 2020.04.04 |