개발은 재밌어야 한다
article thumbnail
반응형

MSA 환경에서의 장애 양상

MSA에서 더 위험한(빈번한) 실패 시나리오는 서버가 완전히 뻗어버리는게 아니라,버벅거리지만 동작하는 서버가 더 위험할 수 있다. Feign Client와 같이 호출하는 쪽에서는 다른 서비스로 호출을 동기식으로 하기 때문에 응답에 오랜 시간이 걸리더라도 호출을 중단하지 않기 때문입니다.
 

https://dev.gmarket.com/40

위 처럼 A,B,C의 서비스가 MSA를 구성하고 있다고 했을때. 
서비스 C는 DB를 읽어오는 역할을 하고 있습니다. 지속적으로 DB에서 응답 지연이 발생하고 있는 상황에서 서비스 B가 지속적으로 C를 호출하게 된다면 C에서는 DB에 붙는 커넥션 풀이 고갈되고, B는 C를 반복 호출하면서 C의 스레드 풀을 모두 소진하게 된다.
서버 A 역시 B의 스레드 풀이 소진되면 장애가 발생할 가능성이 높습니다. 응답이 조금 지연될 뿐 서비스 A, B, C 모두 정상 동작하고 있기에 서버 측에서는 장애 직전까지 문제 상황을 감지하기가 쉽지 않습니다. 고객 입장에서는 서비스 페이지 로딩이 조금씩 느려지다가 어느 순간 에러 페이지가 뜨는 최악의 고객 경험을 하게 됩니다.
 

클라이언트 회복성 패턴

우리는 이에 대응하기 위해 회복성 패턴이라는 안전장치를 두어 치명적인 시스템 실패를 피할 수 있습니다. 회복성 패턴은 우리 일상에서도 친숙한 아래의 개념들로 이루어져 있습니다.
 

  • Circuit Breaker(회로 차단기) => 호출차단
두꺼비집

 
 응답이 지연되는 원격 서비스를 차단하여 반복 호출하지 못하도록 합니다. 가정집에 있는 두꺼비집을 서비스에 설치한다고 생각하시면 되겠습니다. 두꺼비집이 과전류 발생 시 전류를 차단하듯이 특정 원격 서비스에 대한 호출이 정해진 횟수 이상으로 실패한 경우 해당 자원에 대한 호출을 더 이상 반복하지 않습니다.
 
 

  • Fallback(폴백) => 예외 처리

 서비스를 차단한 경우 예외를 발생시키는 대신 대비책을 제공하거나 미리 준비된 동작을 실행합니다. 기본값을 반환하거나, 장애가 복구된 후 다시 처리할 수 있도록 재시도 큐에 보관할 수도 있습니다. 예를 들어, 개인화 추천 서비스에 문제가 생겨 회로를 차단했다면 프로모션 상품이나 인기 상품을 대신 응답하도록 하여 매끄럽게 서비스를 운영할 수 있습니다.
 

  • Bulkhead(벌크헤드) => 자원격리
https://dev.gmarket.com/40
마치 코로나때 식당에 칸막이를 두어서 감염을 방지하는것 처럼 서비스의 스레드풀을 격리합니다.

마치 칸막이 처럼, 응답시 지연되는 서비스에 자원을 모두 소진하지 않도록 스레드 풀을 격리합니다. 
위의 예시처럼 하나의 서버에서 가용한 스레드풀을 응답이 지연되는 원격 호출에 모두 소진하면 다른 문제없는 기능까지 마비될 수 있습니다. 벌크헤드 패턴은 선박 건조 시 선체 일부가 파손되거나 화재가 발생하여도 다른 부분에 영향이 없도록 격벽을 두는 것과 같습니다. 이러한 회복성 패턴의 아이디어는 원격 서버의 지연이 시스템 전체로 전파되지 않도록 빠르게 실패로 간주하고(빠른 실패), 지연되는 서비스에 소모되는 자원을 격리(실패의 격리)함에 있습니다. 회복성 패턴이 적용된 경우 어떤 식으로 장애가 조치되는지 살펴보겠습니다.
 
 

