Hystrix with Spring ( Circuit Breaker ) 를 사용할 때 주의해야 할 점.
기본 Hystrix 라이브러리는 직접 HystrixCommand를 상속받아 정의해서 써야 하므로 약간 번거롭다.
Spring을 쓰고 있다면 @HystrixCommand 애너테이션으로 이런 번거로운 설정을 대신하는 라이브러리를 도입할 수 있다.
아래 dependency를 추가하도록 하자.
```xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
```
`` spring-cloud-starter-hystrix``는 deprecated 되었기 때문에 `` spring-cloud-starter-netflix-hystrix``를 사용해야 한다는 점과, SpringBoot 2.x 대를 사용하고 있다면 hystrix 버전도 2.x.x 버전으로 맞춰 주어야 한다는 점이다. 1.x.x 버전을 사용하면 서버 시작 시 다음과 같은 에러가 발생한다.
```
14:53:46.246 [restartedMain] DEBUG org.springframework.boot.context.logging.ClasspathLoggingApplicationListener - Application failed to start with classpath: [file:/Users/user/source/pointchange/target/classes/]
14:53:46.272 [background-preinit] DEBUG org.jboss.logging - Logging Provider: org.jboss.logging.Log4j2LoggerProvider
14:53:46.273 [background-preinit] INFO org.hibernate.validator.internal.util.Version - HV000001: Hibernate Validator 6.0.17.Final
...
14:53:46.668 [restartedMain] ERROR org.springframework.boot.SpringApplication - Application run failed
java.lang.NoSuchMethodError: org.springframework.boot.builder.SpringApplicationBuilder.<init>([Ljava/lang/Object;)V
```
Circuit Breaker는 기본적으로 @Repository에 붙여준다고 생각한다.
예를 들어, 내 서비스에서 여러 외부 API를 호출하고 있는데 이 중 한 개의 외부 API에서 장애가 발생해서 이 API call은 계속 Timeout(e.g., 2초)이 발생하는 상황이다. 이런 경우 장애가 해결될 때 까지 내 서비스는 기존보다 최대 2초 느려질 것이다.
Circuit-Breaker는 이렇게 어떤 API가 제대로 응답을 못해주는 상황일 때, 굳이 매번 [요청-Timeout]을 반복하지 말고, 아예 일정 시간 동안 요청을 하지 않고 바로 default 응답을 반환해주도록 하는 패턴을 의미한다.
그래서 Circuit-Breaker는 외부 API에 대한 호출이 발생하는 @Repository, 즉 Persistence layer에 붙여주는게 일반적이다.
Hystrix를 추가/제거해도 기존 코드를 크게 변경하지 않는 설계?
Hystrix를 도입하면서 제일 신경을 쏟은 부분은, 기존 코드가 최대한 Hystrix 의존성을 갖지 않도록 하는 것이었다. Hystrix를 제거할 때 코드의 변경 지점을 최소화하여 언제든 추가/제거 가능하도록 구성하고 싶었다.
Hystrix는 지정된 메서드에서 예외가 발생하면 자동으로 fallback Method가 대신 실행되어 리턴되는 형식으로 동작한다. 따라서 기존 코드에 fallback layer가 생기는 것은 피할 수 없다. 다만 이 fallback layer가 없는 것 처럼 만들 수는 있었는데, fallback에서 잡은 예외를 다시 위로 던져주는 것이다. 이렇게 구성하면 Hystrix가 제거되면서 fallback layer가 사라져도 기존 코드는 변경하지 않을 수 있다.
```java
fun fallback(Throwable cause) {
throw (RuntimeException)cause;
}
```
그러나 이렇게 모든 예외를 throw하게 되면 Hystrix 관련 Exception까지 상위 메서드로 던져진다.
이를 별도로 처리해줘야 하는 상황이라면, 상위 메서드에서 HystrixException을 처리하기 위한 별도의 코드가 들어가야 하니 이 또한 상위 layer에 Hystrix 의존성이 생긴다.
결론은? Hystrix 관련 Exception은 fallback에서 처리해서 리턴해주고, 관련 Exception이 아니라면 그냥 다시 throw해버리는 식으로 처리하는 것이 가장 의존성을 낮추는 방법이 아닐까 생각한다.
```java
public UserInfoResponse getUserInfoFallback(String ci, Throwable cause) {
if (cause instanceof RuntimeException && cause.getMessage().contains("Hystrix circuit short-circuited and is OPEN")) {
return new UserInfoResponse(Status.CIRCUIT_OPEN);
}
throw (RuntimeException)cause;
}
```
물론 이러한 방식은 `` Status.CIRCUIT_OPEN``이라는 코드를 추가해주어야 한다. 그러나 Code 타입을 수정하는 것이 비즈니스 로직을 수정하는 것 보다는 낫다.
생각해보면 Exception 시 fallbackMethod가 호출된다는 것은 다음과 같은 방식으로도 표현 가능하다.
```java
fun originalMethod() {
try {
return thisBody();
catch (Throwable cause) {
return fallbackMethod();
}
}
```
둘 중 어느 방식을 사용할 것이냐는, try-catch 책임이 어느 클래스에 있는지에 따라 결정하면 된다. 책임이 상위 layer에 있는 경우 fallback에서 바로 throw해서 상위 layer에서 잡아 처리하도록 하면 되고, 책임이 originalMethod가 속한 클래스에 있다면 fallback 메서드에서 잡아서 적당히 return해주면 된다.
internal use method에 @HystrixCommand를 붙여도 fallback으로 빠지지 않는 현상
2019/07/30 - [Web/Spring] - Spring AOP / @annotation 여기에 원인과 해결방법을 옮겨 두었다!
HystrixTimeoutException이 발생한다고 해서 작업이 중단되는 것은 아니다.
`` @HystrixCommand``가 걸려 있는 메서드가 호출되었을 때 HystrixTimeout 내에 작업이 끝나지 않는 경우, `` HystrixTimeoutException``이 발생하면서 fallbackMethod가 실행되고 원래 메서드 대신 fallbackMethod의 리턴값이 리턴된다.
하지만 그렇다고 해서 원래 메서드에서 진행되던 작업이 취소 되는 것은 아니다!
예를 들어, 외부 API가 응답하는데 3초, HystrixTimeout이 1초라고 가정하면
1초가 지나면서 fallbackMethod가 대신 바로 리턴 되고, 원래 메서드는 계속 외부 API의 응답을 대기하다가 3초 이후 응답을 수신하면서 이후 작업들을 계속 진행하게 된다.
그래서 Timeout이 발생하면 아예 메서드를 종료해야 하는 경우는 HystrixTimeout을 사용하지 말고 자체적인 Timeout(예를 들면 `` restTemplate``의 Timeout)을 사용해야 한다.
commandKey는 꼭 명시해주는 것이 좋다. 중복되지 않는 것으로.
https://github.com/Netflix/Hystrix/issues/1617
interface나 class를 상속받아서 구현하는 경우 메서드 이름이 같아지는 케이스에 특히 주의해야 함.
commandKey를 명시하지 않는 경우 기본적으로 메서드 이름을 사용하기 때문에 같은 commandKey에 연결된다. 의도치않게 카운팅이 같이 되고 circuit도 함께 동작하게 되는 수가 있음.
설정 옵션들
https://github.com/Netflix/Hystrix/wiki/Configuration
https://github.com/Netflix/Hystrix/tree/master/hystrix-contrib/hystrix-javanica#configuration
```properties
execution.isolation.thread.timeoutInMilliseconds : 3000
HystrixTimeout 시간 제한 설정. default 1000 (1s)
errorThresholdPercentage : 50
정해진 시간 내 몇 % 의 요청이 error이면 circuit을 열 것인가?를 결정 default 50
requestVolumeThreshold : 2
몇 회 이상 호출되었을 때 circuit을 열기 위한 카운트를 계산할 것인가? default 20
metrics.rollingStats.timeInMilliseconds: 10000
발생하는 error를 몇 초 동안 카운트 할 것인가?
I.e. 10s 동안 2회 이상 호출되었을 때 50% 이상의 요청이 실패이면 circuit을 open한다. default 10000
circuitBreaker.sleepWindowInMilliseconds : 5000
한 번 open한 circuit을 몇 초 동안 유지할 것인가? (5s가 지나면 check 후 open을 유지할지 close할지 결정) default 5000
```
애너테이션을 해당 클래스의 모든 메서드에 일괄 적용하기
https://github.com/Netflix/Hystrix/tree/master/hystrix-contrib/hystrix-javanica#defaultproperties
Hystrix dashboard 적용하기
별도의 모니터링용 서버를 따로 둬도 되고 그냥 서버 하나에서 모니터링까지 해도 됨. gradle이나 maven에 의존성 추가하고
```xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
<version>2.1.2.RELEASE</version> // hystrix랑 버전 맞춰서
</dependency>
```
http://127.0.0.1:8080/hystrix 접속하면 hystrix가 구동중인 서버의 주소를 입력 할 수 있는 모니터링 페이지가 뜸.
여기서 http://127.0.0.1:8080/actuator/hystrix.stream 뭐 이런 식으로 입력하면 된다고 안내가 나와있는데, 해도 404가 뜬다. 당연...
특정 url로 접근했을 때 현재 circuit의 상태를 반환해줄 수 있도록 의존성을 추가해준다.
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>
```
그리고 application.properties에
```
management.endpoints.web.exposure.include=*
- http://127.0.0.1:8080/hystrix로 접속
- http://127.0.0.1:8080/actuator/hystrix.stream 입력하고 Monitor Stream 버튼 누름
- circuit 관련 함수가 한 번 이상 호출되어야 모니터링 탭에 나타나므로, 관련 url에 접속하고 나서 확인해보면 그와 관련된 circuit 상태가 나타남
참고
https://jeong-pro.tistory.com/183
https://bcho.tistory.com/1250?category=431297
https://supawer0728.github.io/2018/03/11/Spring-Cloud-Hystrix/
'Java Stack > Spring' 카테고리의 다른 글
[Spring] Controller에서 사용하는 애너테이션 (0) | 2019.09.25 |
---|---|
Spring AOP / @annotation resolve (0) | 2019.07.30 |
[Spring] context.getBean() 사용하기 (0) | 2019.07.29 |
[Spring] DB 관련 : H2 설정 (0) | 2019.07.23 |
RestTemplate 사용 시 ResponseType으로 generic 타입 받기 (ParameterizedTypeReference) (1) | 2019.07.11 |