LOGIN • JOININ

서버 엔진의 성능을 제공하는 데에 있어 여러가지 요소들이 있을 수 있습니다.

전송을 위해 메시지를 쓰고 읽는 직렬화 처리, 쓰레드 처리, 접속 능력 등과 같은 여러 가지 요소들이 있지만

서버 혹은 서버 엔진의 성능에 있어 가장 중요한 요소는 무엇보다도 I/O성능일 것입니다.


 C++으로 작성된 CGCII과 boost::asio로 작성된 에코 서버 그리고 C#의 라이브러리로 유명한 Super Socket로 제작된 에코 서버의 성능을 테스트 해보기로 했습니다.


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



1. 테스트한 내용과 환경


1) 100개의 접속을 한다

2) 100개의 접속을 통해 크기가 8Byte인 메시지를 전송한다.

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


 100개의 연결을 만들어 8Byte크기의 메시지를 마구 전송해서 얼마나 많은 메시지를 에코 전송해 오는가 하는 성능을 테스트한 것입니다.

또 에코전송을 받은 메모리를 통채로 에코 전송하는 것이 아니라 메시지 단위로 전송 요청하도록 했습니다.

 왜 그럼 8Byte냐?  수십 KByte단위의 큰 사이즈의 메시지를 전송하는 에코 테스트의 경우 사실상 의미가 없습니다. 그것은 서버의 메시지 처리 성능이나 메시지 송수신 능력이 아니라 그냥 하드웨어 혹은 플랫폼의 I/O 성능을 따지는 것 뿐이 되지 않기 떄문입니다.

작은 단위의 메시지를 얼마나 잘 처리하고 얼마나 잦은 Send() 요청을 처리 할 수 있느냐를 테스트해야 의미가 있는 테스트가 될 것입니다.

 실제 서비스 되는 서버 역시 다운로드 서버가 아닌 이상 큰 덩어리의 메시리를 통채로 전송하는 경우는 거의 없기 때문에 실제 서버에서 사용되는 것과 유사하게 메시지 단위로 송수신하는 것이 실제 성능을 확인하는데 더욱 근접하기 때문입니다.


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


Windows 7 64bit

Intel i7-3770k 3.5GHz 4Core

16GByte


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

즉 서버 클라이언트를 모두 실행한 상황이고 일반적인 PC정도의 스펙에서 테스트이기 때문에 고성능 서버 하드웨어 하에서는 훨씬 더 높은 성능을 발휘할 것입니다.



2. Super Socket을 사용한 C# 서버

1) SuperSocket 간단한 소개

Super Socket은 C#을 사용해 고성능과 편리한 프로그래밍으로 유명한 공개 프레임워크형 라이브러리 중에 하나입니다.

뛰어난 성능과 확장성 있는 설계로 다양한 형태의 서버를 비교적 손쉽게 개발할 수 있도록 설계되어 있어 많은 호평을 받기도 했습니다.


Winform으로 제작했으며 네트워크  처리 부분의 Source는 아래와 같습니다.

using System;
using System.Threading;
using System.Text;

using SuperSocket.Common;
using SuperSocket.SocketBase;
using SuperSocket.SocketBase.Protocol;
using SuperSocket.Facility.Protocol;

public class CSessionTCP : AppSession<CSessionTCP, BinaryRequestInfo>
{
	public void OnMessage(BinaryRequestInfo _RequestInfo)
	{
		// 1) 전송을 위해 임시 버퍼를 생성한다.
		byte[]	bufSend	 = new byte[_RequestInfo.Body.Length+4];

		// 2) Head를 임시 버퍼에 써넣는다.
		BitConverter.GetBytes(_RequestInfo.Body.Length+4).CopyTo(bufSend, 0);

		// 3) Body를 임시 버퍼에 써넣는다.
		Buffer.BlockCopy(_RequestInfo.Body, 0, bufSend, 4, _RequestInfo.Body.Length);

		// 4) 임시 버퍼를 전송한다.
		TrySend(bufSend, 0, bufSend.Length);
	}
}