회복성 패턴 시나리오(https://dev.gmarket.com/40)

서비스 C를 호출하는 서비스 B에 회로 차단기가 적용되었습니다. 이런 상황에서 서비스 C에서 응답 지연 현상이 발생하면 어떻게 될까요? 마냥 응답을 기다리며 반복 호출을 하던 앞서의 상황과 다르게 이제 기설정한 timeout 기준을 넘기는 횟수가 일정 수준을 넘으면 서비스 C를 빠르게 장애로 판단(fail fast, 빠른 실패)하여 회로를 open(차단)합니다.
 
차단된 동안 서비스 B는 서비스 C로부터 필요한 데이터를 가져올 수 없기에 서비스 A로 fallback 동작을 수행(fail gracefully, 원만한 실패)합니다.
 
서비스 C는 응답지연이 지속되는 상황에서 더 이상 호출되지 않아 복구할 수 있는 여유가 생깁니다. 많은 경우 이렇게 추가 호출 없이 시간적인 여유를 주는 것만으로 문제가 자연스럽게 해결될 수도 있습니다
 
 
서비스 B의 회로 차단기는 일정 시간이 지나면 서비스 C의 정상화 여부를 간헐적으로 확인합니다. 그리고 DB 이슈가 해결되어 서비스 C가 정상화되었다면 사람의 개입없이 자동(recover seamlessly, 원활한 회복)으로 회로를 close(차단 해제)하여 호출을 허용합니다.

1. 엄청나게 호출을 하다가 (닫힘)
2. 호출을 멈춰 버리고 (오픈)

 

3.천천히 확인해본다 (반오픈)
4.그러다가 정상인거 확인되면 다시 원래대로 (닫힘)

 

https://velog.io/@hgs-study/CircuitBreaker

 
 

Netflix Hystrix

 지금까지 클라이언트 회복성 패턴을 대하여 이야기했습니다만, 가장 중요한 문제가 있습니다. 이 완벽해 보이는 해결책의 가장 큰 문제는 구현이 매우 어렵다는 점입니다. 스레드를 직접 조작하는 개발은 기술이 아닌 예술의 영역이라는 말도 있습니다. 하지만, 언제나 그랬듯 우리는 남이 만들어둔걸 잘 가져다가 쓰면 됩니다😏 Netflix의 API 팀에서 개발하여 2011년부터 자사 프로덕트에 적용해온 Hystrix는 위에서 이야기한 회복성 패턴을 다년간 검증받은 falut tolerance 라이브러리입니다. Hystrix는 고슴도치의 한 종류인데, 가시로 스스로를 보호하듯 서비스를 장애로부터 보호하겠다는 의미로 이름을 붙인 듯합니다.

고슴도치를 본 딴 Hystrix 로고

 
Hystrix사용
 Spring 생태계의 여러 library들처럼, Hystrix도 의존성 추가 후 어노테이션 기반으로 간단히 시작할 수 있습니다. 메이븐 dependency 추가 후, application 클래스에 @EnableCircuitBreaker 어노테이션을 추가하면 우리의 애플리케이션에 회로 차단기를 적용할 준비가 끝났습니다. 

<!-- netflix-hystrix maven dependency 추가 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
@SpringBootApplication
@EnableCircuitBreaker // 회로 차단기 사용
public class RestConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(RestConsumerApplication.class, args);
    }
}

 
1. 어노테이션 사용
원격 서비스에 의존하는 메서드에 @HystrixCommand 어노테이션을 추가하여 회로 차단기 패턴을 적용할 수 있습니다. 간단히@HystrixCommand 어노테이션만 추가하여, Hystrix는 해당 메서드를 wrapping 하는 proxy를 생성합니다. 그렇게 함으로써, 원격 호출을 처리하는 스레드 풀을 확보하여, 해당 메서드의 모든 호출을 관리할 수 있게 됩니다.

@HystrixCommand
public void callOtherServer() {
    // 다른 서버 호출
}

 
2.어노테이션을 추가하는 대신, HystrixCommand를 상속하여 run method를 오버라이드함으로써 회로 차단기 패턴을 구현할 수 있습니다.

public class CallOtherServer extends HystrixCommand<String> {
    @Override
    protected String run() throws Exception {
    	String result = callOtherServer();
        return result;
    }
}

 
회로 차단기 구현
회로 차단기는 4가지 parameter를 이용한 통계 값을 내어 차단 여부를 결정합니다.
- 일정 시간(metrics.rollingStats.timeInMilliseconds) 동안
- 일정 개수(requestVolumeThreshold) 이상 호출을 했는데,
- 일정 비율(errorThresholdPercentage) 이상 에러가 발생하면,
- 일정 시간(sleepWindowInMilliseconds) 동안 회로 차단기를 open(차단)합니다.
 

