ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 9. [MSA 구현 퀵스타트] 장애대응 Hystrix 초간단 구현
    Spring Cloud 2021. 7. 19. 01:49

    이번 포스팅에서는 장애 대응(장애 전파 방지 및 복구)에 중요한 라이브러리인 Hystrix를 구현해 보겠습니다.

    이론적인 부분(1~3장)과 구현할 전체 프로젝트 구조(4장)를 보지 읽지 못하신 분들은 먼저 읽어보시기를 권장 드립니다.

     

    지난 포스팅인 "3. MSA 구현을 위한 핵심 프레임워크를 알아보자 2"에서 Hystrix에 대한 개요를 알아보았는데, 더욱 자세히 정리를 해보겠습니다.

     

    Hystrix의 중요한 아래와 같은 용어가 있습니다.

    • Circuit Breaker
    • Fallback
    • Thread Isolation
    • Timeout

     

    1. Circuit Breaker & Timeout

    Hystrix가 관리하는 기능들은 요청 처리에 대한 통계가 내부적으로 집계됩니다.

    통계를 분석하여 특정 기능이 일정 시간동안 일정 횟수의 요청에 대해 일정 비율로 에러(Exception)가 발생할 경우 Circuit Open 합니다.

     

    Circuit Open되면 이후 일정 시간동안 이후 요청(기능)을 차단함으로써 지속적으로 발생하는 에러로 인한 자원 고갈 및 반복 장애를 차단하고 복구를 위한 시간을 벌수 있습니다. 이는 서비스 간의 연계에세 연쇄적인 서비스 장애를 방지할 수 있습니다.

     

    이후 한 건의 요청을 허용하며 해당 요청이 성공하면 Circuit Close를 하며, 실패할 경우 다시 일정 시간 Circuit Open을 유지 합니다.

     

    Timeout 처리는 특정시간 동안 해당 기능이 완료되지 않으면 예외를 발생하도록 합니다. 

    하지만 너무 긴 타임아웃을 가지는 것 또한 장애를 유발할 수 있는 요소입니다. 모든 스레드의 요청이 응답을 대기하게 되면 더이상 추가 요청을 받지 못하고 해당 요청은 대기큐에 쌓이며 밀리게 됩니다. 이에 기능별로 적절한 수치의 Timeout의 지정이 필요 할수 있습니다.

     

    좀더 이해하기 쉽게 상세한 예를 들어 보겠습니다.

     

    서비스1에서 DB로 부터 회원ID를 SELECT하는 기능이 있습니다. SELECT를 무한정 대기 할 수 없기에 해당 기능에 3초의 Timeout을 설정합니다. 즉 3초 동안 SELECT를 하지 못하면 에러를 발생합니다.

     

    또한  "10초간 20개 이상의 호출에서 75% 이상의 에러가 발생하면 7초간 Circuit Open 하라" 라고 설정 합니다.

     

    이렇게 하면 Circuit은 10초 단위로 20개 이상의 SELECT를 실행되었는지 집계된 통계를 통해 확인하며, 10초안에 만약 75% 이상의 에러(SELECT가 3초 이상 수행된 경우)가 했다면 Circuit을 Open합니다.

    Circuit이 Open되면 그 이후 7초간 요청이 와도 해당 SELECT를 수행하지 않고, 바로 에러를 반환합니다.

     

    이렇게 하는 이유는 50% 이상의 지속적인 SELECT 지연이 자원을 고갈 시켜 다른 기능 까지 연쇄적으로 장애를 일으키는 것을 방지하기 위해 장애를 조기 차단하는 것입니다. 또한 7초 동안 해당 SELECT 기능이 안정화 될 수 있는 시간을 벌 수가 있습니다.

     

    7초 이후 1개의 요청을 허용하며(Half Open), 이 호출이 성공하면 Circuit을 닫고 다시 서비스를 재개 합니다.

     

    위와 같은 설정은 @HystrixCommand 애너테이션을 이용해 아래와 같이 간단히 설정 할 수 있습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @HystrixCommand(
           /** 정의 : 15초 동안 10건의 호출 중 75% 에러(3초 이상 SELECT 지연)가 발생하면
            *              Circuit을 7초 동안 Open 하라 */
           commandProperties={
          // DB SELECT Timeout 설정 3초
          @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds", value="3000"),
           // 서비스 호출 문제를 모니터할 시간 간격을 설정 (위 정의에서 15초 동안에 해당)
           @HystrixProperty(name="metrics.rollingStats.timeInMilliseconds", value="15000"),
           // 히스트릭스가 호출 차단을 고려하는데 필요한 요청 수 (위 정의에서 10건에 해당)
           @HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="10"),
           // 호출 차단 실패 비율 (위 정의에서 75%에 해당)
           @HystrixProperty(name="circuitBreaker.errorThresholdPercentage", value="75"),
           // 차단 후 서비스의 회복 상태를 확인할 때까지 대기할 시간 (위 정의에서 7초에 해당)
           @HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds", value="7000")})
    public String getIdInfo(String id) {
       return dao.DB_SELECT_반환(id);
    }
    cs

    주석을 보면 쉽게 이해하실 수 있으실 겁니다.

     

     

    2. Fallback (폴백 패턴)

    Circuit이 Open되어 에러를 반환하는 것보다는 에러를 대비하여 미리 준비된 응답을 반환 할 수 있습니다.

    예를 들어 쇼핑몰에서 특정 고객에게 맞춤형 상품을 제공하는 기능이 있다고 합시다. 만약 이 기능이 문제가 발생하였을 때, 에러화면을 그대로 고객에게 노출하는 것보다 미리 준비해둔 상품의 정보를 제공할 수 있습니다.

     

    코드는 아래와 같이 설정할 수 있습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    @HystrixCommand(fallbackMethod = "buildFallbackIdInfo")
    public String getIdInfo(String id) {
      return hystrixTestDao.getIdInfo(id);
    }
     
    private String buildFallbackIdInfo(String id){
      return id + "'s Fallback Info";
    }
    cs

    타임아웃값을 지정하지 않으면 기본 1초 입니다. 즉 1초 동안 SELECT를 하지 못하였을 경우 buildFallbackIdInfo() 메소드가 호출되어 그 결과 값이 응답으로 반환 됩니다.

     

     

    3. Thread Isolation (벌크헤드 패턴)

    Hystrix로 감싼 기능은 Intercept되어 Hystrix가 생성한 스레드들이 대신 실행합니다.

    이때 각 기능이 사용할수 있는 자원을 격리(제한) 함으로서 1개 기능 장애가 전체 시스템에 영향을 미치는 것을 차단합니다.

    예를 들어 상품서비스의 전체 가용 자원이 100이라고 하였을때, 상품서비스의 A,B,C,D,E 기능들이 같이 자원을 공유하며 서비스가 운영됩니다.  갑자기 A기능에 부하가 심해지거나 심각한 지연이 발생하여 자원의 대다수의 점유해버립니다. 그러면 B,C,D,E는 얼마 남지 않은 자원으로 운영되다 보면 연쇄적인 장애로 이어질 수 있습니다.

    Thread Isolation을 하여 각각의 서비스에 자원을 30씩만 할당해 격리한다면 A 서비스가 30개의 자원을 모두 고갈하더라도 B,C,D,E는 영향을 받지 않게 됩니다.

     

    코드로 구현하면 아래와 같습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @HystrixCommand(
        threadPoolProperties =
                // 스레드 풀의 갯수를 정의
                {@HystrixProperty(name = "coreSize",value="30"),
                        // 스레드 풀 앞에 배치할 큐와 큐에 넣을 요청 수를 정의
                        //  => 스레드가 분주할 때 큐 이용
                        @HystrixProperty(name="maxQueueSize", value="10")})
    public String getIdInfo(String id) {
        return hystrixTestDao.getIdInfo(id);
    }
    cs

    getIdInfo()메서드를 수행하는 총 스레드 자원을 30개 할당 합니다. 만약 30개가 모두 꽉 찼다면 대기할 수 있는 큐를 10개 설정 하였습니다.

     

    구현하기

    위에서 다룬 내용들을 토대로 간단히 기능을 구현해 보도록 하겠습니다.

    예제 소스

    이번장에 진행될 소스는 아래 Github에서 다운로드 받으시고, "4.Hystrix" branch를 체크아웃하시면 구현된 소스 확인이 가능합니다.

     

    서비스1에 "/hystrix1", "/hystrix2" 2개의 API가 있습니다.

     

    "/hystrix1" API는 서비스2의 API를 호출하여, 회원 ID를 가져오는 기능이 있습니다.

    이 기능에서 1초 이상의 지연이 발생하면 에러를 발생하도록 하였습니다.

     

    "/hystrix2" API는 DB에서 회원 ID를 가져오는 기능이 있습니다. (실제로는 DB연동 없이 객체명만 DAO이고 고정된 값을 반환하는 단순한 기능으로 대체하였습니다.)

    이 기능에서 위에서 예를 든 "10초간 20개 이상의 호출에서 75% 이상의 에러(3초 이상 지연)가 발생하면 7초간 Circuit Open 하라" 라는 Hystrix 설정을 하였습니다.

     

    그럼 구현해 보겠습니다.

     

    Library Dependency (build.gradle)

    1
    2
    3
    4
    5
    6
    dependencies {
        ..... 중략 .....
        implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
        ..... 중략 .....
        }
    }
    cs

    spring-cloud-starter-netflix-eureka-client 라이브러리가 필요하여 설정합니다.

     

    코드 구현

    이미 위에서 설명이 있었거나, 추가 설명이 필요 없는 부분은 생략하겠습니다.

     

    HystrixTestController 

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @RestController
    public class HystrixTestController {
     
        @Autowired
        HystrixTestService hystrixTestService;
     
        @GetMapping(value = "/hystrix1")
        public String hystrix1() {
            String id = "1";
            return hystrixTestService.getName(id);
        }
     
        @GetMapping(value = "/hystrix2")
        public String hystrix2() {
            String id = "1";
            return hystrixTestService.getIdInfo(id);
        }
     
    }
    cs

     

    HystrixTestService 

    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
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    @Service
    public class HystrixTestService {
     
        @Autowired
        RibbonClientCommunicator ribbonClientCommunicator;
     
        @Autowired
        HystrixTestDao hystrixTestDao;
     
        /** 기본 설정 - 1초 이상 지연 발생시 500 Internal Serve Error 발생 */
        @HystrixCommand
        public String getName(String id) {
            log.info("Communicating ...");
     
            /** 호출 3회중 랜덤 1회 3초 통신지 지연된걸로 간주한다. */
            CommonUtil.randomlyRunLong(33000);
            return ribbonClientCommunicator.getName(id);
        }
     
        @HystrixCommand(
        threadPoolProperties =
           // 스레드 풀의 갯수를 정의
           {@HystrixProperty(name = "coreSize",value="30"),
                   // 스레드 풀 앞에 배치할 큐와 큐에 넣을 요청 수를 정의
                   //  => 스레드가 분주할 때 큐 이용
                   @HystrixProperty(name="maxQueueSize", value="10")},
             /** 정의 : 15초 동안 10건의 호출 중 75% 에러(3초 이상 SELECT 지연)가 발생하면
              *              Circuit을 7초 동안 Open 하라 */
             commandProperties={
                 // DB SELECT Timeout 설정 3초
              @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds", value="3000"),
              // 서비스 호출 문제를 모니터할 시간 간격을 설정 (위 정의에서 15초 동안에 해당)
              @HystrixProperty(name="metrics.rollingStats.timeInMilliseconds", value="15000"),
              // 히스트릭스가 호출 차단을 고려하는데 필요한 요청 수 (위 정의에서 10건에 해당)
              @HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="10"),
              // 호출 차단 실패 비율 (위 정의에서 75%에 해당)
              @HystrixProperty(name="circuitBreaker.errorThresholdPercentage", value="75"),
              // 차단 후 서비스의 회복 상태를 확인할 때까지 대기할 시간 (위 정의에서 7초에 해당)
              @HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds", value="7000"),
              // 설정한 시간 간격 동안 통계를 수집할 횟수를 설정
              @HystrixProperty(name="metrics.rollingStats.numBuckets", value="5")}
        )
        public String getIdInfo(String id) {
            return hystrixTestDao.getIdInfo(id);
        }
     
        private String buildFallbackIdInfo(String id){
            return id + "'s Fallback Info";
        }
    }
    cs

    위에서 설명드린 소스코드와 동일 합니다.

     

    HystrixTestDao

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Repository
    public class HystrixTestDao {
     
        public String getIdInfo(String id) {
            log.info("Searching from Database ...");
     
            /** DB 호출 3회중 랜덤 1회 11초 지연 */
            CommonUtil.randomlyRunLong();
            return id + "'s info";
        }
    }
    cs

    실제 DB를 연동하여야겠지만, Spring Cloud 이외의 코드를 최대한 배제하기 위해 단순 Static 값을 반환하도록 하였습니다. 또한 3회 호출 중 랜덤하게 11초 지연이 발생하도록 Sleep을 걸어 장애 지연을 발생하였습니다.

     

     

    동작 확인

    구현이 완료되었으니 실행을 해보도록 하겠습니다.

    앞서 구현한 유레카 서버, 서비스1, 서비스2와 API Gateway를 모드 실행합니다.

     

    "/hystrix1", "/hystrix2" API를 호출하면 랜덤하게 지연이 발생되는 것을 확인 하실 수 있습니다.

     

    또한 "/hystrix2" API의 소스에서 대부분의 호출이 에러가 발생하도록 조금 수정하면 Circuit이 Open되고 Close 되는 것 또한 확인이 가능하십니다.

     

    마무리

    이번장에서는 실무에서 중요한 요소중 하나인 장애 대응을 위한 Hystrix에 대해 알아보고 구현해 보았습니다. 다음 장에서는 설정의 분리를 위한 Spring Cloud Config에 대해 알아 보겠습니다.

     

     

    참고문헌

    - 스프링 마이크로서비스 코딩 공작소 정성권 옮김 길벗

    - https://github.com/klimtever

    댓글

Designed by Tistory.