Java

실습으로 확인하는 tomcat server vs netty server

PSAwesome 2022. 7. 12. 22:56
반응형

reactor logo

안녕하세요.

소스를 분석하다 tomcat 서버에 webclient를 사용한 것을 보고 의문이 생겨 테스트를 한 결과를 공유하고자 합니다.

 

제가 공부한 결과로는 reactive programming은 비동기와 non-blocking으로 데이터가 지속적으로 흘러갈 수 있도록 구현이 중요하다고 기억하고 있습니다.

제가 전에 알고 있던 내용이 맞다고 확신하여 몇 가지 결과를 확인할 수 있었습니다.

 

  1. 비동기 호출이 필요할 때 tomcat 서버여도 webclient를 사용해도 나쁘지 않았다. (async restTemplate은 테스트하지 않았습니다.)
  2. reactive programming의 제대로된 활용은 netty 서버이다.
  3. reactive를 지원하지 않는 서버가 있으면 reactive 성능은 반감된다.

 

글의 흐름은 아래와 같습니다.

  1. 테스트 서버 구성
  2. 주요 소스코드
  3. network pending 결과
  4. 각 서버마다 요청별 상태 확인
    1. 100개 요청
    2. 1000개 요청
    3. visualvm 모니터링

 

 

1. 테스트 서버 구성

  1. 가장 상위 서비스는 레거시 서버입니다.
  2. 하위 tomcat 서버는 spring framework5 내장 tomcat 서버입니다.
  3. 하위 netty 서버는 spring framework5 내장 netty 서버입니다.
  4. actor는 100, 1000개의 요청입니다.

 

2. 주요 소스

 

LoadTest.java : 요청을 보내는 main 클래스 (2019년도에 toby live 방송에서 활용한 코드를 조금 수정했습니다.)

@Slf4j
public class LoadTest {

    static AtomicInteger counter = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException, BrokenBarrierException {
        final var REQUEST_USER = 1000;
        ExecutorService es = Executors.newFixedThreadPool(REQUEST_USER);

        RestTemplate rt = new RestTemplate();
//        String url = "http://localhost:8070/netty/{cnt}";
//        String url = "http://localhost:8070/netty-rest/{cnt}";
//        String url = "http://localhost:8071/tomcat/{cnt}";
//        String url = "http://localhost:8071/rest/{cnt}";
//        String url = "http://localhost:8071/tomcat/{cnt}/block";

//        String url = "http://localhost:8070/rsocket/{cnt}";
//        String url = "http://localhost:8070/netty-rest/rsocket/{cnt}";
        String url = "http://localhost:8071/tomcat/rsocket/{cnt}";

        CyclicBarrier cyclicBarrier = new CyclicBarrier(REQUEST_USER + 1);

        for (int i = 0; i < REQUEST_USER; i++) {
            es.submit(() -> {
                int cnt = counter.addAndGet(1);
                cyclicBarrier.await();

                log.info("Thread {}", cnt);

                StopWatch sw = new StopWatch();
                sw.start();

                TempResponse res = rt.getForObject(url, TempResponse.class, cnt);

                sw.stop();
                log.info("Elapsed: {} -> {} / {}", cnt, sw.getTotalTimeSeconds(), res);

                return null;
            });
        }

        cyclicBarrier.await();
        StopWatch main = new StopWatch();
        main.start();

        es.shutdown();
        es.awaitTermination(103, TimeUnit.SECONDS);

        main.stop();
        log.info("total: {}", main.getTotalTimeSeconds());
    }
}

 

WebClient config : webclient 설정 bean 입니다. 하단의 tomcat, netty에서 활용합니다.

@Bean
WebClient webClient() {
    HttpClient httpClient = HttpClient.create()
                                      .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
                                      .responseTimeout(Duration.ofSeconds(12))
                                      .doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(12, TimeUnit.SECONDS))
                                                                 .addHandlerLast(new WriteTimeoutHandler(12, TimeUnit.SECONDS)));


    return WebClient.builder()
                    .baseUrl("http://localhost:8080")
                    .clientConnector(new ReactorClientHttpConnector(httpClient))
                    .build();
}

 

Netty server - RouterFunction, handler

@Bean
RouterFunction<ServerResponse> router() {
    return route()
            .GET("/netty", this::helloMono)
            .GET("/netty/{cnt}", this::helloFlux)
            .GET("/netty/{cnt}/block", this::block)

            .GET("/rsocket/{cnt}", this::rsocket)
            .build();
}


private Mono<TempResponse> apiCall(LocalDate date, MediaType applicationJson) {
    return client.get()
                 .uri(uri -> uri.path(PATH)
                                .queryParam("baseDate", date)
                                .build()).accept(applicationJson)
                 .retrieve()
                 .bodyToMono(TempBody.class)
                 .map(TempBody::getResult)
                 .map(s -> s.setBaseDate(date))
                 .log();
}


private Mono<ServerResponse> rsocket(ServerRequest request) {
    var cnt = Integer.parseInt(request.pathVariable("cnt"));
    log.info("limit rate : {}", cnt);

    return ServerResponse.ok()
                         .body(requester.route(String.format("temp.count.%d", cnt)).retrieveFlux(TempResponse.class), TempResponse.class);
}

 

Tomcat server - block