@HystrixCommand(
    commandProperties={
        @HystrixProperty(name="metrics.rollingStats.timeInMilliseconds", value="10000"),
        @HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="20"),
        @HystrixProperty(name="circuitBreaker.errorThresholdPercentage", value="50"),
        @HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds", value="5000")
    }
)
public void callOtherServer() {
    // Some works
}

 @HystrixCommand 어노테이션에서 앞서 이야기한 parameter들을 입력할 수 있습니다. 위 예시의 callOtherService 메서드는 10,000ms(10초) 동안 20번 이상의 호출이 있는 경우, 호출의 50% 이상에서 에러가 발생하면 5,000ms(5초) 동안 해당 메서드 호출을 차단합니다. 5초가 지나면 바로 차단을 해제하는 것이 아니고, 요청 1개만 먼저 보내본 후 이것이 성공하면 그때 차단을 해제합니다. 요청이 실패한 경우 다시 5초간 회로를 차단합니다.
 

@HystrixCommand(commandKey = "externalServer1")
public void callSameServer() {
    // 동일 원격 서버 호출
}

@HystrixCommand(commandKey = "externalServer1")
public void callSameServer2() {
    // 동일 원격 서버 호출
}

 우리는 MSA 환경에서 다른 원격 서비스 호출 시 발생할 수 있는 여러 예외 상황을 컨트롤하기 위해 Hystrix를 이용합니다. 그렇다면 특정 원격 서비스를 호출하는 메서드들을 하나의 회로 차단기로 구성할 필요가 있습니다. 즉, 동일한 dependency를 갖는 메서드들은 함께 통계를 내어 회로 차단 여부를 결정하는 것이 합리적으로 보입니다. 이런 경우 여러 메서드에 동일한 commandKey를 설정하면 하나의 회로 차단기로 동작합니다. commandKey를 설정하지 않으면, default로 메서드명이 key로 쓰입니다. 즉, 메서드 단위로 회로 차단기가 설정됩니다.
 

@HystrixCommand(commandProperties = {
    @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds", value="10000")
})
public void callSameServer() {
    // 10초 후 timeout
}

 지연되는 응답을 마냥 기다리지 않고, 빠른 실패로 처리하는 것이 회로 차단기의 주요한 임무입니다. Hystrix의 회로 차단기는 기본값으로 1초를 timeout 기준으로 사용합니다. 위의 예시처럼 execution.isolation.thread.timeoutInMilliseconds를 직접 입력하여 timeout 설정값을 변경할 수 있습니다. 메서드가 지연되어 기준 시간이 지나면 HystrixRuntimeException를 발생시키고 (만일 있다면) fallback이 수행됩니다.
 <스프링 마이크로서비스 코딩 공작소>의 저자 존 카넬은 되도록이면 Hystrix의 default timeout 1초를 임의로 수정하지 않기를 권장합니다. Best practice는 서비스/메서드 별 응답 시간을 상세히 파악한 후, 유독 응답이 지연되는 원격 서비스 호출이나 메서드들을 추려내어 별도의 스레드 풀로 격리하는 것입니다.
 

  • Fallback (예외처리) 구현

 Hystrix는 아래 네 가지 경우에 fallback으로 지정된 메서드를 실행합니다.

  1. 회로 차단기가 열린 경우
  2. HystrixBadRequestException을 제외한 모든 Exception
  3. Semaphore/ThreadPoolReject
  4. Timeout
@HystrixCommand(
    fallbackMethod = "myFallBackMethod",
    commandProperties = {
        @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds", value="1000")
})
public void callSameServer() throws InterruptedException {
    // delay 2 seconds
    Thread.sleep(2000);
}

public void myFallBackMethod() {
    System.out.println("Fallback executed");
}

 위의 코드는 timeout으로 인해 fallback이 실행되는 예시입니다. 메서드 내에서 2초간 sleep 하여, 설정된 timeout 1초를 넘기면 HystrixRuntimeException이 발생하고, fallbackMethod로 설정된 myFallBackMethod가 실행됩니다.
 
 

  • Bulkhead(자원격리) 구현 

 회로 차단기 별로 격리된 스레드 풀을 지정할 수 있습니다. Hystrix는 스레드 격리 방법으로 THREAD, SEMAPHORE 두 가지를 제공합니다.

THREAD 방식의 격리 전략
 THREAD. Hystrix의 기본 격리 방식으로서, caller로부터의 호출 스레드를 intercept 하여 Hystrix가 관리하는 스레드로 대신 호출합니다. 회로 차단기 별로 threadPoolKey를 이용하여 자신이 사용할 스레드 풀을 지정합니다. 위의 그림처럼 회로 차단기 여러 개가 같은 스레드 풀을 공유할 수 있습니다.

