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_微服务

当一个请求进入 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,参考 )。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 的代码结构图:

网关后端架构图 网关zuul_ide_02