class CReceiveFilter : FixedHeaderReceiveFilter<BinaryRequestInfo>
{
	public CReceiveFilter()	: base(4)	// Header Size는 4로 설정한다.
	{
	}
	protected override int GetBodyLengthFromHeader(byte[] _Header, int _Offset, int _Count)
	{
		return BitConverter.ToInt32(_Header, _Offset)-4;
	}
	protected override BinaryRequestInfo ResolveRequestInfo(ArraySegment<byte> _Header, byte[] _Body, int _Offset, int _Count)
	{
		// Key는 그냥 null로, body는 복사해서 리턴한다.
		return new BinaryRequestInfo(null, _Body.CloneRange(_Offset, _Count));
	}
}

class CServer : AppServer<CSessionTCP, BinaryRequestInfo>
{
	public CServer()
		: base(new DefaultReceiveFilterFactory<CReceiveFilter, BinaryRequestInfo>())
	{
	}
	protected override void ExecuteCommand(CSessionTCP _Session, BinaryRequestInfo _RequestInfo)
	{
		_Session.OnMessage(_RequestInfo);
	}
}


2) SuperSocket 테스트 결과

SuperSocket을 사용해 간단하게 서버를 제작해 테스트를 했습니다. 

명성대로 SuperSocket으로 서버 제작하는 것은 매우 간략한 코드로 구현이 가능했습니다.


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

IOTestSuperSocket.png

위 스샷은 테스트 중 한 시점에서 캡처을 뜬 것이라 일정 기간의 평균은 아닙니다만 대략 중간치 쯤 되는 값을 캡처했습니다.

 8Byte 메시지를 초당 408만 2043개를 수신하고 초당 408만 2041개를 전송했군요. 

대략 초당 408만 메시지 정도를 수신하고 408만 메시지를 송신했습니다. 즉 초당 816만개 정도의 메시지 처리를 했습니다.

또 데이터 처리량으로 따진다면 초당 31메가 정도를 수신하고 송신했습니다. 즉 초당 62M(496M bps)정도의 I/O를 처리했습니다.

이때 CPU 사용율은 75%정도에 육박했습니다.

사용 메모리량은 16M Byte 정도였습니다.

알려진대로 C#에서 아주 간단한 코드로 매우 훌륭한 성능을 낼 수 있는 라이브러리인 듯합니다..



2. boost::asio를 사용한 C++서버

1) boost::asiio의 간단한 소개

asio는 'boost'의 대표적인 네트워크 라이브러리로 하나로 많은 사용자 분들이 사용하고 있습니다. .

기존의 오래된 API형태의 socket 표준은 Windows의 IOCP나 Linux의 ePoll과 같은 비동기 I/O를 고려하지 않고 설계되어 있어 비동기 I/O를 프로그래밍 할 수 없었습니다.

 boost::asio는 이런 비동기 I/O를 포함한 표준화된 인터페이스 제공을 시도했다는데 의미를 가집니다.

차기에 표준 C++ 라이브러리의 형태로 포함될 가능성도 있는 라이브러리기도 하죠.

boost::asio를 사용하면 Linux와 Windows 모두 지원해서 C++언어 차원에서 크로스 플랫품이 가능하다는 것도 장점 중에 하나일 것입니다. 


asio는 서버용 고성능 라이브러리라기 보다는 비동기를 지원하는 표준 API(?)라고 할수 있습니다.

따라서 서버로  사용되기 위하서는 메시지처리나 고성능 고안정성 다중 쓰레드 처리를 위한 구현을 모두 직접 해주어야 합니다.

일반 Native API로 서버를 작성하는 것과 생산성 면에서는 큰 차이는 없다고도 할수 있습니다. 일부 편리한 부분도 있지만 어떤 면에서는 오히려 족쇄가 되는 수도 있어 호불호가 갈리기도 합니다.

개인적으로는 크로스플랫폼이 목적이 아니라면 서버 제작을 위해 굳이 boost::asio로 작성할 필요성은 크지 않다고 봅니다. 

(만약 차후 boost::asio가 표준으로 지정이 된다면 상황은 바뀌겠죠.)


2) boost::asio로 테스트용 TCP Echo 서버 제작

