Spring WebFlux + Coroutine 환경 비동기 개발에서 살아남기 💪 - Netty와 Java NIO

Hyebin Lee·2022년 10월 22일
0
post-thumbnail

NIO

IO vs NIO

  • 스트림 vs 채널
    IO는 스트림 기반이고 NIO는 채널 기반이다. IO는 입력 스트림과 출력 스트림을 따로 생성해야 하지만 NIO는 양방향으로 입력과 출력이 가능하다. 별도의 채널을 만들 필요가 없다
  • non버퍼 vs 버퍼
    IO에서는 출력스트림이 1바이트를 쓰면 입력 스트림이 1바이트를 읽는다. 버퍼가 없기 때문이다. 따라서 IO는 보조스트림인 BufferedInput/OutputStream을 사용하기도 한다. 하지만 NIO는 기본적으로 버퍼를 이용하므로 IO보다 성능이 좋다. 하지만 이러한 버퍼는 많은 양의 데이터를 처리하는 경우 모든 입출력에 버퍼를 사용해야하므로 문제가 된다.
  • 블로킹 vs 논블로킹
    IO는 블로킹이라 읽거나 쓸 때 블로킹된다. IO 쓰레드가 블로킹 되면 스트림을 닫기 전까지는 할 수 있는 일이 없다.
  • BIO : 자바는 소켓이나 파일에서 Stream이 들어오면 커널 버퍼에 데이터를 써야하는데 IO 가상머신의 한계로 OS의 커널버퍼에 직접적으로 핸들링할 수 없었다. 그 대안으로 BIO는 JVM이 커널에 시스템콜을 하게 했다. 커널버퍼에 있는 데이터를 JVM 버퍼에 복사를 해서 썼는데 내부 버퍼 복사시 CPU가 관여하기 때문에 CPU 오버헤드가 나고 Buffer는 활용 후 GC의 대상이 된다는 문제가 있다. 복사중인 IO요청 스레드는 블로킹 상태로 남아있기 때문에 처리 속도도 저하된다.
    이러한 이유로 IO는 블로킹으로만 작동할 수밖에 없었다.
    NIO는 OS별 커널버퍼에 접근이 가능해졌다. 블로킹과 논블로킹 특징을 모두 가지고 있다. NIO 블로킹은 스레드를 인터럽트할 수 있다는 것. 논블로킹의 경우 입출력 작업 준비가 완료된 채널만 선택해서 작업 스레드가 처리하기 때문에 작업 스레드가 블로킹 되지 않는다. 이 핵심은 멀티플렉서인 Selector에 있다

java.nio.channels.Selectors에서 입출력을 하고 있는 Socket의 집합 상태를 확인하기 위해 이벤트 통지 API를 사용하는데 그렇기 때문에 클라이언트마다 스레드를 생성할 필요도 없고 필요할 때마다 스레드에게 통지해주기 때문에 비동기적 통신 구조를 갖는다.

다시 말해 하나의 thread가 IO 결과를 지켜보는 것을 담당하고 특정 채널로 그 결과를 추후에 전달하는 구조이다

SocketChannel

ServerSocketChannel

  • 서버측에서 사용됨
  • 서버측 포트와 바인딩을 한 후에 연결요청이 오면 SocketChannel을 생성하여 클라이언트와 통신

SocketChannel

  • 클라이언트, 서버 측 모두에서 사용되며 read/write 등의 기능

블로킹 방식의 통신


서버는 bind를 통해 자신의 OS 포트에 연결 후 accept()를 통해 클라이언트 요청을 기다림 (blocking)
클라이언트 또한 bind를 통해 자신의 OS 포트에 연결, connect()를 이용하여 서버측에 연결
서버는 요청을 받은 뒤에 새로운 SocketChannel 생성
이후 상호 통신

비동기 방식의 통신


서버 앞단의 Selector가 요청에 따라서 알맞는 Channel을 반환

selector

이 하나의 쓰레드를 selector라고 하고
유일하게 blocking되는 쓰레드이다
selector는 Reactor 패턴의 구현체이다
selector는 어느 channel set이 IO event를 가지고 있는지 알려준다.
Selector.select()는 IO 이벤트가 발생한 채널 set을 리턴
return 할 channel이 없다면 계속 block된다.

하나 이상의 채널을 셀렉터에 등록하고 select() 메서드를 호출해서 등록된 채널 중 이벤트 준비가 완료된 하나의 채널이 생길때까지 block됨

