티스토리 뷰

728x90
반응형

애플리케이션을 개발할 때 멀티코어를 활용하는 멀티 스레드 또는 멀티 프로세스 방식을 도입하면 서버의 성능을 극대화할 수 있습니다. 멀티 프로세스와 멀티 코어의 개념, 장점에 대해 궁금하신 분은 아래 포스팅을 참고 부탁드립니다.

 

 

멀티 프로세서와 멀티 코어의 차이점

회사에서 일하다 보면, 혼자서 일하는 게 편할 때도 있고, 동료와 함께 일하는 게 좋을 때도 있습니다. 일할 때 언제 혼자가 편하고, 언제 동료와 할 때 좋을까요? 사람마다 다르겠지만, 기계는

it-techtree.tistory.com

이번 포스팅에서는 Node.js에서 HTTP 요청을 멀티 스레드, 멀티 프로세스를 활용하여 병렬로 처리하는 방법에 대해 알아보겠습니다.

싱글 스레드로 구동되는 Node.js

Node.js의 V8 런타임 엔진은 단일 스레드로 구동되며, 이벤트 루프, 콜 스택을 이용하여 비동기식으로 작업을 처리합니다. 네트워크 또는 파일 I/O로 인해 대기시간이 발생하는 경우, 비동기식 처리 방식을 도입하면 작업 완료 신호를 받기 전에 다른 작업을 처리할 수 있어 효율을 높일 수 있습니다. 웹 서버 또는 클라이언트의 경우, 잦은 HTTP 요청과 응답을 처리해야 하므로 비동기식 처리방식을 많이 사용합니다. 그러나, 싱글 스레드 기반으로 구동되는 Node.js는 앞 단계의 작업이 완료되어야 그 이후의 작업이 진행되므로, 처리시간이 길어집니다. HTTP 요청을 단일 스레드 기반 서버로 구현한 후, 이를 병렬로 처리하는 방법을 도입하여 서버의 성능을 개선해보겠습니다.

HTTP 요청하는 클라이언트 만들기

다수의 HTTP 요청을 전송하는 클라이언트 코드를 아래와 같이 작성합니다. /request GET 요청을 Query Parameter와 함께 전송한 뒤 서버로부터 응답받은 메시지를 출력하는 코드입니다.

const http = require('http')
const querystring = require('querystring')

const request = function (param) {
  return new Promise((resolve, reject) => {
    const parameters = {
      menu: param,
    }
    const get_request_args = querystring.stringify(parameters)

    const options = {
      hostname: 'localhost',
      port: 3000,
      path: '/request?' + get_request_args,
      method: 'GET',
    }
    console.log(printTime() + ' > Request : ', get_request_args)
    const req = http.request(options, res => {
      res.on('data', d => {
        process.stdout.write(d)
        resolve(d)
      });
    });
    req.on('error', error => {
      console.error(error)
      reject(error)
    });
    req.end()
  })

}

function printTime() {
  var time = new Date()
  return time
}

for (let i = 1; i <= 10; i++) {
  request('req' + i)
}

request 함수를 호출할 때, for문을 통해 10번 호출하는데 request 함수는 프로미스 기반으로 동작하여 비동기식으로 HTTP GET Request를 전송합니다. 다음으로 서버를 구현하고 클라이언트를 실행해보겠습니다.

HTTP 요청을 처리하는 서버 만들기

Express 프레임워크를 활용하여 Http 요청을 처리하는 서버를 구현하겠습니다. HTTP 요청을 받으면 1초 Sleep 한 뒤, 응답을 전송하는 서버입니다.

const express = require('express');
const app = express();
const { Worker, isMainThread, parentPort } = require('worker_threads');

app.get('/request?', function (req, res) {
  console.log(printTime() + ' > got message : ' + req.query.menu)
  sleep(1000);
  console.log(printTime() + ' > check message : ' + req.query.menu)
  res.send(printTime() + ' > ' + req.query.menu + ' check' + '\r\n')
});


let port = 3000
app.listen(port, () => {
  console.log(`listening at http://localhost:${port}`);
});

function printTime() {
  var time = new Date()
  return time
}

function sleep(ms) {
    const wakeUpTime = Date.now() + ms;
    while (Date.now() < wakeUpTime) { }
}

작성한 서버를 실행한 뒤 클라이언트도 함께 실행해보겠습니다. 클라이언트 실행결과를 보면, HTTP 요청은 비동기식으로 16:14:31에 10건을 중단 없이 모두 요청합니다. 싱글 스레드 기반 서버의 경우, 서버로부터 응답이 온 메시지를 1초 간격으로 처리가 완료되는 것을 확인할 수 있습니다.

Node.js HTTP 요청 실행 결과
Node.js 싱글 스레드 기반 서버 실행결과

Worker Thread를 이용한 병렬 처리

Node.js는 싱글 스레드 기반으로 동작하지만, 사실상 다른 작업들을 처리할 수 있는 별도의 스레드 풀을 가지고 있습니다. worker_thread 모듈을 활용하면, 병렬로 작업을 처리할 수 있습니다. Node.js에서 멀티 스레드를 이용하여 http 요청을 처리하는 서버의 모습은 아래와 같습니다.

