Netty 통신 서버 개발 관련 주저리
Development/Netty & FlatBuffers

Netty 통신 서버 개발 관련 주저리

반응형

Netty?

Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.

 

netty는 JAVA 진영의 Transport Layer에서 가장 유명한(?) 프레임워크이다.

TCP 뿐만 아니라 HTTP, UDP 등 사실상 byte를 송/수신하는데에 두루두루 쓰이고 있는 상황이다.

 

수 많은 프레임워크들이 클러스터링 구성을 하거나 할 때 내부 통신(inner-communication)을 하는 프레임워크로 netty를 주로 사용하고 있다.

 

내부 구현은 JAVA NIO로 되어 있으며, 플랫폼에 따라 Epoll(Linux)이 추가로 Socket을 사용할 수 있도록 구현되어 있다

 

Node.js 동작 도식도

 

고성능/경량 HTTP 서버를 구축할 수 있는 Node.js의 경우의 동작 시퀀스가 위와 같은데, netty도 이와 같은 구조를 채택하고 있다.

 

하나의 EventLoop라는 Acceptor가 들어오는 incoming request에 대한 I/O를 담당하고(single thread) 해당 작업에 대한 처리는 worker thread에서 수행하게 한다.

 

gRPC의 내부 통신 레이어

 

구글의 RPC 프레임워크인 gRPC에서도 netty를 내부 transport로 사용하고 있다.

 

도식에서 보다시피 하나의 EventLoop에서 여러 request를 받아 worker thread로 분산시켜주는 것을 알 수 있다.

 

이와 같은 구조로 얻는 이점은

 

  • 자원 사용 효율성 증대(적은 오버헤드, low context switching)
  • CPU 코어 사용 증대(default로 worker thread는 CPU Core * 2로 잡힌다)
정도로 요약할 수 있겠다.
 
Java OIO(Old I/O)

 

 
JAVA로 통신을 하게 된다면 다음과 같은 시퀀스로 로직을 개발을 하게 되는데(완전 기초적이면서도 구식 프로그래밍 방식, Old I/O)
 
하나의 Client가 Connection을 수립하게 되면 accept()의 return으로 Socket이 반환이 된다.
 
이 Socket을 가지고 I/O를 지속적으로 감시하고 데이터를 송/수신 해야 한다.
 
그래서 해당 Socket의 I/O를 감시하기 위해선 Thread를 생성해서 관리해야 한다.
 
그렇지 않으면.. 어떤 Socket에서 이벤트가 일어났는지 알 수가 없게 된다.
 
이런 구조로 개발을 하게 되면 접속자 수에 따라서 엄청난 양의 Thread가 생성될 수 있고 그것은 바로 많은 Context-switching과 Heap 사용을 초래하게 된다.
 
비효율을 개선하기 위해 JAVA NIO가 등장하게 되는데(JDK 1.4)
 
Java NIO(New/Non-blocking IO)

 

위와 같은 구조로 개발을 할 수 있게 되었다.

 

큰 차이점은 Client의 Connection 수립마다 Thread를 생성하지 않아도 된다는 점이다.

 

Selector라는 클래스가 가지고 있는 Key에 대한 I/O 이벤트를 감시하고 있으며, Accept, Read, Write, Connection Closed 와 같은 이벤트를 감시해서 non-blocking하게 동작한다.

 


try (final Selector selector = Selector.open(); 
     final ServerSocketChannel serverSocket = ServerSocketChannel.open();) {
    final InetSocketAddress hostAddress = 
          new InetSocketAddress(Constants.HOST, Constants.PORT);
    serverSocket.bind(hostAddress);
    serverSocket.configureBlocking(false);
    serverSocket.register(selector, serverSocket.validOps(), null);

    while (true) {
       final int numSelectedKeys = selector.select();
       if (numSelectedKeys > 0) {
           handleSelectionKeys(selector.selectedKeys(), serverSocket);
       }
    }
}
위 예제처럼 Selector를 open하고 ServerSocket을 configureBlocking(false)로 주게 되면 non-blocking하게 동작하게 된다.
 
while(true) 안에서는 selector.select(); 가 호출되고 있는데, 이 부분에서 이벤트가 발생한 채널의 Event가 잡히게 되고
 
selectedKeys()로 이벤트가 발생한 key들의 리스트를 받아올 수 있다.
 
key에서는 Socket 클래스를 얻을 수 있으며 이를 통해서 I/O를 처리하면 된다.
 
위 구조로 변경을 하게 되면 over-head는 많이 줄게되나, CPU를 효율적으로 사용할 순 없다.
 
왜냐면 non-blocking으로 동작하게 되므로 하나의 Thread에서 Network I/O 및 소켓관리를 다 해야 하기 때문이다.
 
CPU 코어수가 늘어날 수록 위와같은 구조는 비효율을 보일 수 있다.
 
여기서 착안을 한 것이 Network I/O(Connection Accept, Closed) , Packet I/O(read, write) 를 분리해서 구조를 잡는 것이다.
 
사실 정상적인 케이스라면 Connection이 맺어지고 끊어지는 것보다, Packet의 read/write가 많은 케이스가 대부분일 것이다.
 
그러나 그것을 또 Thread로 해결을 하려고 하면 I/O 개수만큼 Thead를 생성 및 소멸 시키는데 비용이 많이 발생한다.
 
netty는 위의 상황에서 Thread pool을 적절히 활용함으로써 고성능을 발휘할 수 있는 구조로 만들어져 있다.
 

 

