Web/Spring 2020. 2. 20. 00:46

 

오늘 다루어볼 내용은 Webflux에서 사용되는 WebClient가 onCancel이 호출되는 시점이다.  이번에 애플리케이션을 개발하면서 많은 서비스가 통신을 하는데, 필자가 개발한 애플리케이션의 jaeger 로그를 보니 WebClient가 cancelled("The subscription was cancelled") 되었다는 로그가 찍혀있었다. 무슨 이유로 이러한 로그가 남는지 확인했더니, 아래와 같은 이유였다.

 

"AServer -> BServer ->CServer"

 

이렇게 3개의 서버가 통신하는 상황인데, AServer의 Client는 ReadTimeOut이 3초이고, BServer는 ReadTimeOut이 5초이다. 그런데 CServer가 응답을 주는데 4초가 걸렸다면? 

 

이런 상황에서 발생하는 것이 cancelled 상황이다. AServer는 이미 Readtimeout이 나버려서 500 에러가 발생하였고, 이제 더이상 BServer는 응답을 받아올 필요가 없어졌으므로 subscribe를 cancel을 해버리는 것이다. 간단히 위와 같은 상황을 샘플 앱으로 작성해 보았다.

 

[AServer:9090]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Configuration
public class WebClientConfig {
 
    @Bean
    public WebClient.Builder webClientFactory(){
        TcpClient tcpClient = TcpClient.create()
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000)
                .doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(3000, TimeUnit.MILLISECONDS)));
 
        return WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient)));
    }
 
    @Bean
    public WebClient webClient() {
        return webClientFactory().build();
    }
 
}
 
@Slf4j
@RestController
@SpringBootApplication
@RequiredArgsConstructor
public class ExamApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(ExamApplication.class, args);
    }
 
    private final WebClient client;
 
    @GetMapping("/")
    public Mono<String> hello(){
        log.info("exam server accept request");
        return client.get("http://localhost:8080/")
                .retrieveBodyToMono(String.class);
}
 
 
 
cs

 

[BServer:8080]

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Configuration
public class WebClientConfig {
 
    @Bean
    public WebClient.Builder webClientFactory(){
        TcpClient tcpClient = TcpClient.create()
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000)
                .doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS)));
 
        return WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient)));
    }
 
    @Bean
    public WebClient webClient() {
        return webClientFactory().build();
    }
 
}
 
@Slf4j
@RestController
@SpringBootApplication
@RequiredArgsConstructor
public class ClientApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(ClientApplication.class, args);
    }
 
    private final WebServiceClient client;
 
    @GetMapping("/")
    public Mono<String> hello(){
        log.info("client server accept request");
        return client.get("http://localhost:7070/")
                .retrieveBodyToMono(String.class);
    }
 
}
 
 
cs

 

[CServer:7070]

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@SpringBootApplication
public class ServerApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(ServerApplication.class, args);
    }
 
    @GetMapping("/")
    public String hello() throws InterruptedException {
        Thread.sleep(10000);
        return "hello";
    }
 
}
cs

 

이제 "http://localhost:9090/" 으로 요청을 보내보자!

 

 

보면 AServer(exam)은 ReadTimeOut Exception이 발생하였고, subscription was cancelled라는 메시지를 남기고 있다. 혹시나 이러한 문제가 발생한 사람이 있다면.. 적절히 서버들의 WebClient 옵션을 조정해줄 필요가 있거나 혹은 비정상적으로 응답이 느린 구간이 있을 수 있다. 하지만 때때로 클라이언트 애플리케이션에서 커낵션 풀을 사용하지 않아 과도하게 로컬 포트를 많이 개방한다거나 등의 문제로 cancelled되는 경우가 있으니, 여러가지 상황을 고려해보아야 한다.

 

마지막으로 ConnectionTimeOut, ReadTimeOut, SocketTimeOut의 차이점을 모른다면 아래 링크를 확인하자.

 

2019/02/12 - [프로그래밍언어/Java&Servlet] - Java - ConnectionTimeout,ReadTimeout,SocketTimeout 차이점?

 

Java - ConnectionTimeout,ReadTimeout,SocketTimeout 차이점?

Java - ConnectionTimeout,ReadTimeout,SocketTimeout 차이점? 사실 지금까지 웹개발을 해오면서 ConnectionTimeout,ReadTimeout,SocketTimeout에 대해 대략적으로만 알고있었지 사실 정확히 설명해봐라 혹은 차이..

coding-start.tistory.com

 

posted by 여성게
:
Web/Spring 2018. 10. 11. 22:14

