1. 개요


서비스에 대용량의 트래픽이 몰리면서, 특정 API 호출에 대한 응답이 지연되면 전체적 로직에 영향을 미치게 된다.

이러한 상황을 재현하기 위해 응답을 늦게 보내는 서버를 구현 하고자 했다.

단순하게 요청을 받고 몇초간의 sleep 후 응답을 돌려주면 간단하게 구현할 수 있지만, 

제한된 자원(톰캣 1대)에서 다수(초당 15000명)의 request 를 처리하기 위해서 일반적인 sleep 방법으로는 구현 할 수 없었다.

(성능 이슈가 없다면 이곳 을 참조하면 간단하게 응답 지연 api를 얻을 수 있다.)


2. 문제점


사실, response time은 늦으면서, 다수의 request를 처리한다는 건 역설적인 말이다. 

request 가 많을 수록, connection 의 수는 늘어 날 것이고, sleep 을 통해 connection을 놓아주지 않으면  더 이상의 request 를 처리할 수 없기 때문이다. 

request 를 위한 connection 비용이 적지 않을 뿐더러, Spring 에서 request 를 처리하기 위한 servlet container thread 자원은 application의 로직을

수행하기 위한 application thread 보다 적고 한정적이다. 


3. 해결 방법


Long polling 방식으로 request 에 대하여 늦은 response를 전달하는 방법으로 서버 성능 문제를 해결하고자 했다.

단순히 클라이언트 쪽에서 long polling 방식은 connection을 그대로 유지하고 있기 때문에 문제를 해결 할 수 없는 방식이었고,

서버쪽에서 long polling 을 적용하여 connection 위한 thread 관리가 필요했다.


사실 long polling 방식도 request - response 모델이어서, 서버에서 클라이언트에게 응답을 주기 전까지 connection을

유지하기 때문에 이 방식도 성능 이슈를 해결하기 어려울 것 처럼 보였다. 

그러던 중 이러한 long polling의 문제점을 보완하기 위해 Spring 3.2에서 추가된 Async Servlet 기능을 보게 되었다. 

Async Servlet는 request 처리를 위한 servlet container thread의 일을 application thread에 위임시키고 servlet container thread를 반납해서

새로운 request 를 처리하는 방식으로 작동한다.


Spring 의 Async Servlet 을 이용하면, 서버의 thread pool 과 acceptCount 값을 조정하여, 

제한된 자원에서 다수의 request 를 처리할 수 있을것이라 생각했다.


4. 적용


소스 코드는 Spring-mvc-showcase 를 참조하였다. 


(1) 소스

@Controller @RequestMapping("/async/callable") public class CallableController { @RequestMapping("/response-body-2") public @ResponseBody Callable<String> callable() { return new Callable<String>() { @Override public String call() throws Exception { Thread.sleep(2000); return "Callable result"; } }; } }



(2) 결과

응답이 2초 후 온 것을 알 수 있다.


위 로직을 수행하는 동안의 로그는 다음과 같다.

15:23:20 [http-apr-80-exec-9] DispatcherServlet - DispatcherServlet with name 'appServlet' processing GET request for [/async/callable/response-body-2]

15:23:20 [http-apr-80-exec-9] RequestMappingHandlerMapping - Looking up handler method for path /async/callable/response-body-2

15:23:20 [http-apr-80-exec-9] RequestMappingHandlerMapping - Returning handler method [public java.util.concurrent.Callable<java.lang.String> org.springframework.samples.mvc.async.CallableController.callable()]

15:23:20 [http-apr-80-exec-9] DispatcherServlet - Last-Modified value for [/async/callable/response-body-2] is: -1

여기 까지 기존 로직을 따른다

15:23:20 [http-apr-80-exec-9] WebAsyncManager - Concurrent handling starting for GET [/async/callable/response-body-2]

WebAsyncManager(application thread) 에서 병행처리를 시작하고

15:23:20 [http-apr-80-exec-9] DispatcherServlet - Leaving response open for concurrent processing

Dispatcher(servlet container thred) 에서 작업을 끝낸다.

