마이크로서비스의 시스템을 개발하게 되면
사용자 서비스 화면 또는 모바일 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 의 등장
Netty
동기 서버와 비동기 서버의 차이점
동기 서버의 장점
- 쓰레드가 받혀주는 경우, 성능이 좋다.
- 개발이 쉽다. 하나의 요청에 하나의 쓰레드가 할당되기 때문에, 그 요청만 신경쓰면 된다.
- 매번 새로운 쓰레드를 생성 또는 풀에서 가져오는 경우에 성능 하락
- 쓰레드가 많아지는 경우 성능 저하
- 쓰레드를 적게 사용하기 때문에 가볍다.
- 위에서 설명한 쓰레드 풀 현상이 발생하지 않는다.
- 개발이 어렵다.
- CPU 작업을 많이 사용해야 하는 경우, 다수의 요청 처리가 힘들다.
실험
아래와 같은 상황을 가정
- 외부의 api 를 사용하는 Front 서비스
- front 서비스는 곰 api, 호랑이 api 를 사용하고 있다.
- 곰 api 에 갑자기 성능 저하 문제가 발생, 요청이 3초로 느려지게 되었다.
이때 다음과 같은 상황을 원하고 있습니다.
- 곰 서비스 는 늦어도 어쩔수 없다, 백 API 가 느린것 이므로
- 하지만 호랑이 서비스에서는 영향이 없으면 좋겠다. 느린건 곰이지 호랑이가 아니므로
@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://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억배는 쉬운거 같네요. (물론 자바 비동기 서버 도 훌륭하지만..)