@GetMapping("{cnt}/block")
List<TempResponse> block(@PathVariable Integer cnt) {
    LocalDate date = baseDate();
    return IntStream.range(0, cnt)
                    .mapToObj(date::plusMonths)
                    .flatMap(d -> Stream.of(apiCall(d).block()))
                    .collect(Collectors.toList());
}

 

RSocket server

@Controller
@RequiredArgsConstructor
@Slf4j
public class RSocketApi {
    private final TempDataRepository repository;

    @MessageMapping("temp.count.{cnt}")
    Flux<TempResponse> getCnt(@DestinationVariable Integer cnt) {
        log.info("limit rate : {}", cnt);
        var ids = Stream.generate(() -> ThreadLocalRandom.current().nextLong(0, 10_000))
                        .limit(cnt)
                        .distinct()
                        .collect(Collectors.toList());

        return repository.findAllByIdIn(ids)
                         .log();
    }

}

@Bean
CommandLineRunner init() {
    return args -> Flux.range(0, 10_000)
                       .map(notUsed -> EnhancedRandomBuilder.aNewEnhancedRandom().nextObject(TempResponse.class, "id"))
                       .flatMap(repository::save)
                       .subscribe();
}

- 초기 rsocket server에 샘플 데이터를 추가합니다.

 

 

3. network pending

webflux 초기에 thymeleaf, reactor postgresql 조합으로 chunk 단위로 network pending을 줄이는 작업을 테스트했던 기억이 있어서 chunk 여부만 확인했습니다.

ex)

  • 1만 개의 row를 응답할 때 10초 pending 후 1만 row를 한 번에 응답
  • 1만 개의 row를 2천 개로 나누어 다섯 번에 걸쳐서 응답.

 

1. netty server

netty network pending

 

2. netty request-mapping (netty server와 차이점 없음)

netty server request mapping 버전

 

3. tomcat server

tomcat server

 

4. rsocket on netty

rsocket on netty server

 

 

4. 각 서버마다 요청/응답 결과

1. tomcat <--- netty webclient

netty server request

  • netty server의 thread 50 선을 유지
  • pending이 한계치를 초과할 때 500 error를 반환

 

결과

  • pending이 초과한 요청은 500 error를 응답하고 60초 근처에서 완료

 

2. tomcat <--- netty webclient

netty server @RestController 호출

  • router function과 차이가 있는지 확인

결과

  • 총 1분 53초 소요

 

3. tomcat <--- tomcat webclient

tomcat server request

  • 요청 당 thread 생성이 기본 구조인 tomcat은 thread가 급격하게 증가
  • 순간 한계치를 초과하는 요청은 500 error 반환

결과

  • 최대 시간을 초과
  • 마무리 해야할 응답은 끝까지 완료
  • 총 2분 14초 소요

 

4. tomcat <-- tomcat (restTemplate)

restTemplate request

  • thread 증가는 webclient 요청과 동일
  • restTemplate timeout 설정을 하지 않았고, 모든 요청은 성공
  • 100 이상으로 추가 요청이 발생될 경우 서버가 다운될 가능성이 큼

결과

  • 총 19분 13초 소요

 

5. tomcat <-- tomcat (webclient block)

@GetMapping("{cnt}/block")
List<TempResponse> block(@PathVariable Integer cnt) {
    LocalDate date = baseDate();
    return IntStream.range(0, cnt)
                    .mapToObj(date::plusMonths)
                    .flatMap(d -> Stream.of(apiCall(d).block()))
                    .collect(Collectors.toList());
}
  • webclient 호출마다 block 코드를 추가

tomcat server webclient block request

  • thread 증가
  • pending 초과 시 500 error 발생

결과

  • 3분 3초 소요

 

레거시 서버에 webclient 요청 시
webclient 서버가 다운되는 것을 어느정도 예방할 수 있다는 것을 알 수 있었습니다.
그리고 궁금증을 확인하기로 합니다.



non-blocking 결과는 어떨까?



1-2. legacy tomcat -> netty  rsocket server

1. rsocket <-- netty webclient

webclient request

  • thread는 50 선 유지
  • 모든 요청 성공

결과

  • 1분 30초 소요

 

2. rsocket <-- netty request-mapping webclient

@RestController webclient request

  • thread 50 선 유지
  • 모든 요청 성공

결과

  • 총 1분 39초 소요

 

3. rsocket <-- tomcat webclient

tomcat server webclient request

  • thread 증가
  • 모든 요청 성공

결과

  • 총 1분 25초 소요

 

1-2. rsocket server 요청 1000개

1. rsocket <-- netty webclient

netty server 1000 request

  • thread 50 선 유지
  • 모든 요청 성공 

결과

  • 로그에는 전부 response 200 OK이긴 하나, 정확한 개수는 확인하지 않았습니다.
  • 총 3분 1초 소요, 로그에 찍힌 총 시간은 1분 43초...

 

2. rsocket <-- tomcat webclient

tomcat webclient 1000 request

  • thread 증가
  • 모든 요청 성공

결과

  • 로그에는 전부 response 200 OK이긴 하나, 정확한 개수는 확인하지 않았습니다.
  • 총 4분 6초 소요, 로그는 1분 43초

 

여기까지 실험으로 알아보는 비교 테스트였습니다.

마지막으로 제가 다시 느낀 것은 19년도랑 같은 결론입니다.

감사합니다.

 

reactive를 하려면 DB에 접속하는 서버를 구축하기로 하고,
가능한 연관된 서버도 reactive 하게 구축하는 것이 가장 좋다.
반응형