selection key

Selector와 Channel 간의 관계를 표현해주는 객체
ServerSocketChannel에 selector가 등록되면 key를 준다
Selector가 제공한 Selection Key를 이용해 Reactor는 채널에서 발생하는 IO이벤트로 수행할 작업을 선택

적은 수의 스레드로 더 많은 연결을 처리할 수 있으므로 메모리 관리와 컨텍스트 전환에 따르는 오버헤드 감소
입출력을 처리하지 않을 때는 스레드를 다른 작업에 활용할 수 있다
https://velog.velcdn.com/images/eeheaven/post/2f1b91f7-8978-47e7-9de7-4f8b02eb8c68/image.png
1. selector라는 이벤트 thread가 socket channel 들과 연결을 맺는다
2. 읽고 쓰는 과정에서 socket이 준비 완료되면 selector에게 이벤트를 알린다
3. selector가 이벤트를 리슨해서 관련 thread에게 알린다
4. thread가 관련 데이터를 버퍼에 쓰거나 버퍼에서 읽는다

논블로킹 네트워크 연결은 작업 완료를 기다릴 필요가 없게 해준다. 완전 비동기 입출력은 이 특징을 바탕으로 한 단계 더 나아간다. 비동기 메서드는 즉시 반환하며 작업이 완료되면 직접 또는 나중에 이를 통지한다.
셀렉터는 적은 수의 스레드로 여러 연결에서 이벤트를 모니터링할 수 있게 해준다.
논블로킹 입출력을 이용하면 블로킹 입출력 방식을 이용할 때보다 더 많은 이벤트를 훨씬 빠르고 경제적으로 처리할 수 있다. 이것은 네트워킹 관점에서 우리가 구축하려는 시스템의 핵심이며, 앞으로 알아보겠지만 네티 설계의 핵심이기도 하다.

All about Netty

Netty는 Reactor 모델의 구현이며 NIO 네트워크 기반 프레임워크
더 나은 처리량, 더 낮은 대기시간, 자원 소비량 감소, 불필요한 메모리 복사 최소화
Netty는 기본적으로 Selector를 멀티플랙서로, Eventloop를 리피터로, PipeLine을 이벤트 프로세서로 사용한다.

왜 Netty인가

Tomcat은 요청 당 하나의 스레드가 동작하는데 netty는 이벤트를 받는 스레드와 다수의 worker스레드로 동작한다
따라서 인바운드와 아웃바운드시 thread 간 추가적인 컨텍스트 스위칭이 불필요하며 이벤트를 처리하는 ChannelHandler간의 동기화가 필요없다
PipeLine에서 책임 체인 모델을 사용하기에 버퍼가 최적화되어 성능이 크게 향상되었다.

Netty Server Architecture

Asynchronous Non-blocking

  • 기존 자바에서의 소켓은 블로킹 형식이다 ServerSocket , Socket
    클라이언트가 서버에게 연결 요청을 할 때 서버가 연결을 수락하고 클라이언트와 연결된 소켓을 새로 생성하는데 이 때 처리가 완료될 때까지 해당 스레드는 블로킹된다. 또, 클라이언트마다 소켓을 새로 생성해주어야 하고 그마다 쓰레드를 할당해주어야 한다, 소켓 heap과 gc도 문제고 쓰레드 context switching도 문제...
  • NIO 논블로킹 IO API -> 소켓도 입출력 채널의 하나로 NIO API를 사용 ServerSocketChannel , SocketChannel
    ServerSocket 채널 객체를 Selector 채널에 등록하고 이벤트 감지시킨다

BootStrap/ServerBootStrap

Netty의 socket mode나 thread 설정을 쉽게 할 수 있고 Eventhandler도 설정해줄 수 있다.

  • bootStrap
    EventLoopGroup을 하나만 전달
    클라이언트를 위한 부트스트랩
  • ServerBootStrap
    EventLoopGroup 두개를 전달
    서버 어플리케이션을 위한 부트스트랩
    ServerBootStrap은 BossGroup과 workerGroup으로 나누어지는데 bossGroup은 클라이언트의 연결 여부를 지속적으로 모니터링하고 새로운 클라이언트가 발견되면 이를 workGroup에서 EventLoop을 선택하여 클라이언트에 바인딩한다. 상호 서버-클라이언트 상호작용은 EventLoop가 처리한다
    논블로킹 소켓, 블로킹 소켓, Epoll(리눅스) 소켓을 설정할 수 있음