위 아키텍쳐에서 보다시피, Network I/O에 Thread pool을 적용했고 Worker Thread를 Socket I/O에 적용했다.

 

incoming 데이터는 Inbound Handler에서 처리하고, outgoing 데이터는 Outbound Handler에서 처리하도록 짜여져 있다.

 

 

Spring Boot?

사실 이건 Transport 프레임워크가 아니다.

 

이름에서 보다시피 "Spring" 프레임워크이며 HTTP Servlet을 다루기 위한 프레임워크인데 왜 사용했냐!? 라고 하면 다음의 이유이다.

 

  • 프레임워크의 높은 확장성으로, 여러 모듈을 활용할 수 있다(JPA, Batch, Scheduling, AMQP .. 등)
  • 개발할 때 Singleton을 일일히 만들면서 static class를 관리할 포인트를 줄여준다.
    • Manager Class의 경우 하나씩만 생성이 되야하는 부분이고, 개발할 양이 많아질 수록 관리하기 힘들다.
  • Netty가 Spring의 Singleton 방식의 Bean을 지원한다.
    • @ChannelHandler.Sharable 어노테이션으로 하나만 생성되서 사용할 수 있다
 
사실 Spring boot 프로젝트에서 제공하는 servlet 컨테이너(tomcat, undertow, jetty)만 제거를 하면 REST API만 제공을 안하고 Spring의 기능을 다 사용할 수 있다.
 
제공하는 어노테이션의 강력함과, 확장성이 더해지면 netty 개발에 큰 도움이 될 것이라고 생각하여 같이 사용하기로 했다.
 

FlatBuffers?

2016/11/18 - [Development/Java Netty & FlatBuffers] - [Flatbuffers] 플랫버퍼란?

 

을 참고하도록 하자.

 

이 라이브러리는 Packet의 Serialization(직렬화) / Deserialization(역직렬화) 에 사용한다.

 

통신 프로그램에서는 데이터를 어떻게 보내고 받을것인지에 대한 규약이 있어야 하는데

 

대부분 그냥 byte[] 로 보내게 되면 이걸 String으로 보내는지.. Object를 byte화 해서 보내는지.. JSON으로 변환해서 보내는지 등의 여러 방법이 있다.

 

그 중 성능이 뛰어난 Flatbuffer는 메세지를 미리 정의해두고, 해당 메세지를 컴파일하게 되면 언어에 맞는 클래스로 자동 변환을 해주는 기능을 지원한다(IDL)

 

따라서 개발할 때 패킷의 구조 변경이나 새로운 패킷을 추가할 때 엄청난 유연성을 제공하게 된다.

 

예로 들면 Flatbuffer를 사용하지 않을 경우 패킷의 변경이 있을 때이다.

  • Server쪽 JSON Object 내부 field를 변경
  • Client쪽에서 JSON Object의 내부 field를 변경(C#)
  • Client쪽에서 JSON Object의 내부 field를 변경(C++)
  • Client쪽에서 JSON Object의 내부 field를 변경(Go)
  • 송/수신할때 로직 처리 분기 추가
 
FlatBuffer를 사용하면 언어에 따라 변경되는 점에 대응하기 쉽다.
  • Server쪽에서 메세지 포맷을 변경
  • Flatbuffer IDL로 언어별 클래스를 생성
  • 각 클라이언트 언어에 맞게 배포 진행
  • 송/수신할 때 로직 처리 분기 추가
이는 지원하는 Client Platform이 많아질 수록 더욱 큰 힘을 발휘한다.
 
IDL을 사용하면 언어에 맞게 알아서 클래스를 생성해주기 때문이다.
 
(자세한 건 위 포스팅을 참고하도록 하자)

 


 

해서 위의 세가지 프레임워크&라이브러리를 사용해서 통신 프로그램을 만들어 보는 것이 목적이다.

 

채팅 or 게임 or inner communication 등의 여러 목적으로 쓰이는 것을 고려하여 확장성 있게 개발해 보는 것을 취미(?) 삼아..

 

프로젝트는 open-source로 github에 공개되며 주소는 ==> https://github.com/Gompangs/GNetServer

 

 

근데 코드를 직접 설명을 해야하나 싶긴한데

 

필요한 부분 부분 설명을 해야할 듯 하다.

 

(flatbuffer 프로젝트 연동해서 자동 빌드 환경 구축하기, netty 설정, packet dispatch 로직 구성 등)

 

 

일단 구상해본 서버 구성도는 위와 같다(사실상 netty component에 서비스를 추가해나가는 확장 구조이므로 크게 별건 없다)

 

p.s

원래 목적은 netty를 사용한.. 채팅이나 게임 서비스의 틀을 잡으려고 했는데

이게 비지니스 로직이 더 많은 프로젝트가 되면서 포스팅을 멈추게 되었다.

 

내용을 보면.. 핸들러와 flatbuffer 보다는 서비스 로직을 만드는 부분에 디펜던시가 커지고 있는 걸 느낌..

 

그만큼 netty를 사용하기가 간단하다는 소리다.

통신에 관련된 부분보다는 로직에 관련된 부분에 집중을 할 수 있게 해주니 말이다..

위의 구조까지 개발된 부분은 github에 있으니 참고하면 좋을 듯 하다.

 

 https://github.com/Gompangs/GNetServer

 

반응형

'Development > Netty & FlatBuffers' 카테고리의 다른 글

FlatBuffers 빌드 자동 구성  (0) 2018.04.15
[Flatbuffers] 플랫버퍼란?  (36) 2016.11.18