@HystrixCommand(
    fallbackMethod = "myFallbackMethod",
    threadPoolKey  = "myThreadPool",
    threadPoolProperties = {
        @HystrixProperty(name="coreSize", value="20"),
        @HystrixProperty(name="maxQueueSize", value="10"),
    })
public void callSameServer() throws InterruptedException {
    // Some works
}

 coreSize는 스레드 풀의 스레드 개수를 의미합니다. maxQueueSize는 스레드가 모두 점유 중일 때 들어온 요청이 대기하는 큐의 크기를 의미합니다. 큐를 사용하고 싶지 않으면 maxQueueSize를 -1로 설정합니다. 스레드 개수는 어떻게 정하면 될까요? Netflix에서는 아래와 같은 공식을 제안합니다.
 

Peak시의 RPS * Latency의 99% quantile(in sec) + 오버헤드를 대비한 여유분 = 스레드 개수

 
 예를 들어, 가장 사용량이 많을 때 초당 30개의 요청이 들어오고, 응답 시간을 오름차순으로 세워놓았을 때 100개 중 99번째 값이 0.2초라고 한다면, 30 RPS * 0.2 sec + 오버헤드를 대비한 여유분 = 10개 정도의 스레드가 적절한 개수라고 볼 수 있습니다.
 
SEMAPHORE. 세마포르는 원래 수기신호라는 뜻으로서, 선박이 정박하는 등의 상황에서 깃발로 신호를 보내는 것을 의미합니다. 앞서 이야기한 THREAD 방식에서는 요청 스레드를 intercept 하여 Hystrix가 관리하는 스레드로 요청을 처리했다면, SEMAPHORE 방식에서는 요청 스레드가 실제로 처리까지 담당합니다. 회로 차단기가 깃발을 하나씩 들고 항구에 서서 선박(요청)을 허용 가능한 수만큼 스레드 풀로 통과시키는 모습을 상상하면 되겠습니다.

복수의 회로 차단기가 하나의 스레드 풀을 공유할 수 있었던 THREAD 방식과 달리 SEMAPHORE방식에서는 회로 차단기 하나가 스레드 풀 하나씩 가지고 있습니다.
 

