(Spring Cloud) Hystrix

Hystrix란

Netflix에서 Circuit Breaker Pattern을 구현한 라이브러리이다. Micro Service Architecture에서 장애 전파 방지를 할 수 있다.

Circuit Breaker Pattern

Inline-image-2018-02-28 17.38.25.834.png
출처 : https://martinfowler.com/bliki/images/circuitBreaker

장애 연쇄

위 그림에서 supplier 서버에 장애가 생겨 항상 Timeout이 발생하는 경우, supplier 서버를 호출한 client 서버는 Timeout이 발생할 때까지 응답이 밀리게 된다. 응답이 밀리는 동안 요청이 계속 쌓여 결국 client 서버까지 요청이 과하게 밀려 장애가 발생할 수 있다.

이러한 상황이 발생하지 않도록 circuit breaker를 두어 장애 전파를 막을 수 있다.

image2
출처 : https://cloud.spring.io/spring-cloud-netflix/multi/multi__circuit_breaker_hystrix_clients.html

Hystrix Flow Chart

Inline-image-2018-03-05 10.55.00.102.png
출처 : https://github.com/Netflix/Hystrix/wiki/How-it-Works
  1. HystrixCommand, HystrixObservableCommand 객체 생성
  2. Command 실행
  3. 캐시 여부 확인
  4. 회로 상태 확인
  5. 사용가능한 Thread Pool/Queue/Semaphore가 있는지 확인
  6. HystrixObservableCommand.construc(), 혹은 HystrixCommand.run() 실행
  7. 회로 상태 연산(Calculate circuit health)
  8. fallback 실행
  9. 응답 반환

Hystrix Circuit Breaker 구현

Inline-image-2018-03-05 11.04.14.221.png
출처 : https://github.com/Netflix/Hystrix/wiki/How-it-Works
  1. circuit health check를 위한 최소한의 요청이 있을 때(HystrixCommandProperties.circuitBreakerRequestVolumeThreshold())
  2. 그리고, 지정한 오류율을 초과했을 때(HystrixCommandProperties.circuitBreakerErrorThresholdPercentage())
  3. 회로의 상태를 CLOSED에서 OPEN으로 변경
  4. 회로가 열린 동안, 모든 요청에 대해서 fallback method을 바로 실행
  5. 일정 시간이 지난 후(HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()), 하나의 요청을 원래 method로 실행(HALF OPEN). 이 요청이 실패한다면 OPEN으로 두고, 이 요청이 성공한다면 CLOSED로 상태를 변경. 다시 1번으로 돌아감.

기본 설정

  • metrics.rollingStats.timeInMilliseconds : 오류 감시 시간, 기본값 10초
  • circuitBreaker.requestVolumeThreshold : 감시 시간 내 요청 수, 기본값 20
  • circuitBreaker.errorThresholdPercentage : 요청 대비 오류율, 기본값 50

기본 설정을 풀어서 설명하면 다음과 같다

감시시간 내(30초)에, 20번 이상의 요청이 있었고, 그 중에서 오류율이 50% 이상일 때 Circuit Breaker가 작동한다(circuit open)
감시 시간 내에 요청이 반드시 20번 이상이 있어야 회로가 열림. 30초 동안 요청이 19번이었고 모두 실패했어도 Circuit Breaker는 작동하지 않는다

예제

dependencies

1
2
3
4
5
6
7
8
9
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}

dependencies {
compile('org.springframework.cloud:spring-cloud-starter-netflix-hystrix')
}

사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@SpringBootApplication
@EnableCircuitBreaker
public class Application {

public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
}

}

@Component
public class StoreIntegration {

@HystrixCommand(commandKey="getStores", fallbackMethod = "defaultStores")
public Object getStores(Map<String, Object> parameters) {
//do stuff that might fail
}

public Object defaultStores(Map<String, Object> parameters) {
return /* something useful */;
}
}
  • StoreIntegration.getStores()가 실패하거나 회로가 열렸을 시에는 .defaultStores가 실행된다.

설정

application.yml로 설정예시

1
2
3
4
5
6
7
8
hystrix:
command:
# 전역설정
default:
execution.isolation.thread.timeoutInMilliseconds: 10000
# 특정 commandKey에 대한 설정
getStores:
execution.isolation.thread.timeoutInMilliseconds: 10000

java 설정예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class UserResource {
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "500")
},
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "30"),
@HystrixProperty(name = "maxQueueSize", value = "101"),
@HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "15"),
@HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "12"),
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "1440")})
public User getUserById(String id) {
return userResource.getUserById(id);
}
}

설정 참고:
https://github.com/Netflix/Hystrix/wiki/Configuration#execution.isolation.strategy

실사용례

Micro Service Architecture에서 하나의 게시판을 보여주는 예제(게시글, 댓글, 추천 게시글 API를 호출)
예제는 spring 4.3.x + reactor로 작성했다.

