로드밸런서를 직접 만들어보자 (feat. javascript)

skyepodium·2022년 1월 29일
1

자바스크립트 로드 밸런서를 만들어봅시다.

다루는 내용

  • 로드밸런서 개요
  • 라운드 로빈 구현
  • 스티키 세션 구현

로드 밸런서란?

1) 정의

로드 밸런서란 부하 분배 장치로 분산 시스템에서 서버에 요청을 나누는 역할을 수행합니다.

2) L4 스위치

로드밸런서는 L4 스위치라고도 불려집니다.

스위칭은 네트워크 요청을 목적지로 전달하는 역할을 하는데, OSI 7계층에서 3계층의 주소정보인 IP, 4계층의 정보인 포트를 사용하기 때문입니다.

3) 알고리즘

라운드 로빈

순차적으로 요청을 분배합니다.
예를 들어 서버 3개 A, B, C 가 있으면 첫번째 요청은 A, 그 다음 요청은 B... 이렇게 순서대로 돌아갑니다.

스티키

기본적으로 라운드 로빈 방식을 따르지만, 세션이 연결되면 해당 서버와 통신합니다.

해싱

요청자의 IP를 해시로 만들어서 특정 서버에만 연결되도록 합니다.

4) 구현

직접 작성해봅시다.

간단하게 작성하기 위해 자바스크립트를 이용합니다.

1. 라운드 로빈

1) 라이브러리 설치

express - 서버 구성에 사용
axios - http 요청에 사용
concurrently - node.js 파일 여러개 동시 시작

npm i express axios
npm i concurrently -g

2) 서버 파일 작성

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))

3) 로드밸런서 - rounrobin 작성

// 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번 포트에서 시작')
})

4) 확인

concurrently "node server.js" "node roundrobin.js"

http://localhost:80 으로 로드 밸런서를 호출하면

로드밸런서가 서버를 호출하고

서버에서 반환된 결과를 확인할 수 있습니다.

5) Nginx와 비교

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 를 종료할 수 있습니다.

2. sticky

0) 개요

sticky는 기본적으로 라운드 로빈을 따르지만, 세션이 연결된 후에는 해당서버와 고정적으로 통신하는 방식입니다.

요즘은 SPA(react, vue)기반의 토큰 방식을 주로 사용하지만, 아직 세션도 많이 사용하니 알아봅시다.

1) 서버 파일 작성

// 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))

2) sticky 로드 밸런서 작성

// 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번 포트에서 시작')
})

3) 확인

  • 실행 - 서버와 sticky 동시에 실행
    concurrently "node server.js" "node sticky.js"

  • 확인
    http://localhost 접속

로그인 후 세션이 생성되면 서버가 변경되지 않습니다.

4) nginx와 비교

nginx의 무료 기능에는 session sticky가 없습니다.

나중에 AWS 클라우드로 해볼께요

profile
callmeskye

0개의 댓글