boost::asio로 구현한  TCP Echo 테스트 서버는 boost 홈페이지에 예제(http://www.boost.org/doc/libs/1_55_0/doc/html/boost_asio/examples/cpp11_examples.html)로 제공된 tcp echo 서버를 바탕으로 다중 쓰레드 처리 등을 추가해서 간략하게 제작을 했습니다.
 boost::asio는 단순한 API 레벨이기 때문에 이미 프레임워크화 되고 성능을 위해 많은 처리를 한 SuperSocket, CGCII등 엔진과 동등한 테스트라고 하기에는 무리입니다만 특별한 처리를 하지 않고  C++로 제작했을 때 성능을 확인해 보기 위해 예제의 내용을  참고해 작성했습니다.


boost::asio로 TCP Echo 테스트 서버의 제작은  '콘솔'로 제작하였는데 Supersocket이나 CGCII 등에 비해 직접 만들어야 하는 부분이 좀 있어서 상당히 손이 많이 가고  코드량도 많아서 제작하기가 그렇게 용이하지 않았습니다.

작성한 Source는 아래와 같습니다.

#include "stdafx.h"
#include <deque>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/thread.hpp>
#include "Messages.h"

using boost::asio::ip::tcp;
typedef std::deque<message> message_queue;

//----------------------------------------------------------------------

class chat_participant
{
public:
  virtual ~chat_participant() {}
  virtual void deliver(const message& msg) = 0;
};

typedef std::shared_ptr<chat_participant> chat_participant_ptr;

//----------------------------------------------------------------------

class session
  : public chat_participant,
    public std::enable_shared_from_this<session>
{
public:
	session(tcp::socket socket) : socket_(std::move(socket))
	{
		InitializeCriticalSection(&csqueue_);
	}

	void start()
	{
		do_read_header();
	}

	void deliver(const message& msg)
	{
		EnterCriticalSection(&csqueue_);

		bool write_in_progress = !write_msgs_.empty();
		write_msgs_.push_back(msg);
		if (!write_in_progress)
		{
			do_write();
		}

		LeaveCriticalSection(&csqueue_);
	}

private:
	void do_read_header()
	{
		auto self(shared_from_this());
		boost::asio::async_read(socket_,
		boost::asio::buffer(read_msg_.data(), message::header_length),
		[this, self](boost::system::error_code ec, std::size_t /*length*/)
		{
			if (!ec && read_msg_.decode_header())
			{
				do_read_body();
			}
			else
			{
			}
		});
	}

	void do_read_body()
	{
		auto self(shared_from_this());
		boost::asio::async_read(socket_,
			boost::asio::buffer(read_msg_.body(), read_msg_.body_length()),
			[this, self](boost::system::error_code ec, std::size_t /*length*/)
			{
				if (!ec)
				{
					deliver(read_msg_);

					do_read_header();
				}
				else
				{
				}
			});
	}

	void do_write()
	{
		auto self(shared_from_this());
		boost::asio::async_write(socket_,
			boost::asio::buffer(write_msgs_.front().data(),
			write_msgs_.front().length()),
			[this, self](boost::system::error_code ec, std::size_t /*length*/)
			{
				if (!ec)
				{
					EnterCriticalSection(&csqueue_);

					write_msgs_.pop_front();
					if (!write_msgs_.empty())
					{
						do_write();
					}

					LeaveCriticalSection(&csqueue_);
				}
				else
				{
				}
			});
	}

	tcp::socket		socket_;
	message			read_msg_;
	message_queue	write_msgs_;
	CRITICAL_SECTION	csqueue_;
};

//----------------------------------------------------------------------

class echo_server
{
public:
  echo_server(boost::asio::io_service& io_service,
      const tcp::endpoint& endpoint)
    : acceptor_(io_service, endpoint),
      socket_(io_service)
  {
    do_accept();
  }

private:
	void do_accept()
	{
		acceptor_.async_accept(socket_, 
		[this](boost::system::error_code ec)
		{
			if (!ec)
			{
			std::make_shared<session>(std::move(socket_))->start();
			}

		    do_accept();
		});
}

  tcp::acceptor acceptor_;
  tcp::socket socket_;
};

//----------------------------------------------------------------------


int _tmain(int argc, _TCHAR* argv[])
{
  try
  {
    boost::asio::io_service		service;
	std::vector<boost::thread*>	threads;

    tcp::endpoint	endpoint(tcp::v4(), 20000);

    echo_server		s(service, endpoint);


	for (int i = 0; i < 20; i++)
	{
		boost::thread* t = new boost::thread(boost::bind(&boost::asio::io_service::run, &service));
		threads.push_back(t);
	}

	for (boost::thread* t : threads)
	{
		t->join();
	}
  }
  catch (std::exception& e)
  {
    std::cerr << "Exception: " << e.what() << "\n";
  }

  return 0;
}


3) boost:asio 테스트 결과

그 결과는...

