Zuul 简介
Zuul 作为微服务的网关组件,用于构建边界服务(Edge Service),致力于动态路由、过滤、监控、弹性伸缩和安全。Zuul 在微服务系统中有着重要作用:
• Zuul、Ribbon 和 Eureka 相结合,可以实现智能路由和负载均衡的功能,Zuul 能够将请求按照某种策略分发到集群中的多个服务实例。
• 网关将所有服务的 API 接口统一聚合,并统一对外暴露。外界系统调用 API 接口时,都是调用的由网关对外暴露的 API 接口,外界不需要知道微服务系统中各服务相互调用的复杂性。
• 网关服务可以做用户身份认证和权限认证,防止非法请求。
• 网关可以实现监控功能,实时输出日志,对请求进行记录。
• 网关可以用来实现流量监控,在高流量的情况下,对服务进行降级。
• API 接口从内部服务分离出来,方便做测试。
Zuul 是通过 Servlet 实现的,Zuul 通过自定义的 ZuulServlet 来对请求进行控制。Zuul 的核心是一系列过滤器,可以在 HTTP 请求的发起和响应期间执行一系列的过滤器。Zuul 包含以下 4 种过滤器:
• PRE 过滤器:它是在请求路由到具体的服务之前执行的,这种类型的过滤器可以做安全验证,例如身份验证、参数验证等。
• ROUTING 过滤器:它用于将请求路由到具体的微服务实例。在默认情况下,它是以 HTTP Client 进行网络请求。
• POST 过滤器:它是在请求已被路由到具体的微服务之后执行的。一般情况下,用作手机统计信息、指标,以及响应传输到客户端。
• ERROR 过滤器:它是在其他过滤器发生错误时执行的。
过滤器之间不能直接相互通信,而是通过 RequestContext 对象来共享数据,每个请求都会创建一个 RequestContext对象。Zuul 过滤器具有以下特性:
• Type(类型):Zuul 过滤器的类型,这个类型决定了过滤器在请求的哪个阶段起作用,例如 Pre、Post 阶段。
• Execution Order(执行顺序):Order 越小,越先执行。
• Criteria(标准):过滤器执行所需的条件。
• Action(行动):如果符合执行条件,则执行 Action(即代码逻辑)。
Zuul 的生命周期如下图:
当一个请求进入 Zuul 网关服务时,网关先进入 pre filters,进行一系列的验证,然后交给 routing filters 进行路由转发,转发到具体的微服务实例进行逻辑处理、返回数据。当具体的微服务处理完以后,最后由 post filters 进行处理,该类型的过滤器处理完成后,将 Response 返回给客户端。
ZuulServlet 是 Zuul 的核心 Servlet,它的作用是初始化 ZuulFilter,并编排这些 ZuulFilter 的执行顺序。该类中有一个 service() 方法:
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
try {
this.init((HttpServletRequest)servletRequest, (HttpServletResponse)servletResponse);
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
try {
this.preRoute();
} catch (ZuulException var12) {
this.error(var12);
this.postRoute();
return;
}
try {
this.route();
} catch (ZuulException var13) {
this.error(var13);
this.postRoute();
return;
}
try {
this.postRoute();
} catch (ZuulException var11) {
this.error(var11);
}
} catch (Throwable var14) {
this.error(new ZuulException(var14, 500, "UNHANDLED_EXCEPTION_" + var14.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
从上面的代码可知,首先执行 preRoute()方法,这个方法执行的是 PRE 类型的过滤器,如果这个方法执行时出错了,那么会执行 error(e) 和 postRoute();然后执行 route() 方法,该方法执行的是 ROUTING 类型的过滤器,最后执行 postRoute(),该方法执行的是 POST 类型的过滤器。
案列
在案例开始之前,先说明一下,在前面的例子中,我使用的 SpringBoot 的版本都是 2.1.0.RELEASE,SpringCloud 的版本都是 Finchley.RELEASE,均运行正常。但是在本例中,使用这两个版本组合,在启动网关服务时出现了如下错误:
***************************
APPLICATION FAILED TO START
***************************
Description:
The bean 'proxyRequestHelper', defined in class path resource [org/springframework/cloud/netflix/zuul/ZuulProxyAutoConfiguration$NoActuatorConfiguration.class], could not be registered. A bean with that name has already been defined in class path resource [org/springframework/cloud/netflix/zuul/ZuulProxyAutoConfiguration$EndpointConfiguration.class] and overriding is disabled.
Action:
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
经过网上搜索,了解到是版本的问题,故本例中使用的 SpringBoot 的版本降为了 2.0.6.RELEASE,SpringCloud 的版本仍然是 Finchley.RELEASE。
本例采用 Mavne 多模块结构,新建一个父项目 spring-cloud-zuul,在父项目下新建 eureka-server、service-provider、service-consumer-hi、service-consumer-hello 几个子项目,这些子项目的代码可以参考使用 Turbine 聚合监控。这里重点看一下 Zuul 网关服务。
新建一个子项目,命名为 service-zuul,其 pom 文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-zuul</artifactId>
<groupId>com.wuychn</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>service-zuul</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
程序启动类:
package com.wuychn;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableZuulProxy
@EnableEurekaClient
public class ServiceZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceZuulApplication.class, args);
}
}
application.yml:
server:
port: 9999
spring:
application:
name: service-zuul
eureka:
client:
serviceUrl:
defaultZone: http://localhost:9001/eureka/
zuul:
routes:
service-consumer-hi-api:
path: /hi/**
serviceId: service-consumer-hi
service-consumer-hello-api:
path: /hello/**
serviceId: service-consumer-hello
service-provider-api:
path: /origin/**
serviceId: service-provider
application.yml 的配置中,zuul.routes.service-consumer-hi-api.path 为 /hi/**,zuul.routes.service-consumer-hi-api.serviceId 为 service-consumer-hi,这两个配置可以将 /hi/** 开头的 Url 路由到 service-consumer-hi 服务。其中,zuul.routes.service-consumer-hi-api 中的 service-consumer-hi-api 是自己定义的。同理,以 /hello/** 开头的 Url 会被路由到 service-consumer-hello 这个服务中,以 /origin/** 开头的 Url 会被路由到 service-provider 这个服务中。
依次启动 eureka-server、service-provider、service-consumer-hi、service-consumer-hello 和 service-zuul,其中 service-provider 在 9002 和 9003 两个端口启动两个实例,在浏览器中多次访问 http://localhost:9999/hi/hi,浏览器会交替显示如下内容,
hi wuychn, I am from port:9002
hi wuychn, I am from port:9003
可见 Zuul 在路由转发时做了负载均衡。
同理,访问 http://localhost:9999/hello/hello,可以看到如下输出:
Hello, wuychn
由此可见,Zuul 确实起到了路由转发的功能,并且会结合 Ribbon 实现负载均衡。
在 Zuul 上配置熔断器
Zuul 作为 Netflix 的组件,可以与 Ribbon、Eureka 和 Hystrix 等组件结合,实现负载均衡、熔断的功能。在默认情况下,Zuul 已经和 Ribbon 结合了,实现了负载均衡的功能,下面看看如何在 Zuul 上实现熔断。
在 Zuul 中实现熔断需要实现 FallbackProvider 接口(老版本中是 ZuulFallbackProvider,参考 https://www.cnblogs.com/yjmyzz/p/8093462.html)。FallbackProvider 的源码如下:
package org.springframework.cloud.netflix.zuul.filters.route;
import org.springframework.http.client.ClientHttpResponse;
public interface FallbackProvider {
String getRoute();
ClientHttpResponse fallbackResponse(String route, Throwable cause);
}
getRoute() 方法用于指定熔断方法用于哪些路由的服务,fallbackResponse(String route, Throwable cause) 方法是进入熔断功能时执行的逻辑。
新建一个类 MyFallbackProvider,作为熔断逻辑,当服务不可用时,输出一句错误提示。代码如下:
package com.wuychn.fallback;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
@Component
public class MyFallbackProvider implements FallbackProvider {
// 指定熔断适用于哪些服务
// 如果需要所有的服务都加熔断,可以使用*通配符
@Override
public String getRoute() {
return "service-consumer-hi";
// return "*";
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
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("系统繁忙,请稍后再试!".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
重新启动 service-zull,并且关闭 service-provider 的所有实例,在浏览器上访问 http://localhost:9999/hi/hi,结果如下:
系统繁忙,请稍后再试!
由于只配置了针对 service-consumer-hi 这个服务使用熔断,所以访问 http://localhost:9999/hello/hello 会出现错误,如果相对所有服务都添加熔断,可以在 getRoute() 方法中返回 “*”。
在 Zuul 中自定义过滤器
前面介绍了过滤器的作用和种类,下面看看如何实现自定义的过滤器。实现自定义的过滤器只需要继承 ZuulFilter 接口,并实现其中的抽象方法即可。本例实现一个自定义的过滤器,用于检查请求中是否携带了 token 这个参数,如果没有传则不路由到具体的微服务实例,直接返回错误消息。
新建一个类 MyFilter,代码如下:
package com.wuychn.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
@Component
public class MyFilter extends ZuulFilter {
// 指定过滤器的类型,一共有4种类型,包括 PRE、POST、ROUTING、ERROR
@Override
public String filterType() {
return PRE_TYPE;
}
// 过滤器的执行顺序,值越小越先执行
@Override
public int filterOrder() {
return 0;
}
// 表示该过滤器是否执行过滤逻辑,为true则执行run()方法,为false不执行
@Override
public boolean shouldFilter() {
return true;
}
// 具体的过滤逻辑
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
Object accessToken = request.getParameter("token");
if (accessToken == null) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
try {
ctx.getResponse().getWriter().write("token is empty");
} catch (IOException e) {
return null;
}
}
return null;
}
}
重新启动 service-zuul 服务,访问 http://localhost:9999/hello/hello,浏览器显示:
token is empty
加上 token 参数后访问可以看到正确返回。可见,MyFilter 对请求进行了过滤,并在路由之前做了逻辑判断。在实际开发中,可以用此过滤器进行安全验证。
最后,来看看一看 service-zuul 的代码结构图: