티스토리 뷰

Java/spring

Netflix Hystrix

LichKing 2018.12.23 17:45

오늘은 Spring Cloud Hystrix 에 대해서 포스팅하려한다. 넷플릭스에서 Hystrix 를 만들어서 공개했는데 이를 좀 더 스프링 친화적으로 스프링에서 래핑해놓았다. Hystrix는 Circuit Breaker 패턴을 적용하여 서비스를 제공하는 Supplier에 장애가 생겼을때 Supplier를 호출하는 Consumer까지 장애가 전파되지않도록 Circuit Breaker 를 오픈하는 방식의 라이브러리이다. 외부 API를 호출하는 경우가 있다면 장애전파를 막고자할때 유용하게 사용할 수 있다. 이번 포스팅에서 작성할 애플리케이션은 Supplier, Consumer와 모니터링에 사용할 Dashboard, 그리고 여러 인스턴스를 모아서 모니터링할때 필요한 Eureka와 Turbine까지 총 5개다.


1. Supplier

먼저 서비스를 제공할 Supplier 를 하나 만든다. 최근 애플리케이션은 꼭 MSA 구조로서 내부 API를 호출하는 경우가 아니라도 외부 API를 호출하는일은 매우 일상적인 일이다. Hystrix는 Client에만 설정해서 처리하므로 외부 API를 호출하는 경우에도 적용할 수 있다. 그러므로 Supplier에는 Hystrix 코드가 전혀 존재할 필요가 없으며, 아주 단순하게 구성해도 상관없다.


@SpringBootApplication
public class SupplierApplication {

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

@RestController
static class Controller {
@GetMapping("/supplier")
public String test() {
return "hello supplier";
}
}
}

(단순한 Rest api 하나만을 노출하는 Supplier)


로컬에서 테스트한다면 포트충돌을 피하기위해 포트지정도 함께 해준다.


server:
port: 8090


2. Consumer

Consumer도 간단한 Spring boot 애플리케이션으로 만들면 된다. Hystrix 의존성만 추가해주자.


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>


별생각 없이 의존성을 추가한다면 spring-cloud-starter-hystrix 의존성을 추가했을수도있다. 내가 지금 작성한 의존성과 위 pom.xml 의 의존성이 동일한건지 확인해보자. 이름이 좀 다른걸 볼 수있다.


난 처음에 아무생각없이 갖다쓰다가 이 두개가 다른걸 확인하고 좀 당황했었는데 위 pom.xml 에 있는 spring-cloud-starter-netflix-hystrix 를 사용하면 된다. spring-cloud-starter-hystrix 는 deprecated 됐다.

( https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-hystrix )


이제 다시 Consumer 코드로 오자. 생각보다 정말 해줄게 별로 없다.


@EnableCircuitBreaker
@SpringBootApplication
public class ConsumerApplication {

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

@RestController
static class Controller {
@GetMapping("/consumer")
@HystrixCommand(fallbackMethod = "fallback")
public String consumer(@RequestParam String path) {
System.out.println("log :: " + path);

ResponseEntity<String> entity = new RestTemplate().getForEntity("http://127.0.0.1:8090/" + path,
String.class);

if(entity.getStatusCode() == HttpStatus.OK) {
return entity.getBody();
}

throw new RuntimeException("supplier is not OK");
}

private String fallback(String path) {
return "hello fallback";
}
}
}

@EnableCircuitBreaker 애노테이션을 달아 Circuit Breaker 를 활성화 시키고 Hystrix 을 적용할 메서드에 @HystrixCommand 애노테이션을 이용해 Hystrix를 적용하면 끝난다. @HystrixCommand 애노테이션에 fallback 메서드를 지정해주면 Hystrix 메서드가 실패했을때 fallback 메서드를 호출해 처리하게된다. fallback 메서드를 작성할때는 기존 메서드와 반환타입과 파라미터를 동일하게 해준다. 접근제어자는 달라도 상관없다.


두가지 짚고 넘어갈게있는데 서킷 브레이커는 닫힌(close) 상태가 기본이고, 상태변이가 발생해 메서드 호출을 막을때가 열린(open) 상태다. 헷갈릴 수 있으니 주의하자. 또 fallback 메서드는 서킷 브레이커가 열리지 않더라도 호출된다. 난 처음에 서킷 브레이커가 열리지않은 상태에서 메서드가 실패하면 그땐 fallback을 호출하지않을줄 알아 미리 적어놓는다.


현재 consumer 메서드는 간단하게 기 작성된 Supplier 서버를 호출하여 200 응답이 아니면 예외를 발생하게 작성되어있다. 메서드가 성공하거나 실패하는 경우를 쉽게 구현하기위해 path 를 파라미터로 받도록 했다.


