本章介绍Netflix构建微服务的另一个 组件一一智能路由网关组件ZuuloZuul 作为微服务系统的网关组件,用于构建边界服务( Edge Service),致力于动态路由、过滤、监控、弹性伸缩和安全。
注:工程代码是在前几章的基础上迭代的,可参考前几章的代码。
一、为什么需要Zuul
Zuul作为路由网关组件,在微服务架构中有着非常重要的作用,主要体现在以下 6 个方面。
- Zuul、 Ribbon以及Eureka相结合,可以实现智能路由和负载均衡的功能, Zuul能够将请求流量按某种策略分发到集群状态的多个服务实例。
- 网关将所有服务的API接口统一聚合,并统一对外暴露。外界系统调用 API接口时, 都是由网关对外暴露的API接口,外界系统不需要知道微服务系统中各服务相互调用的复杂性。微服务系统也保护了其内部微服务单元的API接口,防止其被外界直接调用,导致服务的敏感信息对外暴露。
- 网关服务可以做用户身份认证和权限认证,防止非法请求操作 API 接口,对服务器起到保护作用。
- 网关可以实现监控功能,实时日志输出,对请求进行记录。 网关可以用来实现流量监控,在高流量的情况下,对服务进行降级。
- API 接口从内部服务分离出来,方便做测试。
二、Zuul的工作原理
Zuul是通过Servlet来实现的, Zuul通过自定义的ZuulServlet (类似于SpringMVC的DispatcServlet)来对请求进行控制。 Zuul的核心是一系列过滤器,可以在Http请求的发起和响应返回期间执行一系列的过滤器。 Zuul包括以下4种过滤器。
- PRE: 这种过滤器是在请求路由到具体的服务之前执行的,这种类型的过滤器可以做 安全验证 ,例如身份验证、 参数验证等。
- ROUTE : 这种过滤器用于将请求路由到具体的微服务实例 。在默认情况下 ,它使用 Http Client进行网络请求。
- POST: 这种过滤器是在请求己被路由到微服务后执行的 。 一般情况下,用作收集统计信息、指标,以及将响应传输到客户端 。
-
ERROR: 这种过滤器是在其他过滤器发生错误时执行的 。
Zuul采取了动态读取、编译和运行这些过滤器。 过滤器之间不能直接相互通信,而是通过RequestContext对象来共享数据, 每个请求都会创建一个RequestContext对象。 Zuul过滤器具有以下关键特性。 - Type (类型): Zuul过滤器的类型,这个类型决定了过滤器在请求的哪个阶段起作用, 例如Pre、 Post阶段等。
- Execution Order (执行顺序) :规定了过滤器的执行顺序,Order的值越小,越先执行 。
- Criteria (标准): Filter执行所需的条件 。
- Action (行动): 如果符合执行条件,则执行Action (即逻辑代码)。
Zuul中默认实现的Filter
类型 | 顺序 | 过滤器 | 功能 |
---|---|---|---|
pre | -3 | ServletDetectionFilter | 标记处理Servlet的类型 |
pre | -2 | Servlet30WrapperFilter | 包装HttpServletRequest请求 |
pre | -1 | FormBodyWrapperFilter | 包装请求体 |
route | 1 | DebugFilter | 标记调试标志 |
route | 5 | PreDecorationFilter | 处理请求上下文供后续使用 |
route | 10 | RibbonRoutingFilter | serviceId请求转发 |
route | 100 | SimpleHostRoutingFilter | url请求转发 |
route | 500 | SendForwardFilter | forward请求转发 |
post | 0 | SendErrorFilter | 处理有错误的请求响应 |
post | 1000 | SendResponseFilter | 处理正常的请求响应 |
禁用指定的Filter
可以在application.yml中配置需要禁用的filter,格式如下:
zuul:
FormBodyWrapperFilter:
pre:
disable: true
Filter的生命周期
当一个客户端Request请求进入Zuul 网关服务时,网关先进入“pre filter飞进行一系列的验证、操作或者判断。 然后交给“ route filter”进行路由转发,转发到具体的服务实例进行逻辑处理、返回数据。当具体的服务处理完后,最后由"post filter"进行处理,该类型的处理器处理完之后,将Response信息返回给客户端。
ZuulServlet是Zuul的核心Servlet。ZuulServlet的作用是初始化ZuulFilter,并编排这些 ZuulFilter的执行顺序。该类中有一个service()方法,执行了过滤器执行的逻辑。
@Override
public void service() throws ServletException, IOException {
try {
try {
preRoute();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
route();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
postRoute();
} catch (ZuulException e) {
error(e);
return;
}
} catch (
Throwable e) {
error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
从上面的代码可知,首先执行preRoute()方法,这个方法执行的是P阻类型的过滤器的逻辑。 如果执行这个方法时出错了,那么会执行error(e)和postRoute()。然后执行route()方法,该方法是执行ROUTE类型过滤器的逻辑。最后执行postRoute(),该方法执行了POST类型过滤器的逻辑 。
自定义Filter
实现自定义Filter,需要继承ZuulFilter的类,并覆盖其中的4个方法。
public class MyFilter extends ZuulFilter {
@Override
String filterType() {
return "pre"; //定义filter的类型,有pre、route、post、error四种
}
@Override
int filterOrder() {
return 10; //定义filter的顺序,数字越小表示顺序越高,越先执行
}
@Override
boolean shouldFilter() {
return true; //表示是否需要执行该filter,true表示执行,false表示不执行
}
@Override
Object run() {
return null; //filter需要执行的具体操作
}
}
三、案例实战
3.1 搭建 Zuul 服务
本章的案例是在上一章案例的基础上进行讲解的。新建一个SpringBoot工程,取名为eureka-zuul-client,在pom文件中引入相关依赖,包括继承了主Maven工程的pom文件,具体代码如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
在工程的配置文件 application.yml 中做相关的配置,包括配置服务注册中心的地址为 http://localhost:8671/eureka,端口号为5000,程序名为eureka-zuul-client。最后来重点讲解一下Zuul路由的配置写法,在本案例中,zuul.routes.hiapi.path为 “/hiapi/**”, zuul.routes.hiapi.serviceld为“ eureka-client”,这两个配置就可以将以“hiapi”开头的Uri路由到 eureka-client服务。其中,zuul.routes.hiapi中的“ hiapi”是门己定义的,需要指定它的path和 serviceld,两者配合使用,就可以将指定类型的请求Url路由到指定的Serviceld。同理,满足以“/ribbonapi" 开头的请求Url都会被分发到eureka-ribbon- client,满足以“/feignapi/”开头的请求Url都会被分发到eureka-feign-client服务。如果某服务存在多个实例, Zuul结合Ribbon会做负载均衡,将请求均分的部分路由到不同的服务实例。
spring:
application:
name: eureka-zuul-client
server:
port: 5000
eureka:
client:
service-url:
defaultZone: http://localhost:8671/eureka/
zuul:
routes:
hiapi:
path: "/hiapi/**"
serviceId: eureka-client
ribbonapi:
path: "/ribbonapi/**"
serviceId: eureka-ribbon-client
feignapi:
path: "/feignapi/**"
serviceId: eureka-feign-client
依次启动工程eureka-server、eureka-client、eureka-ribbon-client、eureka-feign-client和eureka-zuul-client,其中eureka-client启动两个实例,端口为8672和8673。在浏览器上多次访问http://localhost:5000/hiapi/hi?name=ben,浏览器会交替显示以下内容:
可见Zuul在路由转发做了负载均衡。同理,多次访问http://localhost:5000/feignapi/hi?name=ben和http://localhost:5000/ribbonapi/hi?name=ben,也可以看到相似的内容。如果不需要用Ribbon做负载均衡,可以指定服务实例的url,用zuul.routes.hiapi.url配置指定,这时就不需要配置zuul.routes.hiapi.serviceld了。一旦指定了url,Zuul就不能做负载均衡了,而是直接访问指定的url,在实际的开发中这种做法是不可取的。修改配置的代码如下:
spring:
application:
name: eureka-zuul-client
server:
port: 5000
eureka:
client:
service-url:
defaultZone: http://localhost:8671/eureka/
zuul:
routes:
hiapi:
path: "/hiapi/**"
url: http://localhost:8672
重新启动 eureka-zuul-service服务, 请求 http://localhost:5000/hiapi/hi?name=ben,浏览器 只会显示以下内容 :
如果想要指定Url,并且想做负载均衡,那么就需要自己维护负载均衡的服务注册列表。 首先,将ribbon.eureka.enabled改为false,即Ribbon负载均衡客户端不向 EurekaClient获取服务注册列表信息。然后需要自己维护一份注册列表,该注册列表对应的服务名为hiapi-self (这个名字可自定义),通过配置hiapi-self.ribbon.listOfServers来配置多个负载均衡的Url。 代码如下 :
zuul:
routes:
hiapi:
path: "/hiapi/**"
serviceId: hiapi-self
ribbon:
eureka:
enabled: false
hiapi-self:
ribbon:
listOfServers: http://localhost:8672,http://localhost:8673
重新启动eureka-zuul-client服务, 在浏览器上访问http://localhost:5000/hiapi/hi?name=ben,浏览器会显示如下内容 :
hi ben, i am from port:8672
hi ben, i am from port:8673
3.2 在 Zuul 上配置 API 接口的版本号
如果想给每一个服务的 API接口加前缀,例如http://localhost:5000/v1/hiapi/hi?name=ben/, 即在所有的API接口上加一个vl作为版本号。 这时需要用到zuul.prefix的配置,配置代码如下:
zuul:
routes:
hiapi:
path: "/hiapi/**"
serviceId: hiapi-self
ribbonapi:
path: "/ribbonapi/**"
serviceId: eureka-ribbon-client
feignapi:
path: "/feignapi/**"
serviceId: eureka-feign-client
FormBodyWrapperFilter:
pre:
disable: true
prefix: /vl
重新启动eureka-zuul-client服务,在浏览器上访问http://localhost:5000/v1/hiapi/hi?name=ben/
浏览器会显示如下内容 :
hi ben, i am from port:8672
hi ben, i am from port:8673
3.3 在Zuul上配置熔断器
Zuul作为Netflix组件,可以与Ribbon、Eureka和Hystrix等组件相结合,实现负载均衡、 熔断器的功能。在默认情况下, Zuul和Ribbon相结合, 实现了负载均衡的功能。下面来讲解如何在Zuul上实现熔断功能。在Zuul中实现熔断功能需要实现ZuulFallbackProvider 的接口。实现该接口有两个方法,一个是getRouteO方法,用于指定熔断功能应用于哪些路由的服务; 另一个方法fallbackResponseO为进入熔断功能时执行的逻辑 。 ZuulFallbackProvider 的源码如下 :
public interface ZuulFallbackProvider {
public String getRoute();
public ClientHttpResponse fallbackResponse();
}
实现一个针对eureka-client服务的熔断器,当eureka-client 的服务出现故障时,进入熔断逻辑,向浏览器输入一句错误提示,代码如下:
@Component
public class MyFallbackProvider implements ZuulFallbackProvider {
@Override
public String getRoute() {
return "eureka-client";
}
@Override
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return 200;
}
@Override
public String getStatusText() throws IOException {
return "OK";
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("called error, i am fallback.".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
return httpHeaders;
}
};
}
}
重新启动eureka-zuul-client,并且关闭eureka-client的所有实例,在浏览器上访问 http://localhost:5000/hiapi/hi?name=ben,浏览器显示如下图 :
如果需要所有的路由服务都加熔断功能,只需要在 getRoute()方法上返回“*”的匹配符, 代码如下:
public String getRoute() {
return "*";
}
3.4 在 Zuul 中使用过滤器
在前面的内容讲述了过滤器的作用和种类 ,下面来介绍如何实现一个自定义的过滤器。 实现过滤器很简单,只需要继承ZuulFilter,并实现ZuulFilter中的抽象方法,包括filterType() 和filterOrder(),以及IZuulFilter的shouldFilter()和run()的两个方法。其中,filterType() 即过滤器的类型,在前面已经介绍了,它有4种类型,分别是“pre'’“post”“route”和 “error”。 filterOrder()是过滤顺序,它为一个Int类型的值,值越小,越早执行该过滤器。shouldFilter()表示该过滤器是否过滤逻辑,如果为true,则执行run()方法:如果为false,则不执行run()方法。 run()方法写具体的过滤的逻辑。在本例中,检查请求的参数中是否传了token 这个参数 ,如果没有传,则请求不被路由到具体的服 务实例,直接返回响应,状态码为 401。代码如下:
public class MyFilter extends ZuulFilter {
private static Logger logger = LoggerFactory.getLogger(MyFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
Object accessToken = request.getParameter("token");
if (accessToken == null) {
logger.warn("token is empty");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
try {
ctx.getResponse().getWriter().write("token is empty");
} catch (Exception e) {
return null;
}
}
logger.info("ok");
return null;
}
}
重新启动服务,打开浏览器,访问 http://localhost:5000/hiapi/hi?name=ben,浏览器显示如下图:
再次在浏览器上输入 http://localhost:5000/hiapi/hi?name=ben&token=permit,即加上了token这个请求参数,浏览器显示如下图:
可见,MyFilter这个Bean注入IoC容器之后,对请求进行了过滤,并在请求路由转发之前进行了逻辑判断。在实际开发中,可以用此过滤器进行安全验证。本例的架构图如下图所示。
3.5 Zuul 的常见使用方式
Zuul是采用了类似于SpringMVC的DispatchServlet来实现的,采用的是异步阻塞模型, 所以性能比Ngnix差。由于Zuul和其他Netflix组件可以相互配合、无缝集成,Zuul很容易就能实现负载均衡、智能路由和熔断器等功能。在大多数情况下, Zuul都是以集群的形式存在的。由于 Zuul的横向扩展能力非常好,所以当负载过高时,可以通过添加实例来解决性能瓶颈。一种常见的使用方式是对不同的渠道使用不同的Zuul来进行路由,例如移动端共用一个Zuul网关实例,Web端用另一个Zuul网关实例,其他的客户端用另外一个Zuul实例进行路由。 这种不同的渠边用不同Zuul实例的架构如下图所示。
另外一种常见的集群是通过Ngnix和Zuul相互结合来做负载均衡。暴露在最外面的是Ngnix主从双热备进行Keepalive, Ngnix经过某种路由策略,将请求路由转发到 Zuul 集群上, Zuul 最终将请求分发到具体的服务上。架构图如下图所示。
总结:这一章节主要学习了zuul的原理,过滤器的类型,zuul结合服务发现和治理进行动态路由,以及zuul的应用场景等内容。
github源码地址