본문 바로가기

개발/충격. 개발일지

비동기 서버에 대한 생각

마이크로서비스의 시스템을 개발하게 되면

사용자 서비스 화면 또는 모바일 API 가 직접 DB 를 통하거나 자체 로직을 타는 것이 아닌 외부 API 를 호출하는 식으로 작동을 하게 됩니다.

( 제 개인적인 생각으로는 완벽한 프론트 서비스는 자체 로직이 없고 외부 API 의 값을 사용자에게 HTML 이나 JSON 으로만 출력하는 식이 가장 이상적이 아닐까 싶습니다.)

자바 개발자분들은 주로 스프링 + 톰캣 조합을 이용한 서버 개발을 하게 되는데요. 이런 경우 대부분

private RestTemplate restTemplate;

@PostConstruct
public void init() {
restTemplate = new RestTemplate();
}

@RequestMapping(value = "/bear", method = RequestMethod.GET)
public String bear() {
return "hello " + restTemplate.getForObject("http://localhost:8090/hello/name", String.class);
}

@RequestMapping(value = "/tiger", method = RequestMethod.GET)
public String tiger() {
return "hello " + restTemplate.getForObject("http://localhost:9090/hello/name", String.class);
}

거진 이런식으로 구성됩니다.(소스를 간단하게 보여드리기 위해 @Service, @Bean 의 효율적인 사용 등등은 뒤로 미뤘습니다.)

그런데 이런식의 구성의 경우 아래와 같은 쓰레드 풀 헬 문제가 발생합니다.

출처  : http://www.slideshare.net/brikis98/the-play-framework-at-linkedin

만약 하나의 API 에 행이 걸리게 되면, 그 API 를 호출한 Thread 자체도 행이 걸리게 되는데요. 이렇게 행이 많이 걸리는 Thread 가 늘어나게 되는 경우 쓰레드를 엄청나게 풀에서 가져오게 되는 것 입니다.

이 경우 아래와 같은 문제점이 생기게 되는데요.

  • 쓰레드가 너무 많이 생성되면서 컨텐스트 스위칭 비용이 발생, 서버가 느려짐
  • 쓰레드가 늘어나면서 Heap 을 엄청 많이 먹음, GC 시간 증가
  • 서블릿의 경우, 사용자의 입력을 처리하는 Dispatcher Thread Pool 이 꽉차면서, 다른 사용자들이 응답을 제대로 받지 못하는 현상이 발생

즉 API 의 요청을 기다리는 일 밖에 하지 않음에도 불구하고 시스템 성능이 급격하게 다운, 전체 서비스에 영향을 주는 경우가 생기는 것 입니다. 


NIO 의 등장

자바의 기존 file api 는 blocking 방식으로 작동하였습니다. 즉 file 에서 값을 읽거나 처리를 할 때 쓰레드가 읽기 처리를 하는 동안 멈추는 현상이 일어났는데, 
이를 해결하기 위하여 nonblocking 형식의 new IO, NIO 가 자바 1.4 부터 추가가 되었습니다.
NIO 를 효과적으로 쓰면 쓰레드 블록킹 문제를 해결할 수 있었지만 치명적인 문제가 있었는데요.

쓰기 너무 어렵다 ㅠㅠ

입니다. NIO 를 잘못하용하면 읽기 힘든 코드가 나오거나 제대로된 성능을 보여주지 못하는 경우가 발생하는 것 입니다.


Netty

NIO 를 활용하면 non blocking 서버를 개발할 수 있지만 이 것이 매우 어렵다는 문제가 있습니다. 다행이 이를 해결해주는 프레임워크 

http://netty.io/

가 있습니다. 이를 활용하여 여러 회사에서 non blocking 서버를 사용하고 있습니다.


동기 서버와 비동기 서버의 차이점

동기 서버의 장점

    • 쓰레드가 받혀주는 경우, 성능이 좋다.
    • 개발이 쉽다. 하나의 요청에 하나의 쓰레드가 할당되기 때문에, 그 요청만 신경쓰면 된다.
동기 서버의 단점
    • 매번 새로운 쓰레드를 생성 또는 풀에서 가져오는 경우에 성능 하락
    • 쓰레드가 많아지는 경우 성능 저하
비동기 서버 장점
  • 쓰레드를 적게 사용하기 때문에 가볍다.
  • 위에서 설명한 쓰레드 풀 현상이 발생하지 않는다.
비동기 서버 단점
  • 개발이 어렵다.
  • CPU 작업을 많이 사용해야 하는 경우, 다수의 요청 처리가 힘들다.