Supplier와 포트가 충돌하지않도록 Client 포트는 9000으로 지정했다.


server.port=9000


이제 Supplier와 Consumer를 구동하여 consumer를 호출해보자.



curl을 이용해 호출했다. 다른 유틸리티를 이용해도되고, 브라우저를 이용해도 된다. path를 supplier로 보내면 정상적으로 호출에 성공하고 "hello supplier" 라는 정상응답을 받게된다.


이번엔 path 파라미터 값을 바꿔보자. 뭘로 바꾸든 상관없다.



supplier1 이라는 path는 Supplier에 구현되어있지않으므로 200 응답이 올수없다. 그리하여 consumer 메서드는 실패했고 그로인해 fallback 메서드가 실행된걸 볼 수 있다.


3. Hystrix config

@HystrixCommand(fallbackMethod = "fallback", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "500"),
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "10000"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "10"),
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "5"),
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000")
}, threadPoolProperties = @HystrixProperty(name = "coreSize", value = "100"))


이번엔 자주 사용할만한 몇가지 설정을 알아보겠다. 처음에는 fallback 메서드만 설정했지만 몇가지 설정을 추가로했다.


- execution.isolation.thread.timeoutInMilliseconds

Hystrix 가 적용된 메서드의 타임아웃을 지정한다. 이 타임아웃 내에 메서드가 완료되지못하면 서킷브레이커가 닫혀있다고 하더라도 fallback 메서드가 호출된다. 보통 외부 API 를 호출하게되면 RestTemplate 과 같은 http client에도 connect, read timeout 등을 지정하게하는데 hystrix timeout은 이를 포함하고 여유를 좀 더 두어 잡는다. 기본값은 1초(1000)


- metrics.rollingStats.timeInMilliseconds

서킷 브레이커가 열리기위한 조건을 체크할 시간이다. 아래에서 살펴볼 몇가지 조건들과 함께 조건을 정의하게되는데 "10초간 50% 실패하면 서킷 브레이커 발동" 이라는 조건이 정의되어있다면 여기서 10초를 맡는다. 기본값은 10초(10000)


- circuitBreaker.errorThresholdPercentage

서킷 브레이커가 발동할 에러 퍼센트를 지정한다. 기본값은 50


- circuitBreaker.requestVolumeThreshold

서킷 브레이커가 열리기 위한 최소 요청조건이다. 즉 이 값이 20으로 설정되어있다면 10초간 19개의 요청이 들어와서 19개가 전부 실패하더라도 서킷 브레이커는 열리지않는다. 기본값은 20


- circuitBreaker.sleepWindowInMilliseconds

서킷 브레이커가 열렸을때 얼마나 지속될지를 설정한다. 기본값은 5초(5000)


- coreSize

위에서 별도의 설명은 안했는데 Hystrix 작동방식은 Thread를 이용하는 Thread 방식과 Semaphore 방식이 있다. Thread 를 이용할 경우 core size를 지정하는 속성이다. 넷플릭스에서는 공식 가이드에 왠만하면 Thread 방식을 권장하고있다.(디폴트 설정도 Thread 방식이다.) 기본 coreSize 는 10


위 설정들을 참고하여 적절한 설정을 해주고 일부러 consumer로부터 실패할 요청만 보내보자. 계속 fallback을 반환하면서 path에 대한 log가 출력되다가 서킷 브레이커가 열리면 consumer를 실행하면 바로 fallback을 호출하면서 내부 코드가 실행되지않기때문에 fallback이 반환되지만 log는 찍히지않는것을 확인할 수 있다.


설정이 매우 다양하니 자세한건 https://github.com/Netflix/Hystrix/wiki/configuration 에서 살펴보자.


4. Monitoring

Hystrix를 적용하고나면 내가 설정한 내용들이 잘 적용은 됐는지, 지금 서킷 브레이커가 열려야되는데 잘 열린게 맞는지 확인하고 싶을 것이다. 위에서 로그를 이용해서 서킷 브레이커가 열렸는지를 확인하는 법을 간략히 적어놓긴했지만 넷플릭스는 Hystrix를 모니터링하는 방법도 제공하고있다. 이를 확인 하기위해서는 또 하나의 애플리케이션이 필요하다.


<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>


새로 만드는 애플리케이션에는 Hystrix Dashboard 의존성을 추가한다.


@EnableHystrixDashboard
@SpringBootApplication
public class MonitorApplication {

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

}


그리고 위처럼 @EnableHystrixDashboard 애노테이션만 붙여주면 끝이다.


server.port=9010


