040 - 무중단 서비스
남기용 261

오라클 클라우드 서버에서 SSR페이지 서비스를 할때 여러번 서비스를 요청할 경우 서버가 느려지거나 심각한경우 멈추게 되어 서버를 다시 실행 해야 하는 경우가 있습니다.

문제의 원인은 SSR 페이지 요청시 메모리가 해제되지 않고 계속 증가되어 그런데 ‘Nodejs 무중단’ 으로 검색해보시면 여러 자료가 나오는데 제가 참고한 문서는

https://engineering.linecorp.com/ko/blog/pm2-nodejs/ 링크에서 참고하시면 됩니다.

개념적으로는 어려운 부분일수는 있으나 pm2는 로드벨런스 기능이 있기 때문에 이를 활용하여 우리는 인스턴스를 2개 만들고 메모리가 어느정도 이상 쌓이면 인스턴스 를 새로 시작되게 하고 다른 인스턴스에서 계속해서 서비스를 하도록 하려고 합니다.

그리고 또하나 문제는 갈수록 앱이 규모가 커지면서 오라클 클라우드에서 빌드 하는 속도가 매우 느려져서 빌드한 결과(dist 폴더)를 서버에 올리고 서버에서는 더이상 빌드 하지 않도록 하겠습니다.

먼저 pm2에서 사용할 환경 파일을 작성하고 server.js를 다음과 같이 수정하도록 합니다.

module.exports = {
	apps : [
		{
			name : 'ezhome',
			script : './server/server.js',
			instances : 0,
			scale : 2,
			exec_mode : 'cluster',
			wait_ready : true,
			listen_timeout : 50000,
			kill_timeout : 5000,
		}
	]
}

require('dotenv').config();
const express = require('express');
const http = require('http');
const path = require('path');
const fs = require('fs');

// 앱 초기화
const app = express();
const port = process.env.VUE_APP_SERVER_PORT || 3000;
const webServer = http.createServer(app);

let isDiableKeepAlive = false;
app.use((req, res, next)=> {
	if(isDiableKeepAlive) {
		console.log('Keep Alive', isDiableKeepAlive);
		res.set('Connection', 'close');
	}
	next();
})

// 파비콘
app.use((req, res, next)=> {
	if(req.path.indexOf('favicon.ico') > -1) {
		const favicon = fs.readFileSync(path.join(__dirname, '../dist/favicon.ico'));
		res.status(200).end(favicon);
		return;
	}
	next();
});

// 파서
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const fileUpload = require('express-fileupload');
app.use(fileUpload());
const cookieParser = require('cookie-parser');
app.use(cookieParser());

// 글로벌 세팅
global.MEMBER_PHOTO_PATH = path.join(__dirname, './upload/memberPhoto');
fs.mkdirSync(MEMBER_PHOTO_PATH, {recursive : true});

// Passport
const passport = require('./plugins/passport');
passport(app);

// 이미지 업로드
const thumbnail = require('./plugins/thumbnail');
app.use('/upload/:_path', thumbnail(path.join(__dirname, './upload')));

// 정적 폴더
app.use(express.static(path.join(__dirname, "../dist")));

// API 라우터
const memberRouter = require('./api/member');
app.use('/api/member', memberRouter);
app.use('/api/*', (req, res) => {
	res.json({ err: '요청하신 API가 없습니다. : ' + req.url });
});

// Vue SSR
const { createBundleRenderer } = require('vue-server-renderer');
const template = fs.readFileSync(path.join(__dirname, 'index.template.html'), 'utf-8');
const serverBundle = require(path.join(__dirname, "../dist/vue-ssr-server-bundle.json"));
const clientManifest = require(path.join(__dirname, "../dist/vue-ssr-client-manifest.json"));

app.get('*', (req, res) => {
	// console.log(req.cookies, req.user);
	const renderer = createBundleRenderer(serverBundle, {
		runInNewContext: false,
		template,
		clientManifest,
	});

	const ctx = {
		url: req.url,
		title: 'Vue SSR App',
		metas: `<!-- inject more metas -->`,
		token : req.cookies.token || null,
		member : req.user || null,
	};

	const stream = renderer.renderToStream(ctx);

	stream.on('end', () => {
		const memSize = Object.entries(process.memoryUsage())[0][1];
		console.log('스트림 렌더 종료', (memSize/1024/1024).toFixed(4));
		if(process.platform == 'linux') {
			if(memSize > 150000000) {
				process.emit('SIGINT'); 
			}
		}
	}).pipe(res);
});

// 서버 응답
webServer.listen(port, () => {
	process.send('ready');
	console.log(`http://localhost:${port}`);
});

process.on('SIGINT', function(){
	isDiableKeepAlive = true;
	webServer.close(function(){
		console.log('server closed');
		process.exit(0);
	})
});

이제 pm2로 서비스를 할때 ecosystem.config.js를 시작하도록 합니다.

 pm2 start ecosystem.config.js

pm2 인스턴스 개수 설정 명령

pm2 scale [서비스이름] 2

그리고 axios 플러그인 등록에 문제가 있어 이 부분도 수정합니다.

"use strict";

import Vue from 'vue';
import axios from "axios";

// Full config:  https://github.com/axios/axios#request-config
// axios.defaults.baseURL = process.env.baseURL || process.env.apiUrl || '';
// axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
// axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';

let config = {
	baseURL: process.env.BASE_URL || process.env.apiUrl || "",
	timeout: 60 * 1000, // Timeout
	proxy: {
		host: 'localhost',
		port: process.env.VUE_APP_SERVER_PORT
	}
	// withCredentials: true, // Check cross-site Access-Control
};

const _axios = axios.create(config);

_axios.interceptors.request.use(
	function (config) {
		const { $Progress } = Vue.prototype;
		if($Progress) $Progress.start();

		return config;
	},
	function (error) {
		// Do something with request error
		return Promise.reject(error);
	}
);

// Add a response interceptor
_axios.interceptors.response.use(
	function (response) {
		const { $Progress, $toast } = Vue.prototype;
		const {data, status} = response;
		let msg = "";

		console.log("AJAX", response);
		if(status != 200) {
			msg = "서버접속 실패";
		}
		if(data && data.err) {
			msg = data.err;
		}

		if(msg) {
			if($toast) $toast.error(msg);
			if($Progress) $Progress.fail();
			console.warn(msg);
			return false;
		} else {
			if($Progress) $Progress.finish();
			return data;
		}
	},
	function (error) {
		// Do something with response error
		return Promise.reject(error);
	}
);

Vue.prototype.$axios = _axios;

export default _axios;

깃 워크플로우에서 yarn build 부분을 삭제하고 .gitignore 에서 /dist를 삭제하세요

이제 오라클 서버에서 빌드하지 않고 빌드된 결과를 git 으로 관리하여 github 서버에 올리도록 합니다.

오라클 서버에서 git pull이 제대로 되지 않으면 이전 내용을 삭제하고 다시 소스를 clone 하시고 .env 파일을 만들어 실행합니다.

회원로그인 후 댓글을 작성하실 수 있습니다. 로그인
© 2024 ezcode all right reserved.