이렇게 작업 쓰레드간 switching 이 이루어지고

15:23:22 [MvcAsync5] WebAsyncManager - Concurrent result value [Callable result] - dispatching request to resume processing

작업을 넘겨받은 MvcAsyc 는 작업을 수행(2초간의 sleep)후 dispatching 을 요청한다.

15:23:22 [http-apr-80-exec-4] DispatcherServlet - DispatcherServlet with name 'appServlet' resumed processing GET request for [/async/callable/response-body-2]

15:23:22 [http-apr-80-exec-4] RequestMappingHandlerMapping - Looking up handler method for path /async/callable/response-body-2

15:23:22 [http-apr-80-exec-4] RequestMappingHandlerMapping - Returning handler method [public java.util.concurrent.Callable<java.lang.String> org.springframework.samples.mvc.async.CallableController.callable()]

15:23:22 [http-apr-80-exec-4] DispatcherServlet - Last-Modified value for [/async/callable/response-body-2] is: -1

15:23:22 [http-apr-80-exec-4] RequestMappingHandlerAdapter - Found concurrent result value [Callable result]

15:23:22 [http-apr-80-exec-4] RequestResponseBodyMethodProcessor - Written [Callable result] as "text/html" using [org.springframework.http.converter.StringHttpMessageConverter@7967f612]

Spring mvc가 callable 을 보고 병행작업임을 알고 나머지 로직을 수행한다.

15:23:22 [http-apr-80-exec-4] DispatcherServlet - Null ModelAndView returned to DispatcherServlet with name 'appServlet': assuming HandlerAdapter completed request handling

15:23:22 [http-apr-80-exec-4] DispatcherServlet - Successfully completed request



5. Servlet container thread 와 application specific thread 의 차이


여기서 servlet container thread 와 application thread 의 차이점을 살펴보자.

servlet thread의 일을 application thread 로 넘겨줌으로 해서 long polling 의 단점을 보완한다고 하는데, 결국 application thread 역시

서버의 자원을 사용하지 않는가? 라는 의문을 가질 수 있다.


Servlet container 란 HTTP Request 와 response 객체를 서블릿에 넘겨주고, 서블릿의 생명주기를 관리한다.

따라서 servlet container thread 는 사용자의 request 를 처리하는 작업을 수행한다. 

반면 application는 서비스 자체를 의미하고, application thread는 Spring framework 에서 발생하는 작업들을 수행한다.


이러한 관점에서 보았을때,  serblet thread 의 일을 application thread 에 위임하는 것이 

많은 request 를 수신하기 위한 효율 적인 방법임을 알 수 있다.

하지만, 결국 한정된 서버의 자원을 공유하여 사용한다는것은 변함이 없다.

spring mvc 내에서 servlet container에 할당 할 thread 수를 조정할 수 있는지 알아보고 최적화가 필요할 것 같다.


6. 결론


서버의 한정된 자원에서 다수의 request 를 처리하는데 한계가 있지만, 

서버는 무엇으로 구성할지 (톰캣, 네티, 직접구현)에 따라서, 그리고 서버 환경 설정을 어떻게 하는냐에 따라 동시에 수용할 수 있는 request의 수를 증가 시킬 수 있다. 

서버의 설정 이후에는, 어플리케이션의 구현 부분에서 최적화가 필요할 것이다.

고민의 시작은 전자에서 시작했지만, 작업은 후자에서 진행되었다.

다시 서버단으로 돌아가서 고민을 해봐야겠지만, 넉넉한 자원이 주어졌을때, 해당 자원을 제대로 활용하지 못하는 경우

application의 구현부분에서의 최적화 역시 필요할것으로 생각한다.


결과적으로  Async Servlet를 이용한 long polling 방식으로 서버에 요청할 수 있는 최대 request 를 늘리는 것은 실패하였다.

추가적으로 서버의 최적화 또는, tomcat 에 comet 을 적용, 심플한 구조의 서버 구축과 같은 작업이 병행되어야 할 것으로 보인다.

 


+ Recent posts