LOGIN • JOININ

[Bench] Node.js의 I/O 성능 테스트

webmaster 2015.08.04 12:55 조회 수 : 6292

지금까지 C#과 CGCII 그리고 단순 asio, CGSF 등의 I/O성능을 테스트해 보았습니다.

Web 개발자들에게 널리 알려진 node.js의 I/O 테스트를 해보도록 하겠습니다.


테스트를 위한 클라이언트는 역시 앞에서 소개한 TCPEchoClient를 사용해 진행했습니다. (서버 성능을 테스트해보자 (1) )


1. 테스트한 내용과 환경


1) 테스트 클라이언트는 100개의 접속을 한다

2) 테스트 클라이언트는 100개의 접속을 통해 크기가 8Byte인 메시지를 전송한다.

3) 테스트 서버가 전송을 받으면 메시지 단위(8Byte)로 나누어 메시지 단위로 에코 전송을 수행한다.
     즉 1개의 메시지는 1개의 Send함수의 호출로 전송하도록 합니다.
     (메시지 단위로 Send를 건다는 이것이 매우 중요한 테스트 요소입니다.)


즉 100개의 연결을 만들어 8Byte크기의 메시지를 마구 전송해서 얼마나 많은 메시지를 에코 전송해 오는가 하는 성능을 테스트한 것입니다.
이 내용은 앞에서 수행했던 테스트와 동일한 내용이므로 테스트 의미에 대한 자세한 내용은 이전 테스트 글(C# SuperSocket vs CGSSi vs asio 테스트)을 참조해 주십시요.

테스트 환경은 다음과 같습니다.


Windows 7 64bit

Intel i7-3770k 3.5GHz 4Core

16GByte


테스트용 서버와 클라이언트는 하나의 하드웨어에서 동작을 시키도록 합니다.


2. node.JS

1) node.JS에 대한 간단한 소개

  node.JS는  v8 자바스크립트 엔진 위에서 동작하며
자바스크립트를 사용하여 간단히 서버를 개발할 수 있도록 개발된 
서버 프레임워크입니다. 

 기존 아파치와 같은 웹서버들은 접속자당 최소 1개 이상을 쓰레드를 사용하는 다중 쓰레드 구조를 가지고 있어 많은 접속자를 동시에 처리하기에는 상당히 비효율적인 구조를 가진데 비해 C10K을 추구하는  node.JS는 비동기적 I/O모델을 사용해 많은 접속 처리에 효율적입니다.

  또 동작 구조상 모든 접속이 독립 처리되는 웹서버와 같은 구조라면 굳이 다중 쓰레드를 사용하는 구조보다 단일 쓰레드를 가진 프로세스를 여러 개 실행하는 것이 더 효율적이고 편리할 수 있기에 단일 쓰레드를 추구하였습니다.

 node.JS는 이런 특징으로 인해
 기존 의 웹서버보다는 더 많은 접속과 성능을 가지면서도 소켓 함수를 사용해 프로그래밍을 처음부터 하지 않고서도 매우 간단히  소켓 프로그래밍까지 가능해 많은 개발자들의 호응과 관심을 끌어 내고 있습니다.
게임 서버 개발 쪽에서도 채팅서버와 같은 
간단한 실시간 서버의 개발용으로 많이 시도 되고 있습니다.

node.JS의 단일 쓰레드와 비동기식 구조는 장점도 많지만 콜백함수의 난무로 인한 코드의 난독화 문제나 스크립트 언어의 한계로 복잡한 프로그래밍이 쉽지 않다는 등의 단점도 있으며 단일  쓰레드의 한계점도 역시 분명히 존재해 호불호가 갈리기도 합니다.


2) node.JS 테스트 준비

node.JS로 테스트를 하기 위해 일반적인 소켓을 사용해 8Byte 데이터를 송수신하도록 테스트 코드를 작성했습니다.

1) node.JS는 Web이 아닐 경우 수신한 데이터를 메시지 단위로 나누어주는 부분이 없어 직접 작성했습니다.
2) 나누어진 메시지 단위로 메아리(echo) 전송하기 위한 코드를 작성했습니다.

 웹기반 통신을 사용한다면 'express'와 같은 프레임웍 등 다양한 지원을 해주기 때문에 상당히 편리할합니다.

뿐만 아니라 간단한 채팅 시스템과 같은 실시간 서버시스템의 제작도 손쉽게 제작이 가능합니다.
하지만 
 복잡한 게임이나 채팅시스템과 같은  서버의 제작은  다른 엔진 이나 라이브러리에 비해 손이 상당히 많이 가고  직접 구현해야할 사항이  많을 수 있고  오히려 코드가 더 복잡해 질수도 있습니다.
또 디버깅의 문제에 있어서도 스크립트 기반인 만큼 실행시간 중 오류가 발생하는 경우가 많아 제작이 상당히 까다로워 질수 있어 생산성이 급격히 하락할수 있다는 점도 간과해서는 안되리라고 봅니다.


이렇게 작성한 전체 소스 코드는 다음과 같습니다.