서비스

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
@Slf4j
@Component
public class BoardService {
@Autowired private BoardClient boardClient;
@Autowired private CommentClient commentClient;
@Autowired private RecommendationClient recoClient;
@Value("service.board.detail.timeout") private Duration detailTimeout;

public BoardDetailDto getDetail(long id) {

// board server에서 게시물 단건 호출, 실패시 실패 응답 반환
Board board = boardClient.getOne(id);
if (board == Board.ERROR) {
return BoardDetailDto.ERROR;
}

// board를 기반으로 댓글과 추천 게시물을 비동기 동시 호출
return Mono.zip(Mono.just(board), commentClient.getCommentsByBoardId(id), recoClient.getRecommendationsByBoardId(id))
.map(TupleUtils.function(BoardDetailDto::new))
.timeout(detailTimeout)
.doOnError(e.getMessage(), e)
.onErrorReturn(BoardDetailDto.create(board)) // 실패시 board 만이라도 DTO로 만들어 응답
.blockOptional()
.orElse(BoardDetailDto.ERROR);
}
}

CommentClient.java

1
2
3
4
5
6
7
8
9
10
11
public class CommentClient {

@HystrixCommand(fallbackMethod = "getCommentsByBoardIdFallback")
public Mono<List<Comment>> getCommentsByBoardId(long id) {
//...
}

public Mono<List<Comment>> getCommentsByBoardIdFallback(long id) {
return Mono.just(Collections.emptyList());
}
}
  • 여러 서비스를 호출하면서도, 하위 서비스의 장애가 client로 노출되지 않고, 성공한 서비스 응답들은 모두 client로 줄 수 있음

Isolation

bulkhead pattern를 채용하여 종속성(dependency)을 분리하며, 각각에 대한 접근을 제한했다.

Inline-image-2018-03-05 11.45.23.425.png
출처 : https://github.com/Netflix/Hystrix/wiki/How-it-Works

Threads & ThreadPool

호출 thread와 별도의 thread(ex. Tomcat thread pool)에서 동작한다.

Inline-image-2018-03-05 11.48.29.958.png
출처 : https://github.com/Netflix/Hystrix/wiki/How-it-Works

ThreadPool을 사용하지 않아도 되는 경우

  1. 네트워크 connection/read timeout, retry 옵션을 사용하여 매우 빨리 실패하거나
  2. client가 항상 정상동작한다는 신뢰가 있는 경우
    즉, 그냥 ThreadPool을 사용하자

Netflix에서 각각의 Thread pool을 사용하여 의존성 격리를 구성한 이유

  • 결론부터 먼저 말하자면, Thread를 나누어 다른 Thread에 접근하기 어렵도록 종속성을 원천차단한다.
  • application은 수없이 많은 팀의, 수없이 많은 back-end service 를 수없이 많이 호출한다
  • 각 service는 client library를 가지고 있다
  • client library는 항상 바뀐다
  • client library는 새로운 네트워크를 호출할 수도 있고, retry, parsing, caching 등의 logic을 가지며 blackbox 취급된다
Inline-image-2018-03-05 11.59.03.923.png
출처 : https://github.com/Netflix/Hystrix/wiki/How-it-Works

Thread Pool 사용상 이점

  • application이 client library로부터 보호된다
  • 덕분에 새 client library를 추가할 때의 risk를 낮출 수 있다, 장애는 격리된 thread에서 발생한다

Thread Pool 사용상 단점

  • queueing, scheduling, context switching 등의 오버 헤드 발생가 발생된다(Netflix에서는 이를 사소한 정도로 간주)

Thread 비용

  • Hystrix는 자식 thread에서 construct(), run()을 실행할 때, 부모 thread에서 총 종단 시간을 측정하여 overhead를 계산한다.
  • Netflix에서는 10억 건 이상의 Hystrix Command를 실행하며, 각 API 인스턴스마다 5-20개의 thread를 가지고 있는 thread pool을 40+개를 설정한다.(대부분의 thread pool 내의 thread 개수는 10개)

ThreadLocal

기본적으로 @HytrixCommand는 다른 Thread로 동작을 하기 때문에, ThreadLocal이나 spring에서 지원해주는 @RequestScope, @SessionScope 빈에 접근할 수 없다. 필요한 경우 execution.isolation.strategy: SEMAPHORE로 변경하여 현재 Thread에서 연산을 실행하게 할 수 있다.
Spring Security를 사용하는 경우, hystrix.shareSecurityContext=true로 설정해서 SecurityContext를 공유할 수 있다.

THREAD 동작 방식의 경우에는 Thread-pool내의 Thread 갯수 만큼, SEMAPHORE 동작 방식의 경우에는 semaphore count 만큼 요청을 수행할 수 있다
execution.isolation.semaphore.maxConcurrentRequests

Semaphore

Thread pool을 사용하는 대신, Semaphore(counter)를 사용하여 종속성에 대한 동시 호출 수를 제한할 수 있다. 따라서 Thread를 사용하지 않고 부하를 분한하지만, timeout과 격리가 느슨해지는 단점이 있다 위에서 ThreadPool을 사용하지 않아도 되는 경우에서 설명한 것과 같이 back-end server를 신뢰할 수 있다면 사용해도 괜찮다

HystrixCommandHystrixObservableCommand는 두 곳에서 semaphore를 지원한다

  • Execution: execution.isolation.strategy=SEMAPHORE로 설정이 되어 있으면, 해당 command를 실행할 수 있는 부모 스레스 수를 제한
  • Fallback 검색