Channel

  • Socket의 추상화
  • 소켓의 입출력 모드 설정
  • 사용자에게 Socket의 상태와 Socket에 대한 읽기 및 쓰기와 같은 작업을 제공

ChannelHandler

  • 네티의 강점은 Pipeline 기반이라는 것
  • ChannelHandler를 기반으로 다양한 핸들러를 자유롭게 결합 가능
  • 예를들어 HTTP 데이터 처리시 파이프라인 앞에 HTTP 인코딩 및 디코딩 처리기를 덧붙여서 성공적으로 파이프라인을 통해 데이터가 흘러 비즈니스 로직 처리까지 도달하도록 한다

group

  • 이벤트 루프에 대한 설정, 이벤트 루프 그룹핑
  • bossGroup은 client의 커넥션 담당, workerGroup은 IO 이벤트 처리 담당

이벤트 기반 프로그래밍

  • 이벤트 발생 주체: 소켓
  • 이벤트 종류: 소켓연결, 데이터 송수신
    네티는 데이터를 소켓으로 접근하기 위해 채널에 직접 쓰기/읽기 하지 않고 데이터 핸들러를 통한다
    따라서 이벤트에 따라 로직이 분리되어 코드가 깔끔해지고, 에러이벤트 등도 같이 핸들링할 수 있다

Component

Netty는 크게 다음과 같은 Component들을 통해 데이터를 처리한다.

  • Channel, EventLoop, ChannelFuture
  • ChannelHandler, ChannelPipeline
  • Bootstrap

Channel

기본 입출력작업은 Socket 클래스를 이용

EventLoop

이벤트 루프는 연결의 수명주기 중 발생하는 이벤트를 처리하는 Netty의 핵심 추상화를 정의

ChannelFuture

Netty의 모든 입출력 작업은 비동기적이다. 이를 위해 Netty는 ChannelFuture를 제공하며, 이 인터페이스의 addListener() 메서드는 작업이 완료되면(성공 여부와 관계없이) 알림을 받을 ChannelFutureListener 하나를 등록한다.

ChannelHandler

인바운드와 아웃바운드 데이터의 처리에 적용되는 모든 Application 논리의 컨테이너 역할
ChannelHandler의 메서드가 네트워크 이벤트에 의해 트리거

ChannelPipeLine

Channel을 통해 오가는 인바운드와 아웃바운드 이벤트를 가로채는 Channelhandler 인스턴스의 체인
ChannelPipeline은 ChannelHandler 체인을 위한 컨테이너를 제공하며, 체인 상에서 인바운드와 아웃바운드 이벤트를 전파하는 API를 정의한다. Channel이 생성되면 여기에 자동으로 자체적인 ChannelPipeline이 할당된다.
Netty는 ChannelInboundHandler와 ChannelOutboundHandler의 구현을 구분하며, 핸들러 간의 데이터 전달이 동일한 방향으로 수행되도록 보장

동작 원리

netty에서는 한 connection 안에서 발생하는 inbound, outbound 데이터 흐름을 핸들링할 수 있는 Handler들이 체인 형식으로 파이프라인을 형성하고 있다.
이때 한 connection에서 발생하는 다양한 이벤트들을 하나의 Eventloop 즉 하나의 쓰레드가 처리하는데 이때 이벤트는 각 handler들의 액션을 말한다. 예를 들어 어떤 Handler가 데이터를 넘겨받는 것도 이벤트고, handler 내에 buffer에 데이터를 쓰는 것도 이벤트이다.
여러 쓰레드에서 handler의 이벤트를 처리하게 한다면 thread-safe 해야하고 로직 순서가 보장되지 않는 문제가 있어서 보통 하나의 thread connection을 사용한다.

이벤트

  • Inbound Event : 연결 상대방이 어떤 동작을 취했을 때 발생
  • Outbound Event: 프로그래머가 요청한 동작에 해당하는 이벤트