요즘의 소프트웨어는 대부분 서버와 Json 형태의 데이터를 주고 받습니다. Json으로 주고 받게 되면서 이종소프트웨어 간의 통신도 자유로워 질수 있었습니다. 하지만 요즘은 클라이언트 단과 서버단의 Json 통신 이외에도 서버와 서버끼리도 Restful 한 통신을 하는 경우가 많아졌습니다.


RestTemplate의 동작원리


org.springframework.http.client 패키지에 있다. HttpClient는 HTTP를 사용하여 통신하는 범용 라이브러리이고, RestTemplate은 HttpClient 를 추상화(HttpEntity의 json, xml 등)해서 제공해준다. 따라서 내부 통신(HTTP 커넥션)에 있어서는 Apache HttpComponents 를 사용한다. 만약 RestTemplate 가 없었다면, 직접 json, xml 라이브러리를 사용해서 변환해야 했을 것이다.


  1. 어플리케이션이 RestTemplate를 생성하고, URI, HTTP메소드 등의 헤더를 담아 요청한다.

  2. RestTemplate 는 HttpMessageConverter 를 사용하여 requestEntity 를 요청메세지로 변환한다.

  3. RestTemplate 는 ClientHttpRequestFactory 로 부터 ClientHttpRequest 를 가져와서 요청을 보낸다.

  4. ClientHttpRequest 는 요청메세지를 만들어 HTTP 프로토콜을 통해 서버와 통신한다.

  5. RestTemplate 는 ResponseErrorHandler 로 오류를 확인하고 있다면 처리로직을 태운다.

  6. ResponseErrorHandler 는 오류가 있다면 ClientHttpResponse 에서 응답데이터를 가져와서 처리한다.

  7. RestTemplate 는 HttpMessageConverter 를 이용해서 응답메세지를 java object(Class responseType) 로 변환한다.

  8. 어플리케이션에 반환된다.



출처: https://sjh836.tistory.com/141 [빨간색코딩]




Restful(서버-서버) RestTemplate


dependency


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations -->
<dependency>
    <groupId>com.fasterxml.jackson.core/groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>2.9.7</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.7</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.9.7</version>
</dependency>
 
cs


RestTemplate도 결국 내부는 httpClient를 사용합니다. (기본 설정으로 사용한다면 상관없지만 세부 설정을 위해 dependency를 추가해줍니다.)





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package com.spring.javaconfig.controller;
 
import java.net.URI;
 
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
 
import com.spring.javaconfig.bean.RestDTO;
 
@RestController
public class RestApiController {
    
    @RequestMapping(value="/rest")
    public Object getMethod() {
        String botCode="D9UD878EAJ";
        HttpComponentsClientHttpRequestFactory clientHttpRequestFactory=
                new HttpComponentsClientHttpRequestFactory();
        clientHttpRequestFactory.setReadTimeout(5000);
        clientHttpRequestFactory.setConnectionRequestTimeout(5000);
        clientHttpRequestFactory.setConnectTimeout(5000);
        HttpClient httpClient=HttpClientBuilder.create()
                .setMaxConnTotal(100)
                .setMaxConnPerRoute(5)
                .build();
        clientHttpRequestFactory.setHttpClient(httpClient);
        RestTemplate restTemplate=new RestTemplate(clientHttpRequestFactory);
        /*UriComponentsBuilder.newInstance()
            .scheme("http")
            .host("127.0.0.1")
            .port("8983")
            .path("/solr/{botCode}/textanalysis")
            .queryParam("q", "faq테스트")
            .queryParam("rows", 1)
            .queryParam("knn.cinfo", true)
            .queryParam("knn.k", 1)
            .queryParam("intent", true)
            .queryParam("ner", false)
            .queryParam("log", false)
            .build().expand(botCode)
            .encode()
            .toUri();
            
            queryParams 로 map객체를 이용해 한번에 쿼리스트링을 넣을수도 있다.
            */
        return restTemplate.getForObject(UriComponentsBuilder.newInstance()
                .scheme("http")
                .host("localhost")
                .port("8983")
                .path("/solr/{botCode}/select")
                .queryParam("q""faq테스트")
                .build().expand(botCode)
                .encode()
                .toUri(), Object.class);
    }
}
 
cs



RestTemplate은 커낵션을 열고 사용을 마친 후에 커넥션을 닫게 되면 close Wait상태가 된다고 합니다. 즉, db의 커넥션 풀과 같은 설정을 추가하여 불필요한 리소스, 성능이슈를 적절히 해결할 필요가 있습니다. 이 예제는 검색엔진인 solr를 이용하여 간단히 쿼리를 날려 결과를 받아 보는 요청입니다. 각각 메소드의 기능은 구글링해보시길...

posted by 여성게
: