[Framework] Light4J(light-java)
Development/Java

[Framework] Light4J(light-java)

반응형

음.. 프레임워크에 대해 알아보고자 하는데


익히 알고있는 웹 서비스를 하기 위한 프레임워크.


즉, Web framework의 종류엔.. 널리 알려진것들이 몇개 있는데


Spring, Spring-boot, undertow, latpack 등등이 있다


근데 워낙 최근 성능에 대한 관심이 많아져서 찾아보다보니 


그 중, light-4j(예전엔 프레임워크 이름이 light-java 였다) 에 대해 알아보게 되었다.


하여.. 성능이 뭐 얼마나 차이날까 해서 봤는데


FrameworkLanguageMax ThroughputAvg LatencyTransfer
Go FastHttpGo1,396,685.8399.98ms167.83MB
Light JavaJava1,344,512.652.36ms169.25MB
ActFrameworkJava945,429.132.22ms136.15MB
Go IrisGo828,035.665.77ms112.92MB
VertxJava803,311.312.37ms98.06MB
Node-uwsNode/C++589,924.447.22ms28.69MB
Dotnet.Net486,216.932.93ms57.03MB
SeedStack-FilterJava343416.334.41ms51.42MB
Jooby/UndertowJava317,385.054.31ms45.70MB
Spring Boot ReactorJava243,240.177.44ms17.86MB
Pippo-UndertowJava216,254.569.80ms31.35MB
SparkJava194,553.8313.85ms32.47MB
Pippo-JettyJava178,055.4515.66ms26.83MB
Play-JavaJava177,202.7512.15ms21.80MB
Go HttpGo170,313.0215.01ms20.95MB
JFinalJava139,467.8711.89ms29.79MB
Akka-HttpJava132,157.9612.21ms19.54MB
RatPackJava124,700.7013.45ms10.82MB
Pippo-TomcatJava103,948.1823.50ms15.29MB
Bootique + Jetty/JerseyJava65,072.2039.08ms11.17MB
SeedStack-Jersey2Java52310.1126.88ms11.87MB
BaseioJava50,361.9822.20ms6.39MB
NinjaFrameworkJava47,956.4355.76ms13.67MB
Play 1Java44,491.8710.73ms18.75MB
Spring Boot UndertowJava44,260.6138.94ms6.42MB
Nodejs ExpressNode42,443.3422.30ms9.31MB
DropwizardJava33,819.9098.78ms3.23MB
Spring Boot TomcatJava33,086.2282.93ms3.98MB
Payra MicroJava24,768.69118.86ms3.50MB
WildFly SwarmJava21,541.0759.77ms2.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();
        }


저렇게 HashMap을 이용해서 설정을 하는 부분은 오타나, 적용되었는지 확인이 힘들어서 별로 내키지 않는 부분이긴 한데.. 여튼

설정을 위와 같이 예시로 하여 map에 넣은 뒤, 해당 설정을 json파일로 내보내서 차후 로드할때 사용하는 것을(혹은 다른곳에서 동일 설정으로 사용하고자 할 때) 이용해서 Config를 지원하고 있다.

다음은 해당 Config를 가지고 서버를 실행하는 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

반응형