실험

아래와 같은 상황을 가정

  1. 외부의 api 를 사용하는 Front 서비스
  2. front 서비스는 곰 api, 호랑이 api 를 사용하고 있다.
  3. 곰 api 에 갑자기 성능 저하 문제가 발생, 요청이 3초로 느려지게 되었다.

이때 다음과 같은 상황을 원하고 있습니다.

  1. 곰 서비스 는 늦어도 어쩔수 없다, 백 API 가 느린것 이므로
  2. 하지만 호랑이 서비스에서는 영향이 없으면 좋겠다. 느린건 곰이지 호랑이가 아니므로
다음과 같은 상황을 가정하고 실험을 해보겠습니다. 

우선 동기식 서버의 코드는 다음과 같습니다.

@RestController
@RequestMapping(value = "/animal")
public class HelloController {

private RestTemplate restTemplate;

@PostConstruct
public void init() {
restTemplate = new RestTemplate();
}

@RequestMapping(value = "/bear", method = RequestMethod.GET)
public String bear() {
return "hello " + restTemplate.getForObject("http://localhost:8090/hello/name", String.class);
}

@RequestMapping(value = "/tiger", method = RequestMethod.GET)
public String tiger() {
return "hello " + restTemplate.getForObject("http://localhost:9090/hello/name", String.class);



이 경우 /animal/bear (곰 서비스) 를 1000 번 호출, /animal/tiger (호랑이 서비스) 를 1번 호출 하려고 합니다.

테스트 프로그램은 다음과 같습니다.

public class TestSpringApp {
public static void main(String[] args) throws InterruptedException, IOException {
AsyncHttpClient asyncHttpClient = new DefaultAsyncHttpClient();
BoundRequestBuilder request = asyncHttpClient.prepareGet("http://localhost:10000/animal/bear");

long startTime = System.currentTimeMillis();
CountDownLatch countDownLatch = new CountDownLatch(1001);
for (int i = 0; i < 1000; i++) {
request.execute(new AsyncCompletionHandler<Integer>() {
@Override
public Integer onCompleted(Response response) throws Exception {
countDownLatch.countDown();
return response.getStatusCode();
}
});
}

Thread.sleep(3000);

final long tigerTime = System.currentTimeMillis();
asyncHttpClient.prepareGet("http://localhost:10000/animal/tiger").execute(new AsyncCompletionHandler<Integer>() {
@Override
public Integer onCompleted(Response response) throws Exception {
System.out.println(response.getResponseBody());
System.out.println("tiger end : " + (System.currentTimeMillis() - tigerTime) / 1000.0);
countDownLatch.countDown();
return response.getStatusCode();
}

@Override
public void onThrowable(Throwable t) {
t.printStackTrace();
}
});

countDownLatch.await();
System.out.println("end time : " + (System.currentTimeMillis() - startTime) / 1000.0);
asyncHttpClient.close();
}
}

결과는 이렇게 나옵니다.


hello tiger

tiger end : 32.419

end time : 36.531


전체 요청을 처리하는데 약 36초가 걸렸으며. 타이거 서비스를 처리하는데 32초가 걸린 것을 확인할 수 있습니다.

 Jconsole 을 이용하여 쓰레드 증가량을 확인해보면

이런식으로 쓰레드가 급격하게 221 까지 증가하였던 것을 확인하실 수 있습니다.

이번엔 Netty + Vert.x 로 이루어진 비동기 Front 서버에서 테스트를 해보겠습니다.

코드는 다음과 같습니다.

public class MainApp {
public static void main(String[] args) {
AsyncHttpClient asyncHttpClient = new DefaultAsyncHttpClient();

Handler<HttpServerRequest> handler = request -> {
String path = request.absoluteURI().getPath();
HttpServerResponse response = request.response();
if (path.equals("/cmd/kill")) {
response.end("Exiting...");
SimpleHttpServer.shutdown();
} else if (path.equals("/cmd/ping")) {
response.end("PONG");
} else if (path.equals("/animal/bear")) {
BoundRequestBuilder bearRequest = asyncHttpClient.prepareGet("http://localhost:8090/hello/name");
bearRequest.execute(new AsyncCompletionHandler<Integer>() {
@Override
public Integer onCompleted(Response httpResponse) throws Exception {
response.end(httpResponse.getResponseBody());
return httpResponse.getStatusCode();
}
});
} else if (path.equals("/animal/tiger")) {
BoundRequestBuilder bearRequest = asyncHttpClient.prepareGet("http://localhost:9090/hello/name");
bearRequest.execute(new AsyncCompletionHandler<Integer>() {
@Override
public Integer onCompleted(Response httpResponse) throws Exception {
response.end(httpResponse.getResponseBody());
return httpResponse.getStatusCode();
}
});
} else {
response.setStatusCode(404);
response.setStatusMessage("not found");
response.end("not found");
}
};
String host = "localhost";
int port = 9999;
SimpleHttpServer httpServer = new SimpleHttpServer();
httpServer.registerHttpHandler(host, port, handler);
}
}

소스코드 더러운건 넘어가주세요

이를 이용하여 테스트를 해보겠습니다. 테스트 방식은 위의 방식과 유사합니다.

결과는 다음과 같이 나옵니다.


tiger

tiger end : 0.026


테스트 코드의 문제인지 전체 테스트 완료는 되지 않고 있습니다만. 호랑이 서비스의 경우 만족할 정도로 빠르게 응답이 오는 것을 확인할 수 있었습니다.

쓰레드 수는 다음과 같습니다. 동기 서버와 마찬가지로 급격하게 올라가긴 하지만 몇백개 수준으로는 올라가지 않네요. 아마 네티 내부에서 멀티쓰레드 처리가 되어있어서 올라가지 않았나 하는 생각이 듭니다.


이렇게 테스트를 하는 동안 갑자기 드는 생각이 있었습니다.

" Node.js 를 사용하면 어떨까"

옛날 등장해서 각종 센세이션을 일으킨 Node.js 도 마찬가지고 비동기 서비스 입니다,.

이를 이용하면 어떨까 하는 생각에 Node.js 도 테스트해보았습니다.

이에 활용한 node.js 코드는 아래와 같습니다.

var express = require('express');
var request = require('request');

var app = express();

app.get('/', function (req, res) {
res.send('Hello World!');
});

app.get('/animal/tiger', function(req, res) {
request('http://localhost:9090/hello/name', function (error, response, body) {
res.send("hello " + body);
});
});

app.get('/animal/bear', function(req, res) {
request('http://localhost:8090/hello/name', function (error, response, body) {
res.send("hello " + body);
});
});

app.listen(10001, function () {
console.log('Example app listening on port 10001!');
});

이를 테스트 해보겠습니다.

테스트 방식은 위와 동일합니다.


tiger end : 0.031

end time : 35.847


Netty 서버와 마찬가지로 호랑이 서비스에서 빠른 응답이 왔습니다. 



결론

http://www.zdnet.com/article/how-replacing-java-with-javascript-is-paying-off-for-paypal/

Paypal 에서 front 서비스를 Node.js 로 변경했다는 소식이 화제가 되었습니다. Node.js 로 변경하면서 엄청난 코드 품질 향상, 성능 향상이 있었다고 하는데요.
그 뿐만 아니라 트위터에서도 Netty 를 활용한 자체 비동기 서버 프레임워크 Finatra( https://twitter.github.io/finatra/) 를 사용하여 서비스를 하고 있습니다.

이런 대용량 서비스의 Front 서비스는 기존 Tomcat 환경 뿐만 아니라 다른 서버 환경에 대해서 고려를 해야할 것이라 생각됩니다.

다만 이 글이 단순히, Netty, Node.js 가 좋다. Tomcat, Servlet, Spring 은 정말 안좋고 성능이 나쁘다. 라는 뜻은 아닙니다. 외부 API 의 경우 매우 위와 같은 문제가 발생하지만, 서버 CPU 를 제대로 활용하려면 Thread 방식의 blocking 서버 또한 매우 훌륭한 서비스라고 생각합니다.

대규모 서비스 구축에서는 상황에 맞는 방식의 서버를 고르는 능력 및 해당 서버에 맞는 프로그래밍 실력또한 필요할 것이라 생각합니다.

소스코드는 좀 더 정리 후 Github 에 올려 공유하도록 하겠습니다.

참고한 사이트, 책

카카오 기술 블로그 , http://tech.kakao.com/2016/05/04/asynchronous-programming-and-monad-transformers-in-scala/

https://github.com/trieu/netty-with-gradle

자바 네트워크 소녀 네티, http://www.yes24.com/24/Goods/20600128?Acode=101


기타 잡설

이번 테스트를 진행하면서 Node.js 가 출시 당시 매우 큰 반향을 일으키고 많은 회사에서 솔루션으로 채택하는지 몸으로 느끼게된 계기였습니다. 비동기 서버를 구축할 때 자바 기반보다 1억배는 쉬운거 같네요. (물론 자바 비동기 서버 도 훌륭하지만..)