// 1. 그냥 TCP소켓을 사용할 것이므로 'net'을 사용
var app  = require('net');

// 2. TCP처리를 위한 서버를 생성한다.
var server = app.createServer(function (socket) 
{
    // @ Socket이 data를 받았을 때 처리 내용을 설정
    socket.on('data', function (_data) 
    {
        // 1) 저장된 미처리 데이터와 새로 받은 데이터를 합친다.
        var dataStored       = socket.buffer;
        var dataReceived     = new Buffer(dataStored.length+ _data.length);
        
        dataStored.copy(dataReceived, 0);
        _data.copy(dataReceived, dataStored.length);
        
        // 2) [남은데이터 크기]와 [오프셋] 변수 초기화.
        var remained = dataReceived.length;
        var offset   = 0;
        
        // 3) [남은 데이터 크기]가 헤더크기(4Byte)보다 클거나 같을 때까지 메시지를 읽어낸다.
        while (remained >= 4) 
        {
            // - 먼저 [메시지 길이]를 읽는다.
            var messagesize = dataReceived.readIntLE(offset, 4);
            
            // Check) [메시지 길이]가 [남은 데이터 크기]보다 작으면 끝낸다.
            if (messagesize > remained) 
            {
                break;
            }
            
            // - 메시지 부분을 잘라낸다.
            var message = dataReceived.slice(offset, offset + messagesize);
            
            // - 메시지를 전송한다.(메아리 전송)
            socket.write(message);
            
            // - [남은 데이터 크기]와 [오프셋]을 변경
            offset += messagesize;
            remained -= messagesize;
        }
        
        // 4) [미처리 데이터]를 저장해 놓는다.
        socket.buffer  = dataReceived.slice(offset, offset + remained);
    });
   
    // @ socket이 종료 되었을 대 처리 내용을 설정
    socket.on('end', function () 
    {
        console.log('- Closed');
    });
});

// @ Listen할 port는 20000번으로
var port = 20000;

// @ Listen을 했을 때 처리 내용 설정
server.on('listening', function () 
{
    console.log('@ Starting Node.JS Echo Server [', port, ']');
});

// @ 새로운 접속을 받았을 때 처리 내용 설정
server.on('connection', function (_socket) 
{
    // Trace) 
    console.log('- Connected');

    _socket.buffer = new Buffer(0);
});

// @ 서버를 닫았을 때 처리 내용 설정
server.on('close', function () 
{
    // Trace) 
    console.log('@ Closing Node.JS Echo Server');
});

// @ 에러 발생 시 처리 내용 설정
server.on('error', function () 
{
    // Trace) 
    console.log('[Error] ', err.message);
});

// 3. 서버를 시작한다.
server.listen(port);




3) node.JS 테스트 결과

node.JS는 기본적으로 스크립트 기반인데다 단일 쓰레드로 동작하도록 되어 있어 기존의 웹서버에 비해서는 우수한 편입니다만 일반 어플리케이션 서버와 비교한다면 여러가지 불리한 면이 있어 성능에 있어서는 많은 차이를 보였습니다.

100개의 접속으로 8Byte크기의 메시지를 대규모로 전송한 테스트를 한 결과는 다음과 같습니다.

nodejs.PNG

위의 스크린샷은 송신한만큼 다시 에코 수신되는 량이 최대 수치일 때를 잡아 캡춰를 한 것입니다.


8Byte 메시지를 초당 9만 8천개 가량을 수신했고 초당 9만 8천개 정도를 전송했습니다. 
즉 대략 초당 20만개 정도의 메시지를 처리했습니다.
데이터 처리량으로 따지면 초당 770KByte 정도를 수신하고 770KByte 정도를 송신했습니다. 
즉 초당 1.5MByte 정도의 I/O를 처리했습니다.
이때 CPU 사용율은 8% 수준에 달했습니다.
아무래도 스크립트 기반이다 보니 처리량에 비해서는 상당히 많은 CPU를 소모하는 듯합니다.

 메모리 사용량은 40M 수준이었습니다.


전반적인 성능 자체는 스크립트 기반에 단일 쓰레드인 것을 감안하면 생각보다 상당히 훌륭한 편으로 생각됩니다. 



3. 테스트 결과 비교

node.JS는 일반적인 실행파일 기반의 네트워크 엔진과 비교할 수는 없지만 기본의 웹서버에 비하면 또 스크립트 기반의 단일 쓰레드인 것을 감안하면 상당히 훌륭한 성능을 보여준다고 봅니다.

IOTestPerformance03.PNG


전반적인 성능은 아무런 처리를 하지 않은 asio(C++)에 비해 1/5정도의 성능을 나타내며 C#의 라이브러리인 SuperSocket에 비해서는 1/40 정도의 성능을 나타냅니다. 또 CGCII에 비해서는 1/230 정도의 성능을 나타냅니다.


4. 테스트 서버
Visual Studio의 Project 파일을 첨부합니다.

[node.JS Echo Server]
NodeJSEchoServer.zip