ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Netty 정리
    Programming Language/java 2024. 4. 23. 16:45

    Netty 관련 내가 이해하고 있는 내용을 정리해본다.


    1. Netty가 뭔가요?
    2. Netty를 왜 쓰나요?
    3. Netty가 어떻게 동작하나요?

    위 3가지 관점에서 주요한 내용을 정리해보겠습니다.

     

    1. Netty가 뭔가요?

    Netty는 NIO기반 네트워크 애플리케이션 프레임워크입니다. (NIO는 New Input Output 입니다.)

    다양한 프레임워크에서 Netty를 사용하고 있고 Spring WebFlux에서도 Netty를 디폴트 코어로 사용하고 있습니다.

     

    2. Netty를 왜 쓰나요?

    Netty는 고성능 서버를 구현하기 위해 사용됩니다. 고성능이라는 말은 같은 자원을 효율적으로 사용한다는 말과 같습니다.
    그렇다면 무엇에 비해 효율이 좋다는 것일까요? 

    이를 이해하기 위해 기존의 멀티스레드 기반 애플리케이션 서버에 한계점에 대해 알아야 합니다.

     

    한계 1. 스레드 생성 비용이 비싸다

    멀티스레드 기반 서버에선 보통 하나의 요청당 하나의 스레드를 할당하여 요청에 필요한 작업들을 수행합니다. 
    자바에서 스레드 하나당 메모리 크기는 보통 512KB~1MB 라고 합니다. 만약 동시 요청이 몰려서 10000개의 요청이 들어온다고 하면 스레드가 잡아먹는 메모리만 5GB~10GB가 됩니다.
    스레드풀을 사용한다고 하더라도 기본적으로 스레드 비용이 비싸기 때문에 풀 사이즈를 제한할 수 밖에 없게 됩니다.

     

    한계 2. 블로킹 I/O가 비효율적이다.

    스레드에서는 일반적으로 블로킹 I/O 함수들을 사용하게 됩니다. (자바 스레드는 OS스레드의 Wrapper라서 ?)

    블로킹 I/O에 의해 블로킹 되는 동안 이후 작업(코드)이 진행되지 않습니다.

    실제로 블로킹 되는 동안 CPU 자원을 소모하며 대기하는 것은 아닙니다. I/O 작업을 기다리면서 해당 스레드는 대기 또는 블록 상태로 전환하고 CPU 자원을 다른스레드에게 넘기게 됩니다. 이때 컨텍스트 스위칭 비용이 발생하고 이에 따라 CPU 자원을 부가적으로 소모하게 됩니다. 그만큼 CPU를 비효율적으로 사용되게 됩니다.

    메모리 역시 마찬가지로 각 스레드가 CPU를 비효율적으로 사용하는만큼 메모리를 차지하고 있는 것이기 때문에 비효율적으로 사용된다고 볼 수 있습니다.

     

    한계 3. 커널 버퍼에 접근 불가
    소켓이나 파일로 데이터가 들어오면 커널은 커널 버퍼에 데이터를 쓰게 되는데, 기존 I/O 방식에서는 커널 버퍼에 직접 접근이 불가능합니다. 따라서 아래와 같은 비효율이 발생합니다.

    • JVM 내부 버퍼로 데이터를 복사. 이때  CPU 자원 소모
    • 복사를 할 때 블로킹 I/O를 사용하여 스레드가 블로킹 됨
    • 복사한 버퍼 활용 후 GC 대상이 되어 CPU 소모

    JDK 1.4부터 ByteBuffer를 제공하여 커널 버퍼에 직접 접근이 가능하게 됩니다.
    Netty도 ByteBuffer 를 발전시킨 ByteBuf이라는 컨테이너를 사용합니다.

     

    이러한 이유로 기존 IO 방식 대신 NIO 기반의 방식을 사용하게 되고, NIO 기반 프레임워크를 사용하여 고성능을 기대하게 됩니다.

    위의 한계점들을 극복하기 위해 최근에는(자바21) 가상 스레드라는 경량 스레드 모델이 등장했다고 하는데... 나중에 공부해봐야 할 듯


    3. Netty가 어떻게 동작하나요?

    비동기, 이벤트 기반으로 동작합니다.
    Netty에는 이벤트 루프와 채널 개념이 있는데, 채널은 소켓 연결을 추상화한 개념이고 이벤트 루프는 각 채널에 이벤트가 있는지 반복적으로 검사하는 역할을 합니다. 보통 하나의 이벤트 루프는 하나의 스레드로 구성됩니다. 이벤트 루프는 시스템의 코어의 수만큼 생성하여 하나의 코어에서 하나의 이벤트 루프가 지속적으로 실행되도록 하면 효율적입니다. (네티에선 코어 수*2 만큼 생성하는 것이 디폴트인데 이게 최적이라 생각하나 봄)

    • 컨텍스트 스위칭 감소: 이벤트 루프가 동일한 코어에서 지속적으로 실행되면, 컨텍스트 스위칭 비용이 줄어들어 전체 시스템의 성능이 향상됩니다.
    • 캐시 활용 최적화: CPU 캐시는 최근에 사용된 데이터와 명령어를 저장합니다. 같은 코어에서 지속적으로 작업을 수행하면, 캐시 미스가 줄어들어 처리 속도가 빨라집니다.
    • 동시성 관리: 동일한 코어에서 이벤트 루프가 실행되면, 이벤트 루프 내에서 발생하는 동시성 문제를 효과적으로 제어할 수 있습니다.

    각 채널의 이벤트 검사는 OS가 제공하는 Non-Blocking 방식으로 각 채널의 이벤트(읽기, 쓰기 등)를 검사하게 됩니다. OS가 제공하는 함수는 select, poll, epoll 과 같은 함수가 있는데 select,poll 보다 epoll이 가장 효율적이라는 것만 알고 넘어갑시다. (나중에 따로 정리)

    (select, poll은 파일 디스크립터 셋을 루프로 계속 돌면서 이벤트가 발생했는지 애플리케이션단에서 확인해야함. epoll 은 커널이 알아서 이벤트가 발생한 파일 디스크립터 목록을 반환해줌. 커널 내부적으로 이벤트 기반 방식을 사용할 것으로 예상됨. 아마 하드웨어 인터럽트를 사용하지 않았을까.)

     

    이벤트 루프가 채널의 이벤트를 감지하면 해당 이벤트에 대응하는 핸들러에 따라 처리됩니다. (채널과 이벤트 루프 중간에 이벤트큐가 존재한다고 함). 즉 특정 코어에서 이벤트 루프가 돌면서 이벤트를 감지하고, 해당 코어에서 감지된 이벤트를 처리하고 다시 루프를 돌면서 감지하는 상태로 돌아가여 반복하는 것입니다. 물론 이벤트를 처리하는 과정에서 다른 블로킹 I/O 작업이 있다면 소용이 없겠죠. (DB 작업이라던지, 외부 서버에 네트워크 요청이라던지) 이 과정에서도 역시 이벤트 기반의 비동기 방식으로 작업을 해주어야 합니다. 이로써 스레드 기반 모델에서 벗어나고 블로킹 I/O 대신 논블로킹 I/O 방식을 사용하면서 (네트워크 I/O 한정) CPU, 메모리의 비효율을 최대한 줄이고 높은 성능을 내게됩니다. 

    채널 파이프라인을 구성하여 파이프라인을 따라 일련의 작업들을 처리하는 것도 가능합니다.
    Netty의 여러가지 기능은 다음에 살펴보는 것으로 하고 이만 마무리 해보겠습니다. 

    정리
    1. Java에서 기존 스레드 기반 모델은 비효율적이다.
    2. nio기반 비동기, 이벤트 방식으로 기존보다 효율적인 구조로 해결 가능하고 Netty는 이를 쉽게 다루게 해주는 프레임워크다.

Designed by Tistory.