2018년 6월 10일 일요일

Spring Cloud Zuul

Router and Filter: Zuul

Routing은 마이크로 서비스 아키텍처의 필수적인 부분이다.
예를 들어, / 는 웹 응용 프로그램에 매핑 될 수 있으며 /api/users 는 member-service에 매핑되고 /api/product 는 product-service 매핑 되는 Case 가 발생할 수 있을 것이다. Zuul은 Netflix의 JVM 기반 서버측 로드 벨런서로써 동적 라우팅, 모니터링, 탄력성, 보안 등을 제공하는 Edge-Service이다.
엣지(edge)는 서비스의 가장 바깥에서 고객과의 접점인 서비스를 일컫는 것으로 Caching, Proxy, URL 별 TTL, Dynamic Routing 을 통해 다른 모든 서비스에 대한 게이트웨이 역할을하며 플랫폼 서비스를 일컫는 용어이다.
Zuul Edge 서비스는 일련의 필터로 구성되며, 각 필터는 HTTP 요청 및 / 또는 응답에 대해 몇 가지 작업을 수행하여 제어를 다음 필터로 전달하며 이외에도 아래와 같은 역할을 한다.
  • Authentication and Security : 인증 및 보안 - 각 리소스에 대한 인증 요구 사항을 식별하고이를 만족시키지 않는 요청을 거부
  • Insights and Monitoring : 의미있는 데이터 및 통계를 추적하여 정확한 뷰를 제공함
  • Dynamic Routing : 필요에 따라 다른 백엔드 클러스터에 요청함
  • Stress Testing : 성능을 측정하기 위해 트래픽을 클러스터로 점차 증가시킨다.
  • Load Shedding : 각 요청 유형에 대한 용량 할당 및 제한을 초과하는 요청 삭제
  • Static Response handling : 일부 응답을 내부 클러스터로 전달하는 대신 에지에서 직접 작성
  • Multiregion Resiliency : ELB 사용을 다양화하고 요청을 내부에 더 가까이 있는 AWS 지역으로 라우팅 (아카마이 CDN 같은 개념)
아래는 넷플릭스 공식 문서에 있는 서비스 아키텍쳐이다.

Zuul 의 구성