채널 파이프라인

  • 이벤트 핸들러: ChannelInboundhandler , ChannelOutBoundHandler 인터페이스로 나눔
    네티의 채널과 이벤트 핸들러 사이의 연결 통로
    채널에서 발생한 이벤트가 채널 파이프라인을 통해 흘러가고 이벤트 핸들러는 이벤트를 수신한 후에 본인이 처리해야하는 이벤트인지 판단하고 처리
    이벤트 루프 스레드가 blocking하게 동작하면 channel에서 발생한 이벤트들이 제때 처리되지 못하고 밀릴 수 있기 때문에 파이프라인을 사용
    이벤트 핸들러는 채널 파이프라인에 등록된다
    채널 파이프라인은 자신에게 등록된 이벤트 핸들러들의 묶음

    채널에서 발생하는 이벤트는 채널 파이프라인을 따라 흐른다
    흐른 이벤트들을 수신하고 처리하는 기능을 이벤트 핸들러가 한다

이벤트 루프


이벤트를 실행하기 위한 무한루프 스레드
이벤트 큐에 이벤트를 등록하고 이벤트 루프가 큐에 접근하여 처리
이벤트 루프가 다중 스레드면 한 큐를 여러 스레드에 공유해서 쓰면 이벤트 실행순서 불일치 문제가 발생하여 이벤트 큐를 공유하지 않고 이벤트 루프 쓰레드 내부에 둔다

네티의 NioEventLoop 자체는 SingleThreadEventExecutor이므로 NioEventLoop의 시작은 실제로 NioEventLoop에 바인딩 된 로컬 java 스레드의 시작이다.
인스턴스의 실행 메서드는 NioEventLoop에 의해 실행되도록 예약된 작업 대기열에 작업을 추가한다
일반적으로 이벤트 루프는
1. Channel로부터 발생한 Event를 받아 IO
2. taskQueue에 들어있는 Task를 처리하는 non IO
두 가지 작업을 담당

startegy에는 다음 단계에서 어떤 프로세스로 실행되어야 하는지 결정하는데 세 가지 종류가 있다
select : blockingIO가 실행되어야 할 때, 실행해야 할 task는 없기때문에 대기 상태
continue: IO loop가 blocking IO를 실행하지 않고 다시 실행되어야할 때
busy_wait: blocking 없이 새로운 event를 Polling

hasTask가 true면 selectSupplier를 호출해서 event count를 얻어온다
hasTasks가 false면 처리할 task가 없기때문에 blocking상태로 event를 기다린다

네티는 Java NIO의 Selector.select를 지속적으로 호출하는데 사용되는 Selector 스레드가 있다. 그리고 현재 준비된 IO이벤트가 있는지 쿼리한다.
1. selector.open()을 통해 selector를 열고
2. 채널을 선택기에 등록하고 모니터링해야하는 이벤트를 설정한다 (관심 설정)
3. 아래 과정을 반복
1) 스레드 실행 루프 : SingleThreadEventExecutor.this.run() 호출 (NioEventLoop이 구현)
run 이 실행되면 shutdown 시그널을 받기 전까지 무한 loop를 돈다
run 메서드의 첫번째 단계: hasTasks()로 현재 작업 대기열에 작업이 있는지 확인
작업 대기열이 비어있지 않은 경우 selector.selectNow() 메서드 호출
selector.selectNow() : 현재 준비된 IO이벤트가 있으면 반환, 없으면 0
select() : selector.select(timeoutMilis) 호출, 해당 호출은 스레드를 block

  • select() 메서드 호출
  • selector.selectedKeys()를 호출하여 선택한 키를 가져온다
  • 선택한 각 키를 반복

hasTask가 true인 경우 전달받은 selectSupplier를 호출해서 event Count를 얻어온다
hasTask가 false인 경우에는 blocking 상태로 event를 기다린다
이 select 메서드에서 selector.select(timeoutMilis)가 호출되면 이 호출은 현재 스레드를 차단하고 timeoutMilis는 차단 타임아웃을 의미한다. hasTasks() 가 true면 selectNow()메서드는 현재 스레드를 블로킹하지 않는다
hasTasks() -> false면 oldWakenUp 호출시에 현재 스레드를 블로킹

작업 대기열에 작업이 없으면 네티는 IO 준비 이벤트를 블로킹하고 기다린다
작업이 있으면 non-blocking selectNow() 메서드를 호출해서 taskQueue의 작업을 처리한다.

참고 링크

https://hbase.tistory.com/116
https://narup.tistory.com/118
https://velog.io/@monami/Netty
https://effectivesquid.tistory.com/65
https://robin00q.tistory.com/81?category=978531
https://sightstudio.tistory.com/15
https://woooongs.tistory.com/73?category=1009976

0개의 댓글