findmypiece 2022. 7. 21. 03:40
728x90

웹플럭스는 프로젝트 리액터와 네티로 구성된다. 이중 네티로 논블로킹-비동기 처리를 지원하고 덕분에 적은 스레드로 많은 트래픽을 처리할 수 있다. 이에 네티에 대해 간단하게 정리해본다.

 

네티는 각각의 요청을 채널로 받고 채널에서 발생하는 이벤트들을 이벤트 큐에 쌓아놓는다. 그리고 이벤트 루프가 이를 가져와서 처리하는 구조이다. 여기에서 이벤트 루프란 이벤트를 실행하기 위한 무한루프 스레드를 말한다.

 

이벤트 루프는 지원하는 스레드 종류에 따라 단일 스레드 이벤트 루프와 다중 스레드 이벤트 루프로 나뉘고 이벤트 루프가 처리한 결과를 돌려주는 방식에 따라 콜백 패턴과 퓨처 패턴으로 나뉜다.

 

여기에서는 단일 스레드 이벤트 루프와 콜백 패턴은 생략하고 웹플럭스에서 사용되는 다중 스레드 이벤트 루프와 퓨처패턴 위주로 살펴본다.

 

다중 스레드 이벤트 루프란 결국 이벤트 루프가 여러개가 된다는 말이다. 이벤트 루프는 무한루프 스레드라고 했는데 그냥 무한 루프 스레드가 여러개라고 생각하면 된다. 해당 스레드의 기본 사이즈는 CPU 코어갯수x2 다. 이는 하이퍼스레딩을 감안한 수치라고 한다.

 

각각의 이벤트 루프는 단일스레드로 구성되어 있으며 각각 개인의 이벤트 큐를 가지고 있다. 그리고 채널들은 하나의 이벤트 루프에 등록된다.

 

카프카에 익숙한 사람이라면 카프카와 구조가 비슷하다는 것을 느낄 것이다. 즉, 아래와 같이 말이다.

프로듀서 == 채널

토픽 == 이벤트 큐

컨슈머 == 이벤트 루프

 

하지만 카프카는 프로듀서와 컨슈머가 각각 토픽하고만 연결되어 있을 뿐이고 정작 프로듀서와 컨슈머는 완전히 분리되어 있는 반면 네티는 논블로킹-비동기 방식의 이벤트 기반으로 동작하지만 처리결과가 시작점까지 전달되어야 하는 일종의 스트림이다. 

 

이에 채널은 클라이언트와 연결되어 있는 동시에 이벤트 루프와도 연결되어 있어 요청의 처리 결과를 정확히 클라이언트까지 전달한다.

 

결국 카프카에 익숙한 상태라면 네티의 구성을 이해하는데 도움이 될 수는 있지만 카프카와는 분명한 차이가 있기 때문에 동일시해서 생각하기엔 다소 무리가 있다.

 

다시 네티로 돌아와서 퓨처패턴은 무엇인가? 이는 네티에서 논블로킹-비동기작업을 가능하게 하는 핵심 패턴이다.

 

자바의 Future 와 비슷하지만 Future는 수동으로 작업 완료여부를 체크하거나 완료전까지 블로킹하는 단점이 있는 반면 네티에서 제공하는 ChannelFuture를 사용하여 비동기 작업이 자동으로 완료되도록 할 수 있다.

 

ChannelFuture에는 ChannelFutureListener인스턴스를 하나 이상 등록할 수 있으며 작업이 완료되면 이 Listener들이 호출되며 작업의 완료를 확인하고 후처리를 할 수 있다.

 

그런데 처리결과를 ChannelFuture 로 감싸고 자동으로 완료된다고 할지라도 실제 작업이 종료될때까지 대기해야 하는 시간이 있고 이 시간동안 블로킹에 빠지는거 아닌가?

 

결론부터 말하면 그렇지 않다. 네티로 들어오는 요청의 흐름은 위에서 말했듯 채널→이벤트큐→이벤트루프 이고 응답의 흐름은 이벤트루프→채널 이다.

 

결국 ChannelFuture 는 채널까지 전달되어 작업 완료후 클라이언트로 응답을 보내고 해당 채널도 종료한다. 즉, 이벤트루프에는 블로킹이 걸리지 않기 때문에 완전한 논블로킹-비동기 작업이 가능해진다.

 