// MultiThreadServer.js
const express = require('express');
const app = express();
const { Worker, isMainThread, parentPort } = require('worker_threads');

app.get('/request?', function (req, res) {
    const seprateThread = new Worker(__dirname + "/worker.js");
    seprateThread.on("message", (result) => {
        res.send(printTime() + ' > ' + result + '\r\n');
    });
    seprateThread.postMessage(req.query.menu)
});
let port = 3000
app.listen(port, () => {
    console.log(`listening at http://localhost:${port}`);
});

function printTime() {
    var time = new Date()
    return time
}
// worker.js
function sleep(ms) {
    const wakeUpTime = Date.now() + ms;
    while (Date.now() < wakeUpTime) { }
}

function printTime() {
    var time = new Date()
    return time
}

const { parentPort } = require("worker_threads");


parentPort.on("message", (message) => {
    console.log(printTime() + ' > got message : ' + message)
    sleep(1000);
    console.log(printTime() + ' > check message : ' + message)
    let result = message + ' check'
    parentPort.postMessage(result);
});

/request GET 요청을 받으면, Node.js 실행을 담당하는 메인 스레드에서 자식 스레드를 실행함과 동시에 Query Parameter 값을 전달합니다. 자식 스레드는 worker.js 파일에 작성된 자바 스크립트 코드를 실행합니다. 멀티 스레드 서버를 실행하여, HTTP 요청 처리 결과를 보겠습니다.

Node.js 멀티스레드 기반 HTTP 요청 처리 결과

클라이언트에서 HTTP GET 요청을 10건 전송 완료하고, 서버는 HTTP GET 요청을 멀티스레드를 이용하여 병렬로 처리합니다. 싱글 스레드는 HTTP 요청을 순차적으로 처리하여, 전체 요청을 처리하는 시간이 10초 걸렸지만, 멀티 스레드의 경우는 멀티 코어를 적극 활용하여 작업을 병렬 처리하여 요청 처리 시간이 1초로 단축되었습니다.

Cluster를 이용한 병렬 처리

Cluster를 활용한 병렬 처리는 자식 프로세스를 생성하여 처리 성능을 높이는 방식입니다. 멀티 프로세스의 경우, 운영체제의 프로세스 스케줄링에 의해 작업 할당이 진행되기 때문에, 개발자가 원하는 대로 작업의 처리 성능이 나오지 않을 수도 있습니다. 하지만, 프로세스 간 격리되기 때문에, 메모리 초과로 하나의 프로세스가 종료되더라도, 다른 프로세스에는 영향을 미치지 않으므로 서버의 안정성을 높일 수 있습니다. Cluster 기반으로 여러 개의 HTTP 요청을 동시에 처리하는 서버를 만드는 방법은 아래와 같습니다. 

const cluster = require('cluster');
const os = require('os');
const express = require('express');
const app = express();

if (cluster.isMaster) {
  os.cpus().forEach(function (cpu) {
    cluster.fork();
  });

  cluster.on('exit', function (worker, code, signal) {
    console.log('worker exit : ' + worker.id);
  });
}
else {
  app.get('/request?', function (req, res) {
    console.log(printTime() + ' > got message : ' + req.query.menu)
    sleep(1000);
    console.log(printTime() + ' > check message : ' + req.query.menu)
    res.send(printTime() + ' > ' + req.query.menu + ' check' + '\r\n')
  });
  let port = 3000
  app.listen(port, () => {
    console.log(`listening at http://localhost:${port}`);
  });
}

function printTime() {
  var time = new Date()
  return time
}

function sleep(ms) {
  const wakeUpTime = Date.now() + ms;
  while (Date.now() < wakeUpTime) { }
}

서버와 클라이언트를 실행해보면, 멀티 프로세스는 부모 프로세스의 메모리, 소스코드를 복제하여 자식 프로세스를 새로 생성하는 개념이므로, 실행되는 소스코드가 동일합니다. 3000 포트를 리스닝하는 프로세스는 여러 개 생성되어 "listening at http" 문자열이 여러 번 출력된 것입니다. HTTP 요청 처리 결과를 보면, 1초마다 5건의 HTTP 작업이 처리된 것을 확인할 수 있습니다. 클라이언트를 실행할 때마다 HTTP 작업 처리를 수행하는 프로세스 개수가 달라지는데, 운영체제의 프로세스 스케줄링에 의해 작업 가능한 프로세스 개수를 조절하는 것으로 보입니다.

Node.js 멀티 프로세스 기반 HTTP 요청 처리 결과
멀티 프로세스 기반 서버로부터 응답 받은 결과

클라이언트의 HTTP 요청 건수를 1000 단위로 설정하여 실행해보면, HTTP Request 요청을 보내자마자 서버에서 응답을 처리하는 구조가 아닌 것 같습니다. 그리고, HTTP 요청을 10000건 정도 보내면, 서버에서도 요청 데이터를 받아 처리하는데 내부적으로 부하가 걸리는 것처럼 보입니다. 이는 node.js에서 HTTP 트랜잭션을 어떤 식으로 처리하는지 세부적으로 확인해 볼 필요가 있겠네요.

이상으로 Node.js로 HTTP 요청을 병렬 처리하는 방법에 대해 알아보았습니다.

728x90
반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함