Zuul은 다른 Netflix OSS 요소(Hystrix, Ribbon, Turbine, Archaius) 등을 통해 구성된다.
  • Hystrix : Latency and Fault Tolerance for Distributed Systems : 서킷 브레이커 같은 역할을 한다. 문제가 발생했을 때 트래픽을 차단하고 의존 서비스들을 격리한다. (https://github.com/Netflix/Hystrix)
  • Ribbon : Client Side Load Balancing, Multiple protocol (HTTP, TCP, UDP) support in an asynchronous and reactive model : 리본은 Zuul에서 모든 아웃 바운드 요청에 대해 클라이언트이며, 네트워크 성능 및 오류에 대한 자세한 정보를 제공 할뿐만 아니라 부하 분산을위한 소프트웨어의 로드 밸런싱을 처리한다. (https://github.com/Netflix/ribbon)
  • Turbine : real-time metrics, aggregating streams of Server-Sent Event (SSE). 실시간으로 세분화 된 메트릭을 집계하여 신속하게 문제를 관찰하고 대응할 수 있다. (https://github.com/Netflix/Turbine)
여기서는 이전 Eureka 예제를 이어서, Zuul 을 통해 부하 분산을 위한 Routing과 간단한 Filter 를 설정 해 볼 예정이다.

Zuul Routing

먼저 eureka-service 의 application.yml 을 열어 하단에 zuul routing을 설정한다.
zuul:  
  # service will be mapped under /api uri  
  prefix: /api  
  routes:  
    stock-db-service: 
      path: /stock-db-service/**  
      url: http://localhost:8083  
    stock-service:  
      path: /stock-service/**  
      url: http://localhost:8082
EurekaServiceApplication 에 @EnableZuulProxy 를 추가한다.
전체 서버를 기동한 후 http://localhost:8761/api/stock-service/stock/user?userId=edell 로 접근해본다.
http://localhost:8082/stock/user?userId=edell 나 http://localhost:8083/api/v1/user?userId=edell 를 통해 호출한 결과값과 Zuul Proxy를 통해 /api/[Route-path명] 이 동일하다는 것을 확인할 수 있을 것이다.
stock-db-service 역시 호출해서 테스트 해보도록 한다.
정상적으로 호출되는 것을 확인할 수 있다.
stock-service 의 StockController 에 아래와 같이 간단한 헬로 월드를 리턴하는 메소드를 추가해보자.
@GetMapping("/hello")  
public String hello() {  
    return "Stock-service Hello World";  
}
stock-service 만 restart 한 후 proxy를 통해 호출해보면 정상적으로 문자열이 출력되는 것을 확인할 수 있다.
http://localhost:8761/api/stock-service/stock/hello
만약, Spring Cloud Config 를 사용한다면 refresh 스코프의 어노테이션을 통해 stock-service 서버의 재기동 없이도 설정을 추가하거나 수정할 수 있고 새로운 Routing 도 가능하게 될 것이다.
이제 이 소스를 stock-db-service (8083포트) 와 deal-service (8084) 에 심고 로드 밸런싱이 제대로 동작하는지 확인해보자.
이전 eureka 강좌에서 두 서버 모두 application.yml 에 spring.application.name 이 stock-db-service 로 등록 된것을 기억할 것이다.
양쪽 서버 모두 콘솔을 clear 한 후 로그를 보면 http://localhost:8761/api/stock-service/stock/user?userId=edell url 이 호출될 때 마다 번갈아가며 로그가 올라가는 것을 확인할 수 있을 것이다. 이전에는 Zuul proxy routing이 아니라 stock-service 를 통하여 로드 밸런스가 적용되었었다는 것은 기억하기 바란다.
즉, @EnableZuulProxy 를 추가함으로써 동적으로 서비스의 투입, 제거가 가능한데다 Zuul Proxy 서버의 재기동 없이 동적 Routing, Load Balancing 이 가능하다는 것을 확인할 수 있다.

Zuul Filter

대량의 트래픽을 부하분산 하다보면 예기치 못한 문제들을 당면하게 된다. 미리 경고가 발생한다면 예측 가능할 수 있으나 그렇지 않은 경우가 많다 보니 내부적으로 신속하고, 동적으로 해결하고자 하는 방안으로 Filter를 적용하게 되었다.

Zuul Request Lifecycle



 Zuul은 HTTP 요청 및 응답의 라우팅 중에 작업을 수행 할 수있는 필터를 동적으로 읽고 컴파일하고 실행하기위한 프레임 워크를 제공하는데, 필터는 서로 직접 통신하지 않고 각 요청에 고유 한 RequestContext를 통해 상태를 공유하게 된다.
또한 Filter를 통해 requtest 요청을 end-point로 으로 전달하는 대신 Zuul 자체에서 응답을 생성하는 사용자 정의 유형으로 변경하여 실행할 수 있다.

Zuul의 filter

  • Pre Filter : 라우팅전에 실행되는 필터. 로깅이나 인증 등을 실행할 수 있다.
  • Routing Filter : 요청에 대한 라우팅을 다루는 필터이다.
  • Post Filter : 라우팅 이후에 실행되는 필터이다. response 에 대한 처리를 추가하거나 응답속도등의 데이터를 수집 할 수 있겠다.
  • Error Filter : 에러 발생시 실행되는 필터이다.
Filter 를 생성하려면 extends ZuulFilter 를 통해 4개의 메소드를 구현해주면 된다.
– public String filterType(): filter의 타입을 명세한다. (pre, route, post, error) by a String
– public int filterOrder(): filter의 순서를 지정한다.
– public boolean shouldFilter(): filter 의 실행여부를 결정한다.
– public Object run(): filter의 기능을 구현한다.
이번에는 Filter 를 통해 호출되는 Parameter 와 Return 된 HTTP Status 를 로그로 찍어보는 예제를 작성해보자.
먼저 Pre Filter 코드이다.
package com.example.springcloud.eurekaservice.filter;  
  
import com.netflix.zuul.ZuulFilter;  
import com.netflix.zuul.context.RequestContext;  
import com.netflix.zuul.exception.ZuulException;  
import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;  
  
import javax.servlet.http.HttpServletRequest;  
import java.util.Enumeration;  
  
public class SimplePreFilter extends ZuulFilter {  
    private static Logger log = LoggerFactory.getLogger(SimplePreFilter.class);  
  
    @Override  
    public String filterType() {  
        return "pre";  
    }   
  
    @Override  
    public int filterOrder() {  
        return 1;  
    }  
  
    @Override  
    public boolean shouldFilter() {  
        return true;  
    }  
  
    @Override  
    public Object run() throws ZuulException {  
        RequestContext ctx = RequestContext.getCurrentContext();  
        HttpServletRequest request = ctx.getRequest();  
  
        log.info("PreFilter: " + String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString()));  
  
        Enumeration<String> params = request.getParameterNames();  
        while(params.hasMoreElements()){  
            String paramName = params.nextElement();  
            log.info("Parameter Name: " + paramName , ", Value - "+request.getParameter(paramName));  
        }  
  
        return null;  
    }  
}
그리고 나서 EurekaServiceApplication 에 빈을 추가한다.
  @Bean  
  public SimplePreFilter preFilter() {  
       return new SimplePreFilter();  
  }
서버를 재기동 한 후 관련 API들을 호출해보면 아래와 같이 console 에 로그가 찍히는 것을 확인할 수 있다.
2018-06-10 17:57:21.221  INFO 73656 --- [nio-8761-exec-6] c.e.s.e.filter.SimplePreFilter           : PreFilter: GET request to http://localhost:8761/api/stock-service/stock/hello
2018-06-10 17:57:29.250  INFO 73656 --- [io-8761-exec-10] c.e.s.e.filter.SimplePreFilter           : PreFilter: GET request to http://localhost:8761/api/stock-service/stock/user
2018-06-10 17:57:29.250  INFO 73656 --- [io-8761-exec-10] c.e.s.e.filter.SimplePreFilter           : Parameter Name: userId
이제 RouteFilter와 PostFilter, ErrorFilter 를 작성한 후 EurekaServiceApplication 에 빈들을 추가해준 후 리스타트 후 테스트 해보자.
package com.example.springcloud.eurekaservice.filter;  
  
import com.netflix.zuul.ZuulFilter;  
import com.netflix.zuul.context.RequestContext;  
import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;  
  
import javax.servlet.http.HttpServletRequest;  
  
public class SimpleRouteFilter extends ZuulFilter {  
    private static Logger log = LoggerFactory.getLogger(SimpleRouteFilter.class);  
  
    @Override  
    public String filterType() {  
        return "route";  
    }  
  
    @Override  
    public int filterOrder() {  
        return 1;  
    }  
  
    @Override  
    public boolean shouldFilter() {  
        return true;  
    }  
  
    @Override  
    public Object run() {  
        RequestContext ctx = RequestContext.getCurrentContext();  
        HttpServletRequest request = ctx.getRequest();  
 
        log.info("RouteFilter: " + String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString()));  
  
        return null;  
    }  
}
package com.example.springcloud.eurekaservice.filter;  
  
  
import com.netflix.zuul.ZuulFilter;  
import com.netflix.zuul.context.RequestContext;  
import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;  
  
import javax.servlet.http.HttpServletResponse;  
  
public class SimplePostFilter extends ZuulFilter {  
    private static Logger log = LoggerFactory.getLogger(SimplePostFilter.class);  
    
    @Override  
    public String filterType() {  
        return "post";  
    }  
   
    @Override  
    public int filterOrder() {  
        return 1;  
    }  
   
    @Override  
    public boolean shouldFilter() {  
        return true;  
    }  
   
    @Override  
    public Object run() {  
        HttpServletResponse response = RequestContext.getCurrentContext().getResponse();  
        log.info("SimplePostFilter: " + String.format("response's content type is %s", response.getStatus()));  
        return null;  
    }  
}
package com.example.springcloud.eurekaservice.filter;  
  
  
import com.netflix.zuul.ZuulFilter;  
import com.netflix.zuul.context.RequestContext;  
import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;  
  
import javax.servlet.http.HttpServletResponse;  
  
public class SimpleErrorFilter extends ZuulFilter {  
    private static Logger log = LoggerFactory.getLogger(SimpleErrorFilter.class);  
  
    @Override  
    public String filterType() {  
        return "error";  
    }  
  
    @Override  
    public int filterOrder() {  
        return 1;  
    }  
  
    @Override  
    public boolean shouldFilter() {  
        return true;  
    }  
  
    @Override  
    public Object run() {  
        HttpServletResponse response = RequestContext.getCurrentContext().getResponse();  
  
        log.info("ErrorFilter: " + String.format("response status is %d", response.getStatus()));  
  
        return null;  
    }  
}
package com.example.springcloud.eurekaservice;  
  
import com.example.springcloud.eurekaservice.filter.SimpleErrorFilter;  
import com.example.springcloud.eurekaservice.filter.SimplePostFilter;  
import com.example.springcloud.eurekaservice.filter.SimpleRouteFilter;  
import com.example.springcloud.eurekaservice.filter.SimplePreFilter;  
import org.springframework.boot.SpringApplication;  
import org.springframework.boot.autoconfigure.SpringBootApplication;  
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;  
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;  
import org.springframework.context.annotation.Bean;  
  
@EnableZuulProxy  
@EnableEurekaServer  
@SpringBootApplication  
public class EurekaServiceApplication {  
  
    public static void main(String[] args) {  
        SpringApplication.run(EurekaServiceApplication.class, args);  
    }  
  
    @Bean  
    public SimplePreFilter preFilter() {  
        return new SimplePreFilter();  
    }  
  
    @Bean  
    public SimpleRouteFilter routeFilter() {  
        return new SimpleRouteFilter();  
    }  
  
    @Bean  
    public SimplePostFilter postFilter() {  
        return new SimplePostFilter();  
    }  
  
    @Bean  
    public SimpleErrorFilter errorFilter() {  
        return new SimpleErrorFilter();  
    }  
}
각 API들을 호출해보면 작성한 로그들이 찍히는 것을 확인할 수 있을 것이다.
2018-06-10 18:03:37.272  INFO 73856 --- [nio-8761-exec-5] c.e.s.e.filter.SimplePreFilter           : PreFilter: GET request to http://localhost:8761/api/stock-service/stock/user
2018-06-10 18:03:37.274  INFO 73856 --- [nio-8761-exec-5] c.e.s.e.filter.SimplePreFilter           : Parameter Name: userId
2018-06-10 18:03:37.277  INFO 73856 --- [nio-8761-exec-5] c.e.s.e.filter.SimpleRouteFilter         : RouteFilter: GET request to http://localhost:8761/api/stock-service/stock/user
2018-06-10 18:03:37.355  INFO 73856 --- [nio-8761-exec-5] c.e.s.e.filter.SimplePostFilter          : SimplePostFilter: response's content type is 200
2018-06-10 18:03:46.917  INFO 73856 --- [nio-8761-exec-6] c.e.s.e.filter.SimplePreFilter           : PreFilter: GET request to http://localhost:8761/api/stock-service/stock/hello
2018-06-10 18:03:46.918  INFO 73856 --- [nio-8761-exec-6] c.e.s.e.filter.SimpleRouteFilter         : RouteFilter: GET request to http://localhost:8761/api/stock-service/stock/hello
2018-06-10 18:03:46.930  INFO 73856 --- [nio-8761-exec-6] c.e.s.e.filter.SimplePostFilter          : SimplePostFilter: response's content type is 404
2018-06-10 18:06:18.033  INFO 73856 --- [nio-8761-exec-7] c.e.s.e.filter.SimplePreFilter           : PreFilter: GET request to http://localhost:8761/api/stock-service/stock/error
2018-06-10 18:06:18.033  INFO 73856 --- [nio-8761-exec-7] c.e.s.e.filter.SimpleRouteFilter         : RouteFilter: GET request to http://localhost:8761/api/stock-service/stock/error
2018-06-10 18:06:18.148  INFO 73856 --- [nio-8761-exec-7] c.e.s.e.filter.SimplePostFilter          : SimplePostFilter: response's content type is 200
2018-06-10 18:08:45.986  INFO 73856 --- [nio-8761-exec-4] c.e.s.e.filter.SimplePreFilter           : PreFilter: GET request to http://localhost:8761/api/stock-service/stock/user
2018-06-10 18:08:45.986  INFO 73856 --- [nio-8761-exec-4] c.e.s.e.filter.SimplePreFilter           : Parameter Name: userId
2018-06-10 18:08:45.987  INFO 73856 --- [nio-8761-exec-4] c.e.s.e.filter.SimpleRouteFilter         : RouteFilter: GET request to http://localhost:8761/api/stock-service/stock/user
2018-06-10 18:08:48.004  WARN 73856 --- [nio-8761-exec-4] o.s.c.n.z.filters.post.SendErrorFilter   : Error during filtering
---- error trace 
...
...

2018-06-10 18:08:48.017  INFO 73856 --- [nio-8761-exec-4] c.e.s.e.filter.SimpleErrorFilter         : ErrorFilter: response status is 500
2018-06-10 18:08:48.017  INFO 73856 --- [nio-8761-exec-4] c.e.s.e.filter.SimplePostFilter          : SimplePostFilter: response's content type is 500
이상으로 Zuul 의 Proxy와 Filter 에 대한 정리를 마치도록 한다.
= FIN =