본문 바로가기

Spring/Spring Cloud

Hystrix & Ribbon

반응형

Hystrix 란, 

모노리틱 아키텍쳐가 아닌, MAS 를 따를 때, 각 서버에 대한 액세스 지점을 격리하여 하나의 서버에서 발생한 장애가 다른 서버에 영향을 주는 것 ( 계단식 장애 전파 ) 을 방지와 복원하기 위한 위한 여러 기능을 제공해주는 지연 및 장애 내성 (latency and fault tolerance) 라이브러리이다. 
github.com/Netflix/Hystrix/wiki

지연 및 장애 내성 ( Latency and Fault Tolerance )

  • 장애가 전파 방지
  • Fallback 과 graceful degradation.
  • 빠르고 신속한 복구
  • circuit breaker를 이용한 Thread and semaphore를 격리

Realtime Operations

  • 실시간 모니터링 및 configuration 변경
  • 오류 감시 서비스와 property 변경 즉시 적용
  • 경고를 받고, 결정을 내리고, 변화에 영향을 미치며, 몇 초 안에 결과를 확인할 수 있음

Concurrency

  • Parallel execution.
  • Concurrency aware request caching.
  • Automated batching through request collapsing.

fallback 메소드와 circuit breaker 사용해보기

사용자 UI 구성을 위한 Display 서버에서 Product 정보를 가져오는 API는 Product 서버로 분리했다 가정해보자,
이 때, Product 서버에서 장애가 발생하면, Display 서버에도 그 장애가 전파가 되게 된다. 가령 응답 받지 못한 요청에 대한 요청을 기다리는 데에 모든 스레드가 할당되어버릴 수도 있다.

이러한 장애 전파 방지를 위해 

build.gradle

    compile('org.springframework.cloud:spring-cloud-starter-netflix-hystrix')
    compile('org.springframework.cloud:spring-cloud-starter-netflix-ribbon')

application.yml

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 3000 # default 1000
       circuitBreaker:
        requestVolumeThreshold: 10 # default 20
        errorThresholdPercentage: 50 # default 50
      getProducts:
        execution:
          isolation:
            thread:
              timeoutInMilliseconds: 10000
        circuitBreaker:
          requestVolumeThreshold: 10 # default 20
          errorThresholdPercentage: 50 # default 50
  • hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds
    요청에 대한 Time Out 설정
    기본 값이 1초라 요청이 오래 걸리는 경우 반드시 수정이 필요
  • hystrix.command.circuitBreaker.requestVolumeThreshold
    오류 감시 요청 수 (20 개라면 20 개의 요청에 대한 오류를 감시)
  • hystrix.command.circuitBreaker.errorThresholdPercentage
    감시한 요청 개수 (requestVolumeThreshold) 중 해당 퍼센테이지 이상의 호출에서 오류가 발생할 경우 circuit 오픈

Display

DisplayApplication.java

@EnableCircuitBreaker

을 추가한다.

@EnableCircuitBreaker
@SpringBootApplication
public class DisplayApplication {

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    public static void main(String[] args) {
        SpringApplication.run(DisplayApplication.class);
    }

}

DisplayController.java

@RestController
@RequestMapping(path = "/displays")
public class DisplayController {

    private final ProductRemoteService productRemoteService;

    public DisplayController(ProductRemoteService productRemoteService) {
        this.productRemoteService = productRemoteService;
    }

    @GetMapping(path = "/{displayId}")
    public String getDisplayDetail(@PathVariable String displayId) {
        String productInfo = getProductInfo();
        return String.format("[display id = %s at %s %s ]", displayId, System.currentTimeMillis(), productInfo);
    }

    private String getProductInfo() {
        return productRemoteService.getProductInfo("12345");
    }
}

ProductRemoteServiceImpl 에서 API 호출하는 메소드에 

@HystrixCommand(fallbackMethod = "getProductInfoFallback")

 애노테이션을 통해 fallback 메소드를 지정한다.

@Service
public class ProductRemoteServiceImpl implements ProductRemoteService {

    private static final String url = "http://localhost:8083/products/";
    private final RestTemplate restTemplate;