IOTest04.png

     [이 스크린샷은 Test Echo 서버가 아닌 Test Client측의 스크린샷입니다.]

좀 처참한 결과... -_-;

C++을 사용했다는 것이 무색할 정도로 성능은 나오지 않았습니다.

대략 초당 50만개의 메시지를 수신했고 또 송신했습니다. 즉 초당 100만개 정도의 메시지를 송수신 한 것이죠.

데이터 처리량 즉 바이트로 보자면 초당 3.8M byte정도를 송수신 했습니다. 즉 초당 7.5M byte(60M bps) 정도의 I/O를 처리했습니다.

이때의 CPU 사용율 역시 55%~65%정도를 차지했습니다.

사용 메모리량은 11M Byte 정도였습니다.

혹자는 이거 Debug모드로 한거 아니야!라고 하실분도 있으시겠지마 아닙니다. 저도 의심스러워서 여러 번 확인했으니까요.


처리 성능으로 따지면 C#의 Supersocket을 사용한 것에 비해서 1/8 정도의 성능 밖에 되지 않는다는 것이죠.

더군다나 작성 코드량도 boost::asio에 비해 C#의 Supersocket이 훨씬 간단했다는 것을 생각하면 처참한 결과라 할수 있을 것입니다.


결과는 C++로 그것도 그 유명한 boost::asio로 작성했음에도 불구하고  C#의 SuperSocket을 사용한 것보다 비교도 되지 않게 처참한 처리 능력을 냈다는 것이죠.

(물론 이것은 boost::asio로 제작했다해도 성능을 위해 아무런 처리를 하지 않고 제작되었기 때문입니다.)


즉, 아무리 C++서버라 할지라도 제대로 처리 하지 못하면 C#서버만도 못할수 있다는 것을 적날하게 보여줍니다.




3. CGCII를 사용한 C++서버

마지막으로 CGCII를 사용한 서버의 성능 테스트입니다.

이 테스트 서버는 앞에서 "서버 성능을 테스트해보자 (1)" 에 첨부한 TCP 에코 서버를 사용했습니다.


아래는 튜터리얼의 Console로 제작한 TCP Echo Server의  전체 소스로 다른 엔진 혹은 라이브러리에 비해 훨씬 간단합니다. ^ ^

#include "stdafx.h"
#include "CGNetSocketTemplates.h"

// 1) Socket 클래스를 정의한다.(메시지 헤드는 uint32_t[4Bytes]로 설정 한다.)
class CSocket : public CGNet::Socket::CTCP<uint32_t>
{
	virtual int	OnMessage(CGMSG& _MSG) override	
	{
		// 2) 받은 메시지는 바로 Echo 전송한다.
		Send(((CGNETMSG&)_MSG).Buffer); 
		return	 0;
	}
};

int _tmain(int /*argc*/, _TCHAR* /*argv*/[])
{
	// 3) CSocket을 사용하는 Acceptor 객체를 생성한다.
	auto	pacceptor	 = NEW<CGNet::CAcceptor <CSocket>>();

	// 4) Acceptor를 20000번 포트에 Listen하기 시작한다.
	pacceptor->Start(20000);

	// 5) ESC키를 누를 때까지 대기한다.
	while(_getch()!=27);

	return 0;
}


CGCII에 대해서는 이 사이트 전체에 설명을 해놨으니 따로 설명을 드리지 않겠습니다.

테스트한 결과는...

IOTestCGCII03.png

압도적인 성능입니다.


초당 대략 2300만 개의 메시지를 수신하고 도 송신했습니다. 즉 초당 4600만개의 메시지를 처리했다는 의미입니다.

데이터 처리량으로 따지자면 초당 180M Byte 정도를 송수신 했습니다. 즉 초당 360M Byte(2.88G bps) 정도의 I/O를 처리했습니다.

이때 CPU의 사용률은 15%정도에 불과합니다.

사용 메모리량은 40M정도였습니다. (내부적인 Pool로 축적된 량을 감안하면 이보다 실제 사용량은 적습니다.)

두말할 필요 없이 압도적인 성능을 나타냅니다.

CGCII의 이 결과는 기본 튜닝 설정을 사용한 것입니다.

(단순히 최고 성능을 제공하기 위한 설정을 사용할 경우 이보다 더 높은 처리성능을 발휘할 수 있지만 많은 메모리 사용량을 소모하기 때문에 일반 접속보다는 서버간 접속에만 사용되므로 배제했습니다.)