이로 인해 적은 스레드로 많은 요청을 처리할 수 있고 스레드가 적으니 Context Switching 부하도 상대적으로 줄어든다. 심지어 처리속도도 더 빠르다.

 

논블로킹-비동기 모델에서 적은 스레드로 진짜 처리속도가 더 빠를 수 있는지 라면 식당을 예로 들어 생각해보자.

 

우선 톰캣 기준으로 생각해보면 손님을 받을 수 있는 좌석(스레드)이 200개이고 요리를 할 수 있는 자리도 200개이며 요리사(CPU 코어)는 8명이다. 모든 조리시간은 10초로 가정한다.

 

이때 200명의 손님이 한번에 들어온다고 하면 한번에 8개의 라면을 끓일 수 있으므로 모든 손님에게 라면이 제공되는데 걸리는 시간은 200/8*10=250초 이다.

 

네티 기준으로 생각해보면 손님을 받을 수 있는 좌석(채널)이 200개이고 요리를 할 수 있는 자리는 8개이며 요리사(CPU 코어)도 8명이다. 모든 조리시간은 역시 10초로 가정한다.

(원래 네티의 이벤트 루프 스레드는 CPU 코어갯수*2 지만 예를 단순화 하기 위해 CPU 코어갯수와 동일하다고 가정했다)

 

그런데 이곳은 손님에게 완성된 라면을 제공하는게 아니라 라면재료가 셋팅된 냄비와 휴대용버너를 제공한다. 이걸 제공하는 시간은 3초로 가정한다. 

 

이때 200명의 손님이 한번에 들어온다고 하면 셋팅된 냄비와 휴대용 버너를 제공하는데 200/8*3=75초 가 소요되고 제공되는 시간차를 고려해서 그 라면들이 모두 조리되는데 4~78초 정도 소요된다고 생각해볼 수 있다. 즉, 모든 손님에게 라면이 제공하는 시간은 최대 153초가 된다.

 

하지만 이 모든 장점은 로직 중 I/O 블로킹이 없을 때 이야기다. 카프카에서 컨슈머가 메시지를 소비하지 못하면 다음 메시지로 넘어가지 못하는 것과 같은 원리이다.

 

네티에서는 이를 해결하기 위해 I/O 블로킹은 EventExecutor 라는 별도 스레드풀에서 처리되도록 할 수 있다. 그런데 해당 I/O 작업의 결과가 필요없을 수도 있고 필요할 수도 있다.

 

전자라면 그냥 작업을 종료하면 되지만 후자라면 어찌되었건 I/O 작업의 결과를 기다려야 하기 때문에 그 동안 메인 이벤트 루프 스레드가 블로킹 상태가 되는 것은 피할 수 없다.

 

이러한 블로킹마저 피하고 싶다면 I/O 작업을 처리하는 개체에서 ChannelFuture 로 변환될 수 결과를 리턴하면 된다. I/O 블로킹을 발생하는 대표적인 개체인 HttpClient 나 DB 드라이버에서 이러한 결과를 리턴하는 것들을 예로들면 WebClient 나 R2DBC 가 있다.

 

https://perfectacle.github.io/2021/02/28/netty-event-loop/
https://ellune.tistory.com/28
https://effectivesquid.tistory.com/65
https://www.dazhuanlan.com/web_de_world/topics/1685830
https://happygrammer.github.io/netty/intro/
https://stylishc.tistory.com/151
https://slowdev.tistory.com/14
https://kyoko.dev/2020/04/12/netty/day4/
https://simongs.github.io/2016/11/08/Network-Netty-Programming-1/
https://groups.google.com/g/netty-ko/c/n5tdjpTZRBw/m/aIGRhtxZEgAJ
https://sightstudio.tistory.com/15
https://groups.google.com/g/netty-ko/c/bh70EgoqWxE?pli=1
https://velog.io/@joosing/netty-nlocking-handler-effective-and-solve
https://stackoverflow.com/questions/57673715/webclient-maxconnection-pool-limit
https://github.com/wj2kim/Learning-Netty-Framework
https://brunch.co.kr/@myner/45
https://velog.io/@dailylifecoding/netty-study-memo-event-model
https://sungjk.github.io/2016/11/08/NettyThread.html
https://stackoverflow.com/questions/65345371/netty-thread-model-vs-long-running-taks
728x90