자바스크립트 로드 밸런서를 만들어봅시다.
다루는 내용
로드 밸런서란 부하 분배 장치로 분산 시스템에서 서버에 요청을 나누는 역할을 수행합니다.
로드밸런서는 L4 스위치
라고도 불려집니다.
스위칭은 네트워크 요청을 목적지로 전달하는 역할을 하는데, OSI 7계층에서 3계층의 주소정보인 IP
, 4계층의 정보인 포트
를 사용하기 때문입니다.
순차적으로 요청을 분배합니다.
예를 들어 서버 3개 A, B, C 가 있으면 첫번째 요청은 A, 그 다음 요청은 B... 이렇게 순서대로 돌아갑니다.
기본적으로 라운드 로빈 방식을 따르지만, 세션이 연결되면 해당 서버와 통신합니다.
요청자의 IP를 해시로 만들어서 특정 서버에만 연결되도록 합니다.
직접 작성해봅시다.
간단하게 작성하기 위해 자바스크립트를 이용합니다.
express - 서버 구성에 사용
axios - http 요청에 사용
concurrently - node.js 파일 여러개 동시 시작
npm i express axios
npm i concurrently -g
express 서버 3개를 만듭니다.
// server.js
// 1. 서버 구성에 사용할 express
const express = require('express')
// 2. 서버 3개 생성
const app1 = express()
const app2 = express()
const app3 = express()
// 3. 응답 내용 구성
const mainHandler = num => (req, res) => {
res.send(`<h1>안녕하세요 ${num} 번 서버입니다.</h1>`)
}
// 4. 메인 url로 요청이 오면 응답 내용 반환
app1.get('/', mainHandler(1))
app2.get('/', mainHandler(2))
app3.get('/', mainHandler(3))
// 5. 서버 시작 메시지
const errHandler = num => err => {
err ?
console.log(`${num} 번 서버 시작 실패`):
console.log(`${num} 번 서버 시작`)
}
// 6. 서버 시작
app1.listen(3000, errHandler(1))
app2.listen(3001, errHandler(2))
app3.listen(3002, errHandler(3))
// 1. 서버 구성에 사용할 express, http 요청에 사용할 axios
const express = require('express')
const axios = require('axios')
// 2. 인스턴스 생성
const loadBalancer = express()
// 3. 앞에서 만든 서버 등록
let idx = 0
const serverList = [
'127.0.0.1:3000',
'127.0.0.1:3001',
'127.0.0.1:3002',
]
// 파비콘 요청은 따로 분리
loadBalancer.get('/favicon.ico', (req, res) => {
res.status(204)
})
// 4. 요청 핸들러 작성
loadBalancer.all("*", (req, res) => {
const { method, protocol, originalUrl } = req
// 라운드 로빈
// 1) 요청이 오면 서버 목록에서 인덱스 하나씩 증가하면서 분배
const target = serverList[idx++]
if(idx >= serverList.length) idx = 0
// 2) 요청을 받을 서버 url 작성
const requestUrl = `${protocol}://${target}${originalUrl}`
// 3) http 응답 발송
axios.request(requestUrl, {
method
})
// 4) 결과 받으면, header 설정 후 반환
.then(result => {
res.set({...result.headers})
res.send(result.data)
})
.catch(error => {
res.set({...error.headers})
res.send(error)
})
})
// 5. 로드 밸런서 시작
loadBalancer.listen(80, err =>{
err ?
console.log('로드 밸런서 80번 포트에서 시작 실패'):
console.log('로드 밸런서 80번 포트에서 시작')
})
concurrently "node server.js" "node roundrobin.js"
http://localhost:80
으로 로드 밸런서를 호출하면
로드밸런서가 서버를 호출하고
서버에서 반환된 결과를 확인할 수 있습니다.
Nginx는 주로 정적 파일(React.js, Vue.js의 배포 파일)을 위한 웹서버로 사용합니다.
웹서버 이외에도 여러 기능을 담당하는데 그 중 로드밸런싱
도 있습니다.
자바스크립트로 작성한 라운드로빈
과 nginx의 라운드로빈
을 비교해봅시다.
설치
맥 OS 를 사용중이면 homebrew로 설치합니다.
brew install nginx
이동
cd ~/usr/local/etc/nginx
설정 파일 수정
vi nginx.conf
3개의 서버를 등록해주고,
proxy_pass 에 http://express-app
을 작성합니다.
http {
# 생략
upstream express-app {
server 127.0.0.1:3000;
server 127.0.0.1:3001;
server 127.0.0.1:3002;
}
# 생략
server {
# 생략
location / {
proxy_pass http://express-app;
root html;
index index.html index.htm;
}
# 생략
}
}
실행
brew services start nginx
확인
nginx에는 기본적으로 http://localhost:8080
에서 실행됩니다.
자바스크립트로 작성한 라운드로빈과 동일하게 작동합니다.
참고: brew services stop nginx
로 nginx 를 종료할 수 있습니다.
sticky는 기본적으로 라운드 로빈을 따르지만, 세션이 연결된 후에는 해당서버와 고정적으로 통신하는 방식입니다.
요즘은 SPA(react, vue)기반의 토큰 방식을 주로 사용하지만, 아직 세션도 많이 사용하니 알아봅시다.
// server.js
// 1. 서버 구성에 사용할 express
const express = require('express')
// 2. 서버 3개 생성
const app1 = express()
const app2 = express()
const app3 = express()
app1.use(express.json())
app1.use(express.urlencoded({ extended: false }))
app2.use(express.json())
app2.use(express.urlencoded({ extended: false }))
app3.use(express.json())
app3.use(express.urlencoded({ extended: false }))
// 3. 응답 내용 구성
const mainHandler = num => (req, res) => {
res.send(`<h1>안녕하세요 ${num} 번 서버입니다.</h1>`)
}
const loginTemplateHandler = num => (req, res) => {
const loginTemplate = `
<h1>안녕하세요 ${num} 번 서버입니다.</h1>
<form action="/login" method="POST">
<input name="id">
<input name="password" type="password">
<input type="submit" value="전송">
</form>
`
res.send(loginTemplate)
}
const loginHandler = num => (req, res) => {
const { id } = req.body
res.set({
'Set-Cookie': `name=${id}`
})
res.send(`<h1>${num} 서버 <a href="/">메인페이저로 돌아가기</a<</h1>`)
}
// 4. 메인 url로 요청이 오면 응답 내용 반환
app1.get('/', mainHandler(1))
app2.get('/', mainHandler(2))
app3.get('/', mainHandler(3))
app1.get('/login', loginTemplateHandler(1))
app2.get('/login', loginTemplateHandler(2))
app3.get('/login', loginTemplateHandler(3))
app1.post('/login', loginHandler(1))
app2.post('/login', loginHandler(2))
app3.post('/login', loginHandler(3))
// 5. 서버 시작 메시지
const errHandler = num => err => {
err ?
console.log(`${num} 번 서버 시작 실패`):
console.log(`${num} 번 서버 시작`)
}
// 6. 서버 시작
app1.listen(3000, errHandler(1))
app2.listen(3001, errHandler(2))
app3.listen(3002, errHandler(3))
// sticky.js
// 1. 서버 구성에 사용할 express, http 요청에 사용할 axios
const express = require('express')
const axios = require('axios')
// 2. 인스턴스 생성
const loadBalancer = express()
// 3. 앞에서 만든 서버 등록
let idx = 0
const serverList = [
'127.0.0.1:3000',
'127.0.0.1:3001',
'127.0.0.1:3002',
]
const sessionTable = {}
// 파비콘 요청은 따로 분리
loadBalancer.get('/favicon.ico', (req, res) => {
res.status(204)
})
loadBalancer.use(express.json())
loadBalancer.use(express.urlencoded({ extended: false }))
// 4. 요청 핸들러 작성
loadBalancer.all("*", (req, res) => {
const { method, protocol, originalUrl, body } = req
const cookie = req.headers.cookie
const target = sessionTable[cookie] ? sessionTable[cookie] : serverList[idx]
// 2) 요청을 받을 서버 url 작성
const requestUrl = `${protocol}://${target}${originalUrl}`
// 3) http 응답 발송
axios.request(requestUrl, {
method,
data: body
})
// 4) 결과 받으면, header 설정 후 반환
.then(result => {
const session = result.headers['set-cookie']
if(session) {
sessionTable[session] = target
}
idx++
if(idx >= serverList.length) idx = 0
res.set({...result.headers})
res.send(result.data)
})
.catch(error => {
res.set({...error.headers})
res.send(error)
})
})
// 5. 로드 밸런서 시작
loadBalancer.listen(80, err =>{
err ?
console.log('로드 밸런서 80번 포트에서 시작 실패'):
console.log('로드 밸런서 80번 포트에서 시작')
})
실행 - 서버와 sticky 동시에 실행
concurrently "node server.js" "node sticky.js"
확인
http://localhost
접속
로그인 후 세션이 생성되면 서버가 변경되지 않습니다.
nginx의 무료 기능에는 session sticky가 없습니다.
나중에 AWS 클라우드로 해볼께요