4. 결론...

지금까지 테스트 결과를 표로 한번 정리해 보겠습니다.

IOTestPerformance01.PNG


이번 테스트에서 확인해 볼수 있는 중요한 것은...


어설프게 짠 C++ 서버보다는 SuperSocket과 같은 라이브러리를 사용한 C#이 더 좋은 성능을 발휘할 수 있다는 것입니다.

하지만 C#은 CPU 사용량이 과도하다는 점과 부하가 쏠려 발생하는 여러 상황에 대처하기 힘들다는 치명적 단점이 있고 가비지 콜렉션으로 인해 중간 중간 잦은 렉이 발생할 수 있어 실시간 서비스를 위한 서버로는 한계가 있습니다.

Super Socket을 사용한 결과를 C#의 성능으로 바로 받아 들이기도 무리라고 봅니다. 

C# 역시 SuperSocket과 같은 라이브러리를 사용해 구현하지 않고 간단히 작성된  네트워크 라이브러리의 경우 더 성능이 열악할 수 있기 때문입니다.

또 I/O 뿐만 아니라 다양한 서버 로직의 처리를 고려한다면 C++서버의 성능은  C#에 비해 월등히 좋을 수 있다는 점도 고려할 필요가 있습니다.


그리고 가장 중요한 결론은 CGCII의 성능은 압도적이다! (^^)라는 것이죠.

뛰어난 성능을 제공하면서도 CPU 사용률은 매우 낮은 압도적 성능을 제공합니다.

(고성능을 위해 CGCII에 적용된 고성능 풀 시스템, 효율적인 쓰레드 시스템, 고성능 I/O 등과 같은 다양한 기술들은 여러 차례 'GCGC'와 같은컨퍼런스에서  소개한 바가 있습니다.)


서버의 성능이 단순히 C++로 작성했기 때문에 C#보다 우수할 것이다?라고 생각하면 착각일 수 있습니다.

C++은 마치 빈종이와 같아 화공의 능력에 따라 쓸모 없는 낙서가 될수도 있고 최고의 명화가 될 수 있습니다.

C++로 작성했다해도 어떤 알고리즘이 적용되었고 어떤 식으로 처리했느냐에 따라 성능은 하늘과 땅만큼 차이날수 있기 때문입니다.

즉  C++은 각종 기술들을 적용해 엔진을 제대로 제작한다면 압도적인 성능과 적은 CPU 사용률 그리고 안정성까지 제공해주는 최고의 도구가 되지만 어설프다면 오히려 아닌만 못한 것이 된다는 것이죠.

 CGCII가 보여주듯 제대로 짠 C++ 서버 성능은 C#의 최고 수준 라이브러리라도 30배 이상(CPU사용량 대비까지 감안했을 때...)의 성능을 제공해주며 기본적인 라이브러리로 만든 C++서버에 비해 서는 150배 가량(CPU사용량 감안했을 때)의 성능을 제공해 줄 수도 있습니다.


서버의 성능이 나날이 발전하고 클라우드 서비스가 확대되고 있지만 

사용자의 요구 역시 나날이 커지고 있어 서버의 성능은 여전히 중요한 요소이고 성능만이 해결할 수 있는 부분 여전히 많습니다.

또 서버의 성능은 유지 비용과도 직결됩니다.


거의 모든 서버엔진 혹은 네트워크 라이브러리들은 고성능과 고안정성을 주장합니다. 

하지만 그것은 어디까지나 자체적인 기준에 의한 것일 뿐일 수 있습니다.

성능 혹은 안정성과 같은 문제는 유명하다고 혹은 유료라고 해서 반드시 좋은 것은 아닙니다.

따라서 네트워크 라이브러리 혹은 서버 엔진을 채택할 때는 반드시 여러 테스트 해보고 선택하시길 권합니다. ^ ^


또 앞으로 좀더 다양한 환경과 좀더 다양한 엔진 혹은 라이브러리로 테스트를 해 결과를 올리도록 하겠습니다.



5. 테스트한 소스

테스트용 CGCII는  "서버 성능을 테스트해보자 (1)" 에 첨부하였습니다.


[Supersocket TCP Echo 테스트 서버]

TCPEchoServer_withSuperSocket(WinForms).zip


[boost::asio TCP Echo 테스트 서버]

asioEchoTestServer.zip