    public ProductRemoteServiceImpl(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @Override
    @HystrixCommand(fallbackMethod = "getProductInfoFallback")/*(commandKey = )*/
    public String getProductInfo(String productId) {
        return this.restTemplate.getForObject(url + productId, String.class);
    }

    public String getProductInfoFallback(String productId, Throwable t){
        System.out.println("t = " + t);
        return "[This Product is sold out]";
    }
}

Product

ProductController.java

ProductController에서 임의로 exception을 발생시켜 보자.

@RestController
@RequestMapping("/products")
public class ProductController {

    @GetMapping(path = "{productId}")
    public String getProductInfo(@PathVariable String productId) {
       
        throw new RuntimeException("I/O Exception");
    }
}

테스트를 위해 requestVolumeThreshold 를 1 로 지정해놓고 요청을 해본 결과,

단 한번의 exception 이 발생하고, (requestVolumeThreshold 를 1로 설정했기 때문) 서킷이 바로 오픈되었음을 알 수 있다.

실제 요청 응답으로는 Product에서 발생한 에러를 전달받는 것이 아니라, Display 의 fallback 메소드에 정의한 결과가 리턴되는 것을 확인할 수 있다.

Ribbon 란,

github.com/Netflix/ribbon

spring.io/guides/gs/client-side-load-balancing/

Client Side에서 여러 서버를 라운드로빈 방식으로 Load Balancing 해준다.

build.gradle

	compile('org.springframework.retry:spring-retry:1.2.2.RELEASE')  // spring cloud requires spring-retry for auto-retry
	compile('org.springframework.cloud:spring-cloud-starter-netflix-ribbon') 

application.yml

ribbon:
product:
  ribbon:
    listOfServers: localhost:8082,localhost:7777

MaxAutoRetries 와 MaxAutoRetriesNextServer 를 설정하여, 하나의 서버에서 에러가 발생했을 경우, 다른 서버로 재시도 할 수 있다. 

Display

DisplayApplication.java

RestTemplate 빈 정의 시 @LoadBalanced 추가

@SpringBootApplication
@EnableCircuitBreaker
public class DisplayApplication {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    public static void main(String[] args) {
        SpringApplication.run(DisplayApplication.class);
    }

}

DisplayServiceImpl.java

server url 을 설정 파일에서 정의한 product 로 변경해준다.

@Service
public class ProductRemoteServiceImpl implements ProductRemoteService {

    private static final String url = "http://product/products/";
    private final RestTemplate restTemplate;

    public ProductRemoteServiceImpl(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @Override
    @HystrixCommand(commandKey = "productInfo", fallbackMethod = "getProductInfoFallback")
    public String getProductInfo(String productId) {
        return this.restTemplate.getForObject(url + productId, String.class);
    }

    public String getProductInfoFallback(String productId, Throwable t) {
        System.out.println("t = " + t);
        return "[ this product is sold out ]";
    }
}

실행 시, 설정파일에 정의되어 있는 서버 리스트로 요청을 변경하여 호출하는 것을 알 수 있다.

DynamicServerListLoadBalancer:{NFLoadBalancer:name=product,current list of Servers=[localhost:8084, localhost:7777],Load balancer stats=Zone stats: {unknown=[Zone:unknown; Instance count:2; Active connections count: 0; Circuit breaker tripped count: 0; Active connections per server: 0.0;]

restTemplate 는 LoadBalancerInterceptor 가 추가된 것을 확인할 수 있다.

application.yml 파일에 retry 옵션을 설정해주면

  • MaxAutoRetries   동일한 서버에서 실패한 요청을 재시도하는 횟수 (default 0)
  • MaxAutoRetriesNextServer – 현재 서버를 제외하고 시도할 서버 수 (default 0)
  • retryableStatusCodes 재시도할 HTTP 상태 코드 목록 (500, 503, ...)

application.yml

ribbon:
product:
  ribbon:
    listOfServers: localhost:8082,localhost:7777
    MaxAutoRetries: 0
    MaxAutoRetriesNextServer: 1

실행 시, 7777 서버에서 실패할 경우, 8082 서버로 재시도 한다. RetryLoadBalancerInterceptor 추가로, policy에 맞게 서버를 선택하여 요청을 재시도 한다.

참조 > Spring Cloud 기반 MSA로의 전환

반응형

'Spring > Spring Cloud' 카테고리의 다른 글

Zuul - API Gateway  (0) 2021.01.17
Feign  (0) 2021.01.17
Eureka  (0) 2021.01.17