음.. 프레임워크에 대해 알아보고자 하는데
익히 알고있는 웹 서비스를 하기 위한 프레임워크.
즉, Web framework의 종류엔.. 널리 알려진것들이 몇개 있는데
Spring, Spring-boot, undertow, latpack 등등이 있다
근데 워낙 최근 성능에 대한 관심이 많아져서 찾아보다보니
그 중, light-4j(예전엔 프레임워크 이름이 light-java 였다) 에 대해 알아보게 되었다.
하여.. 성능이 뭐 얼마나 차이날까 해서 봤는데
Framework | Language | Max Throughput | Avg Latency | Transfer |
---|---|---|---|---|
Go FastHttp | Go | 1,396,685.83 | 99.98ms | 167.83MB |
Light Java | Java | 1,344,512.65 | 2.36ms | 169.25MB |
ActFramework | Java | 945,429.13 | 2.22ms | 136.15MB |
Go Iris | Go | 828,035.66 | 5.77ms | 112.92MB |
Vertx | Java | 803,311.31 | 2.37ms | 98.06MB |
Node-uws | Node/C++ | 589,924.44 | 7.22ms | 28.69MB |
Dotnet | .Net | 486,216.93 | 2.93ms | 57.03MB |
SeedStack-Filter | Java | 343416.33 | 4.41ms | 51.42MB |
Jooby/Undertow | Java | 317,385.05 | 4.31ms | 45.70MB |
Spring Boot Reactor | Java | 243,240.17 | 7.44ms | 17.86MB |
Pippo-Undertow | Java | 216,254.56 | 9.80ms | 31.35MB |
Spark | Java | 194,553.83 | 13.85ms | 32.47MB |
Pippo-Jetty | Java | 178,055.45 | 15.66ms | 26.83MB |
Play-Java | Java | 177,202.75 | 12.15ms | 21.80MB |
Go Http | Go | 170,313.02 | 15.01ms | 20.95MB |
JFinal | Java | 139,467.87 | 11.89ms | 29.79MB |
Akka-Http | Java | 132,157.96 | 12.21ms | 19.54MB |
RatPack | Java | 124,700.70 | 13.45ms | 10.82MB |
Pippo-Tomcat | Java | 103,948.18 | 23.50ms | 15.29MB |
Bootique + Jetty/Jersey | Java | 65,072.20 | 39.08ms | 11.17MB |
SeedStack-Jersey2 | Java | 52310.11 | 26.88ms | 11.87MB |
Baseio | Java | 50,361.98 | 22.20ms | 6.39MB |
NinjaFramework | Java | 47,956.43 | 55.76ms | 13.67MB |
Play 1 | Java | 44,491.87 | 10.73ms | 18.75MB |
Spring Boot Undertow | Java | 44,260.61 | 38.94ms | 6.42MB |
Nodejs Express | Node | 42,443.34 | 22.30ms | 9.31MB |
Dropwizard | Java | 33,819.90 | 98.78ms | 3.23MB |
Spring Boot Tomcat | Java | 33,086.22 | 82.93ms | 3.98MB |
Payra Micro | Java | 24,768.69 | 118.86ms | 3.50MB |
WildFly Swarm | Java | 21,541.07 | 59.77ms | 2.83MB |
??!?
무려.. Go언어와 맞먹는 처리량에.. 매우 안정적인 Latency까지 보여주고 있다.
도저히 믿기가 힘들어서 뭔가해서 코드를 뜯어보기 시작하는데,
프레임워크 내부에 Undertow를 사용해서 처리를 하고 있다.
다만, 설정적인 부분에 튜닝포인트를 넣어서 처리를 하고 있다.
사실, Spring-boot를 쓰면 요청(Request)를 매핑해서 처리하기엔 Annotation 기반으로 아주 직관적으로 사용하여 쉽고 편리한데
Undertow(내부적으로 NIO를 사용하여 구현)나 Netty 등으로 직접 Transport Layer단을 구현해야 하는 과정이 있다면
쓰기 좀 불편하거니와, 성능적인 부분 및 예외 등 처리해야 할 이슈들이 많다.
Light-4j의 경우는 그런 설정상의 어려움을 좀 직관적으로 매핑해주는 프레임워크인 것으로 느껴졌고,
간단하게 아래와 같은 구조로 서버를 실행할 수 있다.
// inject server config here.
Config config = Config.getInstance();
// write a config file into the user home directory
Map map = new HashMap();
map.put("description", "server config");
map.put("enableHttp", true);
map.put("ip", "0.0.0.0");
map.put("httpPort", 8080);
map.put("enableHttps", false);
map.put("httpsPort", 8443);
map.put("keystoreName", "tls/server.keystore");
map.put("keystorePass", "secret");
map.put("keyPass", "secret");
map.put("serviceId", "com.networknt.apia-1.0.0");
map.put("enableRegistry", false);
addURL(new File(homeDir).toURI().toURL());
config.getMapper().writeValue(new File(homeDir + "/server.json"), map);
if (server == null) {
logger.info("starting server");
Server.start();
}
// 실행하고 종료 Hook을 걸기 위한 부분(없으면 서버가 바로 종료됨)
addDaemonShutdownHook();
final ServiceLoader startupLoaders = ServiceLoader.load(StartupHookProvider.class);
for (final StartupHookProvider provider : startupLoaders) {
provider.onStartup();
}
// application level service registry. only be used without docker container.
// 서비스를 서비스 레지스트리에 등록한다.(내부적인 용도로 사용하는 포트를 여는 것으로 보임 -> microservices)
if(config.enableRegistry) {
// assuming that registry is defined in service.json, otherwise won't start server.
registry = (Registry) SingletonServiceFactory.getBean(Registry.class);
if(registry == null) throw new RuntimeException("Could not find registry instance in service map");
InetAddress inetAddress = Util.getInetAddress();
String ipAddress = inetAddress.getHostAddress();
if(config.enableHttp) {
serviceHttpUrl = new URLImpl("light", ipAddress, config.getHttpPort(), config.getServiceId());
registry.register(serviceHttpUrl);
if(logger.isInfoEnabled()) logger.info("register serviceHttpUrl " + serviceHttpUrl);
}
if(config.enableHttps) {
serviceHttpsUrl = new URLImpl("light", ipAddress, config.getHttpsPort(), config.getServiceId());
registry.register(serviceHttpsUrl);
if(logger.isInfoEnabled()) logger.info("register serviceHttpsUrl " + serviceHttpsUrl);
}
}
HttpHandler handler = null;
// API routing handler or others handler implemented by application developer.
// API 라우팅을 적용. /index, /rest/getName 등등.. 개발자가 매핑한 URL을 Provider로부터 받아온다
final ServiceLoader handlerLoaders = ServiceLoader.load(HandlerProvider.class);
for (final HandlerProvider provider : handlerLoaders) {
if (provider.getHandler() != null) {
handler = provider.getHandler();
break;
}
}
if (handler == null) {
logger.error("Unable to start the server - no route handler provider available in the classpath");
return;
}
// Middleware Handlers plugged into the handler chain.
// 미들웨어 핸들러가 있다면 핸들러에 적용(아직 무슨소린지 모르겠다)
final ServiceLoader middlewareLoaders = ServiceLoader.load(MiddlewareHandler.class);
logger.debug("found middlewareLoaders", middlewareLoaders);
for (final MiddlewareHandler middlewareHandler : middlewareLoaders) {
logger.info("Plugin: " + middlewareHandler.getClass().getName());
if(middlewareHandler.isEnabled()) {
handler = middlewareHandler.setNext(handler);
middlewareHandler.register();
}
}
// 이제 Undertow를 이용해서 서버를 빌드한다.
Undertow.Builder builder = Undertow.builder();
if(config.enableHttp2) {
sslContext = createSSLContext();
builder.addHttpsListener(config.getHttpsPort(), config.getIp(), sslContext);
builder.setServerOption(UndertowOptions.ENABLE_HTTP2, true);
} else {
if(config.enableHttp) {
builder.addHttpListener(config.getHttpPort(), config.getIp());
}
if(config.enableHttps) {
sslContext = createSSLContext();
builder.addHttpsListener(config.getHttpsPort(), config.getIp(), sslContext);
}
}
server = builder
.setBufferSize(1024 * 16)
.setIoThreads(Runtime.getRuntime().availableProcessors() * 2) //this seems slightly faster in some configurations
.setSocketOption(Options.BACKLOG, 10000)
.setServerOption(UndertowOptions.ALWAYS_SET_KEEP_ALIVE, false) //don't send a keep-alive header for HTTP/1.1 requests, as it is not required
.setServerOption(UndertowOptions.ALWAYS_SET_DATE, true)
.setServerOption(UndertowOptions.RECORD_REQUEST_START_TIME, false)
.setHandler(Handlers.header(handler, Headers.SERVER_STRING, "L"))
.setWorkerThreads(200)
.build();
server.start();
if(logger.isInfoEnabled()) {
if(config.enableHttp) {
logger.info("Http Server started on ip:" + config.getIp() + " Port:" + config.getHttpPort());
}
if(config.enableHttps) {
logger.info("Https Server started on ip:" + config.getIp() + " Port:" + config.getHttpsPort());
}
}
if(config.enableRegistry) {
// start heart beat if registry is enabled
SwitcherUtil.setSwitcherValue(Constants.REGISTRY_HEARTBEAT_SWITCHER, true);
if(logger.isInfoEnabled()) logger.info("Registry heart beat switcher is on");
}
위와 같이 Config를 적용하고 Undertow 빌더를 이용해 서버를 빌드한다.
이제 오는 Request에 대해서 undertow 서버가 처리를 하게 되는데, 핸들러를 등록하는 부분이 특이하다.
위와 같이 resources 밑에 META-INF.services 라는 폴더를 생성해두고, 하위에 com.networknt.server.HandlerProvider 라는 파일을 생성하면,
서버가 실행될 때 위의 HanderProvider 를 불러와서 URL 매핑을 하는 부분에서 사용을 하게 된다.
파일의 내용은 간단하게 아래와 같다.
com.networknt.server.TestHandlerProvider
해당 Provider가 어디있는지 패키지명과 함께 적어두게 되면, Provider path를 참조해서 로드한다.
HandlerProvider는 그럼 어떤구조일까
public class TestHandlerProvider implements HandlerProvider {
@Override
public HttpHandler getHandler() {
return Handlers.routing()
.add(Methods.GET, "/", new TestHandler())
.add(Methods.GET, "/test", new Test2Handler());
}
}
위와 같이, Method 타입과, 매핑할 URL, 핸들러 명을 붙여서 HandlerProvider를 구성하게 된다.
이렇게 적용해놓으면
/(GET) -> TestHandler
/test(GET) -> TestHandler2
로 호출이 되어 각 핸들러가 처리를 하게 된다(매우 편리하다고 봄. Undertow의 경우 설정하다가 복잡한 부분이 있는데 이를 해결)
public class TestHandler implements HttpHandler {
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
exchange.getResponseSender().send("Hello World!");
}
}
그리고 핸들러의 내부는 위와같이 handlerRequest 라는 함수를 구현하는 것으로 처리가 된다.
그리고 Response를 보내는 것은 HttpServerExchange라는 클래스의 getResponseSender()의 send() 함수로 보낼 수 있다
(이는 Undertow의 Request-Response 와 관련된 내용)
매우 간단하면서도.. 높은 성능을 보여주고 있는 프레임워크인데,
사실 저 성능이 HTTP Pipeline 성능이라는데, 실제로 클라이언트가 요청한 데이터 기준이 아닐수도 있다.
어떻게 측정했는지 찾아보고 있는데.. 이렇게 높을 성능을 낼 수 있다는것이 놀라울 정도다
궁금한건.. Undertow를 그냥 쌩으로 썼을때보다 5배 이상의 성능을 낸다는 것인데.. 코드 상으로는 그냥 Undertow를 튜닝해서 적용한 것 밖에 안보인다.
여튼.. 차후에 고성능 서버가 필요할 때 더 자세하게 써보는 것으로 해봐야겠다.
아키텍쳐는 깔끔하고 괜찮다고 생각한다.
github : https://github.com/networknt/light-4j
성능 benchmark : https://github.com/networknt/microservices-framework-benchmark
'Development > Java' 카테고리의 다른 글
SpringBoot Jackson(ObjectMapper) Config (0) | 2017.07.07 |
---|---|
[Spring] @Valid Collection Validation 관련 (3) | 2017.06.22 |
[JAVA] Object Pooling (0) | 2017.05.19 |
[Spring] Springboot JUnit 테스트 시 설정 (0) | 2017.05.19 |
[logger] Logback xml 설정 및 디펜던시 설정 (0) | 2017.05.19 |