Supplier, Consumer 와 충돌하지않도록 포트도 지정해준다.


대시보드 애플리케이션을 구동해서 http://localhost:9010/hystrix 로 접속해보면 모니터링 화면이 우릴 맞이한다.



이 화면은 Hystrix 애플리케이션을 모니터링하는 화면이다. 즉 여기서 어떤 Hystrix 애플리케이션을 모니터링할지 입력해줘야한다. 이미 구동중인 Consumer 에 대한 정보를 넣어줘야하는데 Consumer에 대한 내용을 보려면 Consumer 애플리케이션에 추가적인 설정이 필요하다. 먼저 Spring Actuator 의존성이 필요하다.


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>


management.endpoints.web.exposure.include=*


헷갈리지말자. 지금 이 설정은 Dashboard가 아니라 Consumer에 해야한다. actuator 의존성을 추가하고, 모든 endpoints를 열어줬다. consumer 애플리케이션을 재구동한 후 dashboard에 http://localhost:9000/actuator/hystrix.stream 을 넣어주자.


그럼 이런 화면이 뜨게된다. 아직 Hystrix 메서드가 한번도 실행되지않아 대기상태다. consumer를 호출해보자.



5번 호출한 뒤 캡쳐화면이다. stream으로 지속적으로 서버에 대한 응답을 받고있어서 실시간으로 그래프가 변화하는걸 볼 수 있다.



이번엔 실패하는 요청만보내 서킷 브레이커를 열도록 해봤다. 빨갛게 열린걸 볼 수 있다.


5. Clustering

지금이야 로컬에서 간단하게 테스트를 하고있지만 실제 기업에서 서비스하는 환경이라면 인스턴스를 여러대 두고 l4 같은것으로 서비스를 하고있을것이다. 위 대시보드는 이미 그것으로 훌륭하지만 만약 인스턴스가 여러대라면 여러대를 각각 확인해야한다. 이미 회사 서비스에 적용한 내 생각으로는 지금까지의 진도만 와도 충분하다고 생각한다. Supplier 서비스에 전면장애라면 이미 1대만 확인해도 어떤 상황인지 감을 잡을 수 있을것이고, 모든 인스턴스를 확인할 필요까지는 없다고 생각하기때문이다.(이는 나와 다른 생각을 가질수도있다.) 이건 말 그대로 모니터링이고, 이걸 본다고 해당 인스턴스에 어떠한 작업을 할 수 있는건 아니라서 '현재 상태를 가늠하는 정도'의 모니터링은 1대만 되도 충분하다고 본다.


이 아래로는 내가 획득할 수 있는 정보에 비해 수고스러움의 가성비가 매우 떨어진다고 보기때문에 단순히 Hystrix 모니터링만을 위해서 할필요는 없다고 생각한다. 다만 기존에 유레카를 비롯한 서비스를 구성하고있다면 해봄직하다. 나는 회사에서 유레카 환경이 구성되어있지는 않지만 호기심에 가성비 떨어지는 수고스러움을 진행하고 적용을 하긴했다. 그리고 그 수고스러움을 포스팅하고있지만..


일단 클러스터로 모니터링하기위해서는 2개의 애플리케이션이 필요한데 인스턴스들을 집계할 Eureka 애플리케이션과 그 Eureka 클라이언트들의 Hystrix 모니터링을 집계할 Turbine 애플리케이션이다. 유레카 애플리케이션부터 설정을 시작해보자.


@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {

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

}
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
server.port=9020
spring.application.name=eureka
eureka.client.service-url.defaultZone=http://127.0.0.1:9020/eureka
eureka.client.fetch-registry: false
eureka.client.register-with-eureka: false


지금까지 했던것과 크게 다르지않다. 의존성만 추가해주고, eureka 설정들을 해준다. 그리고 서버를 띄운다.

설정할때 하나 주의할점은 defaultZone 이부분은 설정이 아니고 map의 key로 사용되는 부분이다. 그래서 다른곳이랑 똑같이 -으로 설정하면 안되고 저부분은 map의 key 이므로 꼭 카멜케이스로 적어줘야한다. 괜히 통일하겠다고 저부분을 -으로 연결하면 뜻하지않은 삽질을 하게될 수 있다.


혹시 java.lang.TypeNotPresentException: Type javax.xml.bind.JAXBContext not present 이런 에러가 뜨면서 유레카 서버가 구동되지못한다면 아래 의존성들을 추가해주자. 왜 안되는지는.. 뭐 잘 모르겠다. 일단 지금 포스팅의 핵심은 이게 아니니 구동만 되게 만든다.


<dependency>
<groupId>javax.xml.bind</groupId>