SEMAPHORE 방식의 격리 전략(https://dev.gmarket.com/40)

 단, Netflix의 Hystrix 공식문서에서는 SEMAPHORE 방식을 지양하고 기본 전략인 THREAD 방식을 사용하길 권고하고 있습니다.
Netflix에서도 아주 일부만 SEMAPHORE 방식을 사용 (https://github.com/Netflix/Hystrix/wiki/Configuration#thread-or-semaphore)
 

아쉬운 은퇴 및 대안

 아쉽게도 fault tolerance 라이브러리의 대명사격이었던 Hystrix는 현재 개발이 중단되었습니다. 아직까지 프로젝트에 적용은 가능하나 되도록이면 신규 프로젝트에는 resilience4j(application level)나 Istio(infra level)를 이용하길 권장드립니다. 특히 resilience4j는 Spring Boot 프로젝트에서 앞서 소개한 Hystrix의 circuit breaker, fallback, bulkhead(Semaphore, Thread) 등의 패턴들을 모두 대체할 수 있습니다. Application 레벨에서 회복성 패턴을 적용하고자 할 때 Hystrix의 훌륭한 대안으로 꼽을 수 있습니다.
 
 
그래서 신규 프로젝트들은 아래와 같은 이유로 대부분 Resilience4j 를 선택(https://techblog.woowahan.com/2667/)

  • Spring Boot 공식 회로차단기가 Hystrix 에서 Resilience4j 로 변경
  • Netflix 라이브러리가 2018년 12월 18일부터 Maintenance 모드에 돌입
  • Resilience4j 는 Functional Programming의 원칙을 기반

 

Resilience4J란? 

Resilience4j는 Netflix Hystirx에서 영감을 받아 만든 경량의 장애를 견디게 해주는 라이브러리입니다. 그러나 Hystrix가 Java 6을 기반으로 했다면 Resilience4j는 Java 8을 기반으로 하고 있으며 functional 기법을 기반으로 하고 있다는 차이가 있습니다.

 
Resilience4j에서는 기본적인 아래와 같은 특징이 있다.
- Circuit Breaker: Count(요청건수 기준) 또는 Time(집계시간 기준)으로 Circuit Breaker제공
- Bulkhead: 각 요청을 격리함으로써, 장애가 다른 서비스에 영향을 미치지 않게 함(bulkhead-격벽이라는 뜻)
- RateLimiter: 요청의 양을 조절하여 안정적인 서비스를 제공. 즉, 유량제어 기능임.
- Retry: 요청이 실패하였을 때, 재시도하는 기능 제공
- TimeLimiter: 응답시간이 지정된 시간을 초과하면 Timeout을 발생시켜줌
- Cache: 응답 결과를 캐싱하는 기능 제공

 
 

CircuitBreaker

 
간단예제
 
서비스 A와 서비스B로 예를 든 resilience4j 

서비스 B를 호출할 서비스 A의 controller
RestTemplate을 빈으로 등록
서비스 A가 호출할 서비스B의 Controller
서비스 A의 application.yml

 
1. 최초 상태 (서킷브레이커 닫힌상태)

2. B서비스가 꺼진상태에서 5번 호출 => 실패 (서킷브레이커 오픈)

3. 오픈상태에서 5초후 (반오픈 상태)

4. 반오픈상태에서 3회호출 (오픈상태)

5. 다시 5초후 (반오픈상태)

6. 반오픈상태에서 서비스 B 가동 => 3번이 정상이 될때까지 계속 반오픈상태
 
1회호출

A호출시

2회호출

3회 호출 -> 서킷브레이커 닫힘

Retry

A서비스 application.yml

A서비스의 application.yml 을 보면 retry 옵션에서 재시도 횟수와 재시도를 하는 간격을 설정할 수 있다 (예시는 최대5회, 3초간격)
 
서비스 B를 중지하고 서비스 A를 호출하면 

3초간격으로 5회 재시도 (따로 fallback을 지정하지 않아서 에러로그 발생)

 
참고 
resilience4j 를 사용하기 위해서는 spring aop를 추가해야한다

에러로그를 보자

에러로그에 Advice와 Cglib로 생성한 프록시 클래스 객체가 있는걸로 봐서 스프링 aop가 사용되는걸 알 수 있다.
 
resilience4j 에 보면 RetryAspect에서 로 AOP를 통해서 횟수를 통해 재시도를 처리하는 걸 알 수 있다

 
 
 
 
 
 
 
 
 
 
 

참고
 
https://dev.gmarket.com/40

Netflix Hystrix를 이용한 MSA 회복성 패턴 톺아보기

MSA 환경에서의 장애 양상 우리는 장애를 피할 수 없습니다. 아무리 실력이 좋은 소프트웨어 개발자라 할지라도 완전무결한 시스템을 만들 수 없습니다. 물론 우리도 여러 방법으로 장애에 대응

dev.gmarket.com

https://techblog.woowahan.com/2542/

Hystrix! API Gateway를 도와줘! | 우아한형제들 기술블로그

{{item.name}} 들어가기 안녕하세요. 배민FRESH 서비스를 개발하고 있는 조건희입니다. 오늘은 API Gateway를 사용하면서 겪은 뼈아픈(?) 장애 사례와 해결 과정을 간단히 공유하고자 합니다. 너무 자세

techblog.woowahan.com

https://techblog.woowahan.com/2667/

배달의민족 최전방 시스템! ‘가게노출 시스템’을 소개합니다. | 우아한형제들 기술블로그

{{item.name}} 안녕하세요 우아한형제들 프론트검색서비스팀 권용근입니다. 저는 "먼데이 프로젝트" 라는 2019년 대형 프로젝트에서 요란하게 탄생하였고, 탄생한 순간부터 지금까지 배달의민족 최

techblog.woowahan.com

https://velog.io/@hgs-study/CircuitBreaker

CircuitBreaker를 이용한 외부 API 장애 관리

CircuitBreaker는 서비스메시의 쿠버네티스 Istio를 이용해서 인프라 레벨에서 적용가능하나, 이번 포스팅에선 Resilience4j를 이용한 어플리케이션 레벨에서 적용하겠습니다. 1. CircuitBreaker가 필요한 이

velog.io

https://sabarada.tistory.com/205

[MSA] spring boot에서 resilience4j 사용해보자 - Retry, CircuitBreaker 편

안녕하세요. 이번 포스팅에서는 resilience4j를 실제로 spring boot 환경에서 사용해보는 방법에 대해서 알아보는 시간을 가져보도록 하겠습니다. 환경 먼저 오늘 실습에 사용된 환경은 아래와 같습니

sabarada.tistory.com

https://www.youtube.com/watch?v=9AXAUlp3DBw 

반응형
profile

개발은 재밌어야 한다

@ghyeong

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!