Open beadss opened 5 years ago
그림을 좀 추가해야되는데, 텍스트만 쓰는데도 시간이 너무 오래 걸렸습니다. (썼다 지웠다를 많이 하느라..) 그림은 현장에서 그린 후 업로드하도록 하겠습니다.
Tomcat에서 Spring Webflux는 Servlet Non blocking IO 기반으로 동작해서, 어떻게 가능한지 이해는 Servlet 설명만으로 충분할 것 같고, Webflux에 대한 제대로 된 정리는 시간이 매우 오래 걸릴 것 같아서 일단 생략했습니다.
NGINX, Tomcat, Spring Webflux
비동기와 논블로킹의 세상에서, NGINX, Tomcat, Spring Webflux는 웹 개발자에게 가장 일반적으로 소개되는 솔루션 조합입니다. 이 문서의 목적은, 위의 솔루션들의 동작 방식 설명을 통해 요청에서 응답까지 어떤 과정을 거치는지 를 이해하는 것 입니다.
NGINX
NGINX는 널리 알려진 이벤트 루프 방식의 웹서버입니다.
n개의 worker process가 동일한 port를 listen하고 있다가, 연결 요청을 수락합니다. nginx 1.11.3 && Linux 4.5(커널 버전) 이후부터 EPOLLEXCLUSIVE 플래그를 통해, 연결 요청이 worker process들에 round robin으로 할당됩니다. 별도 설정은 필요 없습니다. http://nginx.org/en/docs/events.html#epoll
버전이 그 이전이라면 accept_mutex 라는 옵션을 on 해줘야 1개 요청에 대기중인 모든 worker process들이 깨어나는 상황(aka. thundering herd)을 방지할 수 있습니다.
reuseport라는 옵션도 있습니다만, 여기서는 다루지 않겠습니다.
연결 요청, header read 등은 아래 Tomcat NIO Connector와 마찬가지로 Non blocking으로 수행됩니다. 원리는 동일하니 Tomcat NIO 항목을 참고해주세요.
세부적으로 가면 차이점이 있긴 합니다. Tomcat NIO는 N개의 커넥션을 M개의 worker thread가 니꺼내꺼 없이 처리하지만, NGINX는 1개의 worker process당 N개의 커넥션을 처리합니다. 내꺼가 확실한거죠. 어떤 방식이 더 나은지, 혹은 어떤 차이때문에 구현이 달라졌을지 고민해보는 것도 재미있을 것 같습니다.
특이사항으로는 NGINX 1.7.11 이후부터 worker process마다 thread pool이 추가됐다는 점입니다. 이는 blocking 방식으로 동작하는 File IO를 위한 것입니다. 톰캣으로 패스하는 http 요청 등은 thread pool을 사용하지 않습니다.
Tomcat NIO Connector
NIO Connector란 무엇일까? 왜 자꾸 사람들이 NIO가 Non blocking IO가 아니고 New IO의 약자임을 강조 할까? 그것은 바로 Java의 New IO API를 기반으로 구현된 Connector이기 때문입니다. 그럼 java New IO는 왜 Non blocking IO가 아니고 New IO냐? Non blocking IO만을 추구한 것이 아니기도 하고, 몇몇 IO 기능(File IO)들은 여전히 Blocking 방식으로 동작하기 때문으로 추정됩니다.(잘은 모르겠습니다 ㅎ) 물론 Blocking 방식의 IO 기능들도 성능은 개선됐습니다.
Java의 New IO에 대해서는 아래 문서에 잘 설명돼있습니다. http://eincs.com/2009/08/java-nio-bytebuffer-channel-file/
NIO Connector(이하 NIO)에는 몇가지 특징이 있습니다.(몇개 더 있지만 일단 생략)
참고로 BIO Connector(이하 BIO)는 이렇습니다.
BIO는 1개의 acceptor thread가 요청이 들어올때마다 socket을 생성한 후, 단일 woker thread에 read, process, write를 모두 맡깁니다. 때문에 1개 커넥션이 곧 1개 스레드가 됩니다. 1개 스레드가 언제 올지도 모르는 네트워크 패킷을 하염없이 기다리며(Blocking) 모든 패킷이 도착한 후에야 처리하고 응답합니다.
NIO는 1~2개의 acceptor thread가 요청이 들어올때마다 SocketChannel을 생성(connect completed)한 후, NioChannel에 등록합니다. 1개 커넥션마다 1개 Channel이 생성되며, worker thread들이 Non blocking 방식으로 header read를 수행하게 됩니다. 이후 Servlet 레벨 작업(body read, process, body write)은 Servlet 스펙에 따라, worker thread에서 Blocking으로 수행될지, Servlet에서 관리되는 별도의 thread에서 Non Blocking으로 수행될지 결정됩니다.
이 항목에서는 connect와 header read에 대해서만 이야기합니다. Servlet 3.0 이하라고 생각하고, 서블릿 부분이 worker thread에서 수행된다고 가정합니다.
우리가 주목해야하는 부분은 1개 커넥션마다 1개 Channel이 생성되며, worker thread들이 Non blocking 방식으로 header read를 수행하는 부분입니다.
Channel에 패킷이 도착하면 이벤트를 발생(By Selector)시키고, 이벤트 루프를 돌고 있는 poller thread(최대 2개)가 유휴 worker thread에게 작업을 할당합니다. worker thread는 패킷을 읽어서 Channel(정확히는 Channel의 Wrapper)의 버퍼에 데이터를 써줍니다. 1개 Channel은 1개 이상의 worker thread에 의해 버퍼가 쓰여지고, header read가 완료된 시점에 작업중이던 worker thread가 Servlet 레벨 작업을 이어서 진행하게 됩니다.
참고로 1개 Channel은 1개 이상의 worker thread에 의해 버퍼가 쓰여지고 부분을 I/O Multiplexing이라고 부릅니다.
대체 기존과 뭐가 달라졌기에 이렇게 처리할 수 있게 됐을까? Java의 New IO의 Selector에 대해 이해해하면 알 수 있습니다.
Selector 그리고 select, epoll
Selector는 아래 문서에 잘 설명돼있습니다. 일종의 이벤트 리스너라고 생각하면 됩니다. http://javacan.tistory.com/entry/87
select, epoll은 복잡하게 생각하면 어려우니, 일단은 OS레벨에서 지원되는 알리미 라고 생각하면 됩니다. 본 문서에서는 epoll(리눅스 한정) 기준으로 이야기를 진행합니다. http://ozt88.tistory.com/21
용법을 간단히 이야기하자면 Selector가 여러 Channel에 관심(epoll 방식)을 걸어두고, poller thread가 주기적으로 Selector에 쌓여있는 SelectorKey(epoll에 의해 이벤트가 발생된 채널을 물고있음) 목록을 worker thread들에게 할당해주는 식입니다.
Servlet Async and Non blocking IO
Async Servlet
Async Servlet은 Servlet API 3.0부터 지원됩니다. 간단히 설명하자면, 요청을 받은 스레드와는 다른 스레드에서 응답을 하게 해주는 기능입니다. 아래 문서에 자세히 설명돼있습니다. http://javacan.tistory.com/entry/Servlet-3-Async
Spring MVC에서도 Async Servlet을 지원합니다. 이것만으로는 실무의 어떤 상황에서 써야할지 잘 모르겠습니다. 기존의 Tomcat BIO Connector라서 worker thread를 최대한 아껴야되면 모를까.. Tomcat NIO를 사용한다면, 그냥 worker thread를 늘리는게 낫지 않나 생각이 듭니다.
물론 Servlet Non blocking IO가 Async Servlet을 기반으로 동작하므로, 아예 필요 없는건 아닙니다.
Spring MVC 간략 예제입니다. http://wonwoo.ml/index.php/post/1912
Non blocking IO
Servlet API 3.1부터 Non blocking IO를 지원합니다. Tomcat NIO Connector 설명을 들으면서, 정작 큰 데이터인 body를 blocking 방식으로 read/write한다는데에 큰 의문을 느꼈을 것입니다.
그 이유는 body read를 Servlet 레벨에서 수행하기 때문입니다. 기존까지는 Servlet API에서 Blocking IO만을 지원했고, 이전에 별도로 body를 read하지 않았다면, Servlet 레벨에서 request.getParameterMap()을 수행하는 시점에 Blocking read가 수행됐습니다.
아래는 Non blocking IO에 대한 간략한 설명과 예제입니다. Spring Webflux가 Servlet Container(Tomcat) 기반으로 돌아갈때는 아래 방식을 기반으로 연동됩니다. Non blocking IO: https://docs.oracle.com/javaee/7/tutorial/servlets013.htm Spring Webflux와의 연동: https://github.com/spring-projects/spring-framework/blob/862dd239759053db0a6afc6c8f1703570f21a73f/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java
Spring Webflux
작성 예정