Router and Filter: Zuul
Routing은 마이크로 서비스 아키텍처의 필수적인 부분이다.
예를 들어,
엣지(edge)는 서비스의 가장 바깥에서 고객과의 접점인 서비스를 일컫는 것으로 Caching, Proxy, URL 별 TTL, Dynamic 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: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
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 로 등록 된것을 기억할 것이다.
이제 이 소스를 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 =