《深入理解 Spring Cloud 与微服务构建》第十章 路由网关 Spring Cloud Zuul
文章目录
- 《深入理解 Spring Cloud 与微服务构建》第十章 路由网关 Spring Cloud Zuul
- 一、Zuul 简介
- 二、Zuul 的工作原理
- 三、案例实战
- 1.搭建 Zuul 服务
- 2.在 Zuul 上配置 API 接口的版本号
- 3.在 Zuul 上配置熔断器
- 4.在 Zuul 中使用过滤器
- 5.Zuul 的常见使用方式
一、Zuul 简介
Zuul 作为微服务系统的网管组件,用于构建边界服务(Edge Service),致力于动态路由、过滤、监控、弹性伸缩和安全。Zuul 在微服务架构中的重要作用主要体现在以下 6 个方面:
- Zuul、Ribbon 以及 Eureka 相结合,可以实现智能路由和负载均衡的功能,Zuul 能够将请求流量按某种策略分发到集群状态的多个服务实例
- 网关将所有服务的 API 接口统一聚合,并统一对外暴露。外界系统调用 API 接口时,都是由网关对外暴露的 API 接口,外界系统不需要知道微服务系统中各服务相互调用的复杂性。微服务系统也保护了其内部微服务单元的 API 接口,防止其被外界直接调用,导致服务的敏感信息对外暴露
- 网关服务可以做到用户身份认证和权限认证,防止非法请求操作 API 接口,对服务器起到保护作用
- 网关可以实现监控功能,实时日志输出,对请求进行记录
- 网关可以用来实现流量监控,在高流量的情况下,对服务进行降级
- API 接口从内部服务分离出来,方便做测试
二、Zuul 的工作原理
Zuul 是通过 Servlet 来实现的,Zuul 通过自定义的 ZuulServlet(类似于 Spring MVC 的 DispatchServlet)来对请求进行控制。Zuul 的核心是一系列过滤器,可以在 HTTP 请求的发起和响应期间执行一系列的过滤器。Zuul 包括以下 4 种过滤器:
- PRE 过滤器:它是在请求路由到具体的服务之前执行的,这种类型的过滤器可以做安全验证,例如身份验证、参数验证等
- ROUTING 过滤器:它用于将请求路由到具体的微服务实例。在默认情况下,它使用 Http Client 进行网络请求
- POST 过滤器:它是在请求已被路由到微服务后执行的。一般情况下,用作收集统计信息、指标,以及将响应传输到客户端
- ERROR 过滤器:它是在其它过滤器发生错误时执行的
Zuul 采取了动态读取、编译和运行这些过滤器。过滤器之间不能直接相互通信,而是通过 RequestContext 对象来共享数据,每个请求都会创建一个 RequestContext 对象。Zuul 过滤器具有以下关键特性:
- Type(类型):Zuul 过滤器的类型,这个类型决定了过滤器在请求的那个阶段起作用,例如 Pre、Post 阶段等
- Execution Order(执行顺序):规定了过滤器的执行顺序,Order 的值越小,越先执行
- Criteria(标准):过滤器执行所需的条件
- Action(行动):如果服务执行条件,则执行 Action(即逻辑代码)
Zuul 请求的生命周期如图所示:
当一个客户端 Request 请求进入 Zuul 网关服务时,网关先进入 “pre filter”,进行一系列的验证、操作或者判断。然后交给 “routing filter” 进行路由转发,转发到具体的服务实例进行逻辑处理、返回数据。当具体的服务处理完后,最后由 “post filter” 进行处理,该类型的处理器处理完之后,将 Response 信息返回给客户端
ZuulServlet 是 Zuul 的核心 Servlet。ZuulServlet 的作用是初始化 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 类型过滤器的逻辑
三、案例实战
1.搭建 Zuul 服务
本案例基于上一章案例的基础上进行搭建,新建一个 Spring Boot 工程,取名为 eureka-zuul-client,在 pom 文件中引入相关依赖,包括继承了主 Maven 工程的 pom 文件,引入 Eureka Client 的起步依赖 spring-cloud-starter-netflix-eureka-client、Zuul 的起步依赖 spring-cloud-starter-netflix-zuul、Web 功能的起步依赖 spring-boot-starter-web,以及 Spring Boot 测试的起步依赖 spring-boot-starter-test。代码如下:
<?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>Eureka</artifactId>
<groupId>org.sisyphus</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>eureka-zuul-client</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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
</project>
在程序的启动类 EurekaZuulClientApplication 加上 @EnableEurekaClient 注解,开启 EurekaClient 的功能;加上 @SpringBootApplication 注解,表明自己是一个 Spring Boot 工程;加上 @EnableZuulProxy 注解,开启 Zuul 的功能。代码如下:
package com.sisyphus;
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;
@EnableZuulProxy
@EnableEurekaClient
@SpringBootApplication
public class EurekaZuulClientApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaZuulClientApplication.class, args);
}
}
在工程的配置文件 application.yml 中做相关的配置,包括配置服务注册中心的地址为 http://localhost:8761/eureka,程序的端口号为 5000,程序名为 service-zuul
在本案例中,zuul.routes.hiapi.path 为 “/hiapi/**”,zuul.routes.hiapi.serviceId 为 “eureka-client”,这两个配置就可以将以 “/hiapi” 开头的 Url 路由到 eureka-client 服务。其中,zuul.routes.hiapi 中的 “hiapi” 是自己定义的,需要指定它的 path 和 serviceId,两者配合使用,就可以将指定类型的请求 Url 路由到指定的 ServiceId。同理,满足以 “/ribbonapi” 开头的请求 Url 都会被分发到 eureka-ribbon-client,满足以 “/feignapi” 开头的请求 Url 都会被分发到 eureka-feign-client 服务。如果某服务存在多个实例,Zuul 结合 Ribbon 会做负载均衡,将请求均分的部分路由到不同的服务实例
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
server:
port: 5000
spring:
application:
name: service-zuul
zuul:
routes:
hiapi:
path: /hiapi/**
serviceId: eureka-ribbon-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 启动两个实例,端口为 8762 和 8763。在浏览器上多次访问 http://localhost:5000/hiapi/hi?name=sisyphus,浏览器会交替显示以下内容:
hi sisyphus,i am from port:8762
hi sisyphus,i am from port:8763
可见 Zuul 在路由转发做了负载均衡。同理,多次访问 http://localhost:5000/feignapi/hi?name=sisyphus 和 http://localhost:5000/ribbonapi/hi?name=sisyphus,也可以看到相似的内容
如果不需要用 Ribbon 做负载均衡,可以指定服务实例的 Url,用 zuul.routes.hiapi.url 配置指定,这时就不需要配置 zuul.routes.hiapi.serviceId 了。一旦指定了 Url,Zuul 就不能做负载均衡了,而是直接访问指定的 Url,在实际的开发中这种做法是不可取的。修改配置的代码如下:
zuul:
routes:
hiapi:
path: /hiapi/**
url: http://localhost:8762
重新启动 eureka-zuul-service 服务,请求 http://localhost:5000/hiapi/hi?name=sisyphus,浏览器只会显示以下内容:
hi sisyphus,i am from port:8762
如果你想指定 Url,并且想做负载均衡,那么就需要自己维护负载均衡的服务注册列表。首先,将 ribbon.eureka.enable 改为 false,即 Ribbon 负载均衡客户端不向 Eureka Client 获取服务注册列表信息。然后需要自己维护一份注册列表,该注册列表对应的服务名为 hiapi-v1(这个名字可自定义),通过配置 hiapi-v1.ribbon.listOfServers 来配置多个负载均衡的 Url。代码如下:
zuul:
routes:
hiapi:
path: /hiapi/**
serviceId: hiapi-v1
ribbon:
eureka:
enabled: false
hiapi-v1:
ribbon:
listOfServers: http://localhost:8762,http://localhost:8763
重新启动 eureka-zuul-service 服务,在浏览器上访问 http://localhost:5000/hiapi/hi?name=sisyphus,浏览器会交替显示如下内容:
hi sisyphus,i am from port:8762
hi sisyphus,i am from port:8763
2.在 Zuul 上配置 API 接口的版本号
如果想给每一个服务的 API 接口加前缀,例如 http://localhost:5000/v1/hiapi/hi?name=sisyphus/,即在所有的 API 接口上加一个 v1 作为版本号。这时需要用到 zuul.prefix 的配置,配置示例代码如下:
zuul:
routes:
hiapi:
path: /hiapi/**
serviceId: hiapi-v1
ribbonapi:
path: /ribbonapi/**
serviceId: eureka-ribbon-client
feignapi:
path: /feignapi/**
serviceId: eureka-feign-client
prefix: /v1
重新启动 eureka-zuul-service 服务,在浏览器上访问 http://localhost:5000/v1/hiapi/hi?name=sisyphus,浏览器会交替显示:
hi sisyphus,i am from port:8762
hi sisyphus,i am from port:8763
3.在 Zuul 上配置熔断器
Zuul 作为 Netflix 组件,可以与 Ribbon、Eureka 和 Hystrix 等组件相结合,实现负载均衡、熔断器的功能。在默认情况下,Zuul 和 Ribbon 相结合,实现了负载均衡的功能。下面来讲解如何在 Zuul 上实现熔断功能
在 Zuul 中实现熔断功能需要实现 FallbackProvider 的接口。实现该接口有两个方法,一个是 getRoute() 方法,用于指定熔断功能应用于哪些路由的服务;另一个方法 fallbackResponse() 为进入熔断功能时执行的逻辑。ZuulFallbackProvider 的源码如下:
public interface FallbackProvider {
String getRoute();
ClientHttpResponse fallbackResponse(String route, Throwable cause);
}
实现一个针对 eureka-client 服务的熔断器,当 eureka-client 的服务出现故障时,进入熔断逻辑,向浏览器输入一句错误提示
package com.fallbackProvider;
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 "eureka-client";
}
@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("oooops!error!i am the fallback.".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
重新启动 eureka-zuul-client 工程,并且关闭 eureka-client 的所有实例,在浏览器上访问 http://localhost:5000/hiapi/hi?name=sisyphus,浏览器会显示:
oooops!error!i am the fallback.
如果需要所有的路由服务都加熔断功能,只需要在 getRoute() 方法上返回 “*” 的匹配符,代码如下:
@Override
public String getRoute() {
return "*";
}
4.在 Zuul 中使用过滤器
在前面的章节讲述了过滤器的作用和种类,下面来讲解如何实现一个自定义的过滤器。实现过滤器很简单,只需要继承 ZuulFilter,并实现 ZuulFilter 中的抽象方法,包括 filterType() 和 filterOrder(),以及 IZuulFilter 的 shouldFilter,并实现 ZuulFilter 中的抽象方法,包括 filterType() 和 filterOrder(),以及 ZuulFilter 的 shouldFilter() 和 Object run() 的两个方法。其中,filterType() 即过滤器的类型,有 4 中类型,分别是 “pre”、“post”、“routing” 和 “error”。filterOrder() 是过滤顺序,它是一个 Int 类型的值,值越小,越早执行该过滤器。shouldFilter() 表示该过滤器是否过滤逻辑,如果为 true,则执行 run() 方法;如果为 false,则不执行 run() 方法。run() 方法写具体的过滤的逻辑。在本例中,检查请求的参数中是否穿了 token 这个参数,如果没有传,则请求不被路由到具体的服务实例,直接返回响应,状态码为 401.代码如下:
package com.sisyphus.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 org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
@Component
public class MyFilter extends ZuulFilter {
private static Logger log = LoggerFactory.getLogger(MyFilter.class);
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@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){
log.warn("token is empty");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
try{
ctx.getResponse().getWriter().write("token is empty");
}catch (Exception e){
return null;
}
}
log.info("ok");
return null;
}
}
重新启动服务,打开浏览器,访问 http://localhost:5000/hiapi/hi?name=sisyphus,浏览器显示:
token is empty
再次在浏览器上输入 http://localhost:5000/hiapi/hi?name=sisyphus&token=token,即加上了 token 这个请求参数,浏览器显示:
hi sisyphus,i am from port:8762
可见,MyFilter 这个 Bean 注入 IoC 容器之后,对请求进行了过滤,并在请求路由转发之前进行了逻辑判断。在实际开发中,可以用此过滤器进行安全验证
5.Zuul 的常见使用方式
Zuul 是采用了类似于 Spring MVC 的 DispatchServlet 来实现的,采用的是异步阻塞模型,所以性能比 Nginx 差。由于 Zuul 和其它 Netflix 组件可以相互配合、无缝集成,Zuul 很容易就能实现负载均衡、智能路由和熔断器等功能。在大多数情况下,Zuul 都是以集群的形式存在的。由于 Zuul 的横向扩展能力非常好,所以当负载过高时,可以通过添加实例来解决性能瓶颈
一种常见的使用方式是对不同的渠道使用不同的 Zuul 来进行路由,例如移动端用一个 Zuul 网关实例,WEB 端用一个 Zuul 网关实例,其它的客户端用一个 Zuul 实例进行路由
另一种常见的集群是通过 Nginx 和 Zuul 相互结合来做负载均衡。暴露在最外面的是 Nginx 主从双热备进行 Keepalive,Nginx 经过某种路由策略,将请求路由转发到 Zuul 集群上,Zuul 最终将请求分发到具体的服务上