본문 바로가기
프로그래밍/Spring Cloud

[MSA] Spring Cloud Zuul 1.x - 실습편

by 사바라다 2020. 1. 22.

안녕하세요. 오늘은 저번시간에 이어서 Spring Cloud Zuul 1.0의 실습을 해보도록 하겠습니다. 실습의 순서는 Spring Cloud Zuul 사용을 위한 의존성을 알아보고, Filter 등록 방법, Routing, 그리고 장애에 대비한 Retry 및 Fallback을 사용하는 방법까지 알아보도록 하겠습니다.

pre-condition(사전 조건)

zuul을 실습하기 전에 서비스를 한게 만들고 2개로 띄어두어야 실질적인 테스트를 할 수 있습니다.

아래와 같이 설정 Controller를 제작한 후 build, 2개의 서버를 띄우도록 합시다.

@RestController
public class TestController {
    @GetMapping("/ping")
    public ResponseEntity<String> healthCheck() {
        return ResponseEntity.ok("pong");
    }
}
java -jar application.jar --server.port=8081 &

java -jar application.jar --server.port=8082 &

Dependency(의존성)

Zuul을 사용하기 위해서 필요한 Dependency는 아래와 같습니다.

implementation("org.springframework.cloud:spring-cloud-starter-netflix-zuul:2.1.2.RELEASE")

저번 포스팅에서 zuul에는 내부 Dependency로 hystrix와 ribbon이 기본적으로 포함되어있다고 했습니다. 실제로 intellij에서 Dependency의 구조도를 확인해보았을 때 아래와 같이 출력됨을 확인할 수 있었습니다.

zuul dependency(의존성)

그리고 Zuul을 사용하기 위해서는 아래와 같이 Annotation을 사용하면 됩니다.

@EnableZuulProxy
@SpringBootApplication
public class ZuulApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class, args);
    }
}

@EnableZuulProxy대신에 @EnableZuulServer를 이용할 수 있습니다. @EnableZuulProxy@EnableZuulServer에 eureka를 연동한 proxy 등 추가적인 proxy가 가능하도록 해주는 필터들이 추가되어 있습니다. ex) PreDecorationFilter, RibbonRoutingFilter, SimpleHostRoutingFilter

Routing

대부분 zuul의 설정들은 yml 파일로 설정할 수 있습니다. 기 정의되어있는 기본 값 및 설정할 수 있는 list는 ZuulProperties class를 참조하시면 됩니다.

아래는 기본 라우팅 test를 하기 위해 만들어본 간단한 예제입니다. 아래와 같이 설정되어 있을때 localhost:8080/service-1/ping으로 요청한다면 8081 서버에서 응답이 갈것이고 localhost:8080/service-2/ping으로 요청한다면 8082 서버에서 응답이 올것입니다.

server:
  port: 8080

zuul:
  routes:
    service-1:
      path: /service-1/**
      url: http://localhost:8081
    service-2:
      path: /service-2/**
      url: http://localhost:8082

Retry(Ribbon)

만약 내부서버로 Request가 실패했을 경우 재시도를 하고 다른 서버로 시도하는 역할은 Zuul에서 Ribbon을 통해 구현할 수 있습니다. 설정 방법은 {service-id}.ribbon.~~으로 Ribbon의 설정 방법과 동일합니다. 그리고 추가적으로 zuul.retryable=true로 설정해주면 됩니다. ribbon의 설정 내용은 [MSA] Spring Cloud Ribbon - 개념과 실습편을 참고해주시면 됩니다.

zuul:
  retryable: true

service-1:
  ribbon:
    MaxAutoRetries: 1
    MaxAutoRetriesNextServer: 1
    OkToRetryOnAllOperations: true
    ConnectTimeout: 3000
    ReadTimeout: 5000
service-2:
  ribbon:
    MaxAutoRetries: 1
    MaxAutoRetriesNextServer: 1
    OkToRetryOnAllOperations: true
    ConnectTimeout: 3000
    ReadTimeout: 5000

Hystrix - Fallback

API Gateway는 외부에서 서비스에 접속하기 위한 End-Point의 역할을 대신해줍니다. 그렇기 때문에 만약 서비스가 장애가 난다면 그 영향은 API Gateway에 미칠 수 밖에 없습니다. 이럴때를 위해 Zuul은 Hystrix를 기본적으로 내장하고 있습니다. 내장되어있는 Hystrix는 아래와 같이 yaml 파일로 설정을 변경할 수 있습니다. 설정방법은 기존 hystrix와 같으며 yaml로 hystrix에 대해 설정할 수 있습니다. hystrix의 자세한 설명은 [MSA] Spring Cloud Hystrix - 개념편[MSA] Spring Cloud Hystrix - 실습편을 참고해주시기 바랍니다.

zuul:
  ribbon-isolation-strategy: thread

# hystrix yaml로 설정하는 방법
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 32000
      circuitBreaker:
        enabled: true
        requestVolumeThreshold: 3
        sleepWindowInMilliseconds: 5000
        errorThresholdPercentage: 50
      metrics:
        rollingStats:
          timeInMilliseconds: 10000

route별 fallback 만들기. zuul에서는 FallbackProvider를 구현하여 이용하여 route 별로 fallback method를 제작할 수 있습니다. 제작하는 방법은 아래와 같습니다. 각 method 및 Annotation의 역할에 대해서는 코드에 주석으로 달아두었습니다.

@Configuration // Componenet Scan에 Scan 되도록 하며 bean 등록
public class AuthServiceFallbackConfiguration implements FallbackProvider {

    private static final String NOT_AVAILABLE = "service-1 is not available.";

   /**
      * The route this fallback will be used for.
      * @return The route the fallback will be used for.
     */
    @Override // fallback을 등록할 route return
    public String getRoute() { 
        return "service-1";
    }

    /**
      * Provides a fallback response based on the cause of the failed execution.
      *
      * @param route The route the fallback is for
      * @param cause cause of the main method failure, may be <code>null</code>
      * @return the fallback response
      */
    @Override // fallback 발생 시 호출되는 method
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        if (cause instanceof HystrixTimeoutException) {
            return new GatewayClientResponse(HttpStatus.GATEWAY_TIMEOUT, NOT_AVAILABLE);
        } else {
            return new GatewayClientResponse(HttpStatus.INTERNAL_SERVER_ERROR, NOT_AVAILABLE);
        }
    }
}

위의 형식으로 FallbackProvider를 구현하게 되면 AbstractRibbonCommandFactory에 의해서 자동으로 cache에 등록이 됩니다. 그리고 Error 발생시 Error Filter로 넘어가고 위의 fallbackResponse가 실행되어지게 됩니다. 그리고 각 서비스의 에러 response에 대해서 zuul에서 감싸서 return하고자 하여 GatewayClientResponse 객체를 리턴하게 만들었고 그 안의 내용은 아래와 같습니다.

class GatewayClientResponse implements ClientHttpResponse {

    private HttpStatus httpStatus;
    private String message;

    public GatewayClientResponse(HttpStatus httpStatus, String message) {
        this.httpStatus = httpStatus;
        this.message = message;
    }

    @Override
    public HttpStatus getStatusCode() throws IOException {
        return httpStatus;
    }

    @Override
    public int getRawStatusCode() throws IOException {
        return httpStatus.value();
    }

    @Override
    public String getStatusText() throws IOException {
        return httpStatus.getReasonPhrase();
    }

    @Override
    public void close() {

    }

    @Override
    public InputStream getBody() throws IOException {
        return new ByteArrayInputStream(message.getBytes());
    }

    @Override
    public HttpHeaders getHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        return headers;
    }
}

Custom Filter 추가

Zuul의 Filter를 추가하는 방법은 ZuulFilter을 상속받아서 구현하는 것입니다. 상속을 받게되면 저번 Zuul의 개념편에서 보았던 4가지를 필수적으로 재정의 해주시면 사용할 수 있게 됩니다. 각 내용은 아래에서 보도록 하겠습니다.

public class RouteFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return "route";
    }

    @Override
    public int filterOrder() {
        return 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        System.out.println("Inside Route Filter");
        return null;
    }
}
  • filterType() : filter의 type으로 "pre", "route", "post", "error", "static"을 용도에 따라 return 하면 됩니다.
  • filterOrder() : type안에서 해당 필터가 실행되는 순서입니다.
  • shoudFilter() : run method를 실행한다면 true, 실행하지 않아도 된다면 false를 return합니다.
  • run() : 실제 filter의 로직을 담당하는 method입니다.

이렇게 만든 Class는 Bean으로 등록하면 filter가 등록되어 사용할 수 있게 됩니다.

마무리

zuul system architecture, 출처 : https://netflixtechblog.com/announcing-zuul-edge-service-in-the-cloud-ab3af5be08ee

위의 아키텍처는 Zuul 시스템이 전체적으로 어떻게 돌아가는지 나타낸 아키텍처입니다. 오늘은 위 아키텍처를 구현 및 코드로 확인해보며 알아보는 시간을 가졌습니다.

감사합니다.

참조

#enablezuulproxy-vs-enablezuulserver

https://netflixtechblog.com/announcing-zuul-edge-service-in-the-cloud-ab3af5be08ee

댓글