服务网关配置:Zuul

一曲冷凌霜 提交于 2021-02-04 12:54:39


配套资料,免费下载
链接:https://pan.baidu.com/s/1la_3-HW-UvliDRJzfBcP_w
提取码:lxfx
复制这段内容后打开百度网盘手机App,操作更方便哦


第一章 Zuul介绍

1.1、什么是Zuul

通过前面内容的学习,我们已经可以基本搭建出一套简略版的微服务架构了,我们有注册中心Eureka,可以将服务注册到该注册中心中,我们有Ribbon或Feign或OpenFegin可以实现对服务负载均衡地调用,我们有Hystrix可以实现服务的熔断、降级以及限流以及Dashboard和Turbine来进行服务调用监控。

Zuul是Spring Cloud全家桶中的微服务API网关,所有从移动设备或网站来的请求都会先经过Zuul的API网关然后才能到达后端的Netflix应用程序,作为一个边界性质的应用程序,Zuul提供了动态路由、监控、弹性负载和安全控制等功能。那么Spring Cloud这个一站式的微服务开发框架基于Netflix Zuul实现了Spring Cloud Zuul,采用Spring Cloud Zuul即可实现一套API网关服务。

1.2、为啥用Zuul

如果我们的微服务中有很多个独立服务都要对外提供服务,那么我们要如何去管理这些接口?特别是当项目非常庞大的情况下要如何管理?

在微服务中,一个独立的系统被拆分成了很多个独立的服务,为了确保安全,权限管理也是一个不可回避的问题,如果在每一个服务上都添加上相同的权限验证代码来确保系统不被非法访问,那么工作量也就太大了,而且维护也非常不方便。

为了解决上述问题,微服务架构中提出了API网关的概念,它就像一个安检站一样,所有外部的请求都需要经过它的调度与过滤,然后API网关来实现请求路由、负载均衡、权限验证等功能。

Zuul包含了对请求的路由和过滤两个最主要的功能:其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础,过滤功能则负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础。

Zuul底层利用各种filter实现如下功能:

  • 认证和安全:识别每个需要认证的资源,拒绝不符合要求的请求。
  • 性能监测:在服务边界追踪并统计数据,提供精确的生产视图。
  • 动态路由:根据需要将请求动态路由到后端集群。
  • 压力测试:逐渐增加对集群的流量以了解其性能。
  • 负载卸载:预先为每种类型的请求分配容量,当请求超过容量时自动丢弃。
  • 静态资源处理:直接在边界返回某些响应。

第二章 Zuul路由功能

2.1、项目准备与启动

我们接下来的所有操作均是在Hystrix最后完成的工程上进行操作,相关代码请到配套资料中寻找。

我们打开项目以后,需要对项目进行启动,启动的顺序如下:

  1. eureka-server7001(启动会报错,暂时不用理会,因为第二个注册中心还没有启动,等第二个注册中心启动,过一会就恢复了)
  2. eureka-server7002
  3. service-provider8001
  4. service-provider8002
  5. service-consumer9002
  6. service-consumer9003

注意:有时候你确信自己的代码没有问题,可是效果就是出不来,很有可能是idea编译的缓存的问题,我们目前使用的热部署还存在一些问题,要想解决,我建议你可以先停止对应工程,删除对应工程中的target目录,然后手动启动,这样还不行的话,大概率是你某地方配置错了,在检查一下吧^__^

2.2、工程搭建与测试

(1)在父工程spring-cloud-study下创建子工程gateway-zuul5001

(2)在pom.xml中导入以下依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>

(3)新建application.yaml,在application.yaml中加入以下配置

server:
  port: 5001

spring:
  application:
    name: gateway-zuul5001

eureka:
  client:
    #是否将自己注册到注册中心,默认为 true
    register-with-eureka: false
    #表示 Eureka Client 间隔多久去服务器拉取注册信息,默认为 30 秒
    registry-fetch-interval-seconds: 10
    #设置服务注册中心地址
    service-url:
      defaultZone: http://root:123456@eureka-server7001.com:7001/eureka/,http://root:123456@eureka-server7002.com:7002/eureka/

(4)新建启动类,在启动类中加入以下代码

@SpringBootApplication
@EnableZuulProxy
public class GatewayZuul5001Application {
   
    
    public static void main(String[] args) {
   
    
        SpringApplication.run(GatewayZuul5001Application.class);
    }
}

(5)启动当前项目,这样简单的Zuul就搭建好了,我们可以通过默认的路由规则来访问指定的服务方法,比如:

格式:“http://”+Zuul网关的域名+":"+Zuul网关的端口+"/"+微服务的名称(一定小写)+微服务的服务路径(就是你Controller方法上标注的那个路径)

例如:http://localhost:5001/service-consumer9002/consumer/product/findAll

例如:http://localhost:5001/service-consumer9003/consumer/product/findAll

2.3、配置自定义路由

现在,我们已经基本实现了Zuul默认路由的功能,但是,一般我们也可以自定义路由配置,为什么要自定义,细心的你会发现,现在你访问指定的服务,必须要加注册服务的名称(例如:service-consumer9002service-consumer9003),这个名称可能很长,也可能会暴露你这个服务的一些性质,我们想要给这个服务起个别名,来代替当前这个默认的规则,就必须使用自定义路由配置,自定义路由配置其实很简单,只需要在gateway-zuul5001中的application.yaml加上相对应的路由配置就行了,具体代码如下所示:

zuul:
  routes:
    #这个属性Key可以随便写(一般来说就是注册服务的名称,属性spring.application.name)
    SERVICE-CONSUMER9002:
      #你要映射的路径地址(/**代表后边可以有多级路径,/*只有一级路径)
      path: /consumer9002/**
      #准备转给哪一个服务(不知道的,可以去Eureka注册中心找)
      serviceId: SERVICE-CONSUMER9002

    #这个属性Key可以随便写(一般来说就是注册服务的名称,属性spring.application.name)
    SERVICE-CONSUMER9003:
      #你要映射的路径地址(/**代表后边可以有多级路径,/*只有一级路径)
      path: /consumer9003/**
      #准备转给哪一个服务(不知道的,可以去Eureka注册中心找)
      serviceId: SERVICE-CONSUMER9003

如果你还有更多服务你还可以照着上边的规则继续往下写(服务提供者和服务消费者都算服务),写完后,请重新启动当前的项目,然后依次访问如下地址测试:

地址1:http://localhost:5001/consumer9002/consumer/product/findAll

地址2:http://localhost:5001/consumer9003/consumer/product/findAll

2.4、禁止默认的路由

虽然实现了自定义路由设置,但是如果你使用之前默认的路由规则,他还是可以访问的,我们想要禁用掉,默认的那个路由规则,只需要增加一段配置,如下:

zuul:
  #加入这个配置,代表忽略所有服务,也就是忽略默认的路由规则,你也可以单独指定某个服务不能使用服务名来访问
  #这个虽然是代表禁用掉所有,但是,自定义的路由规则还是会生效的,不必担心
  ignored-services: '*'
  routes:
    #这个属性Key可以随便写(一般来说就是注册服务的名称,属性spring.application.name)
    SERVICE-CONSUMER9002:
      #你要映射的路径地址(/**代表后边可以有多级路径,/*只有一级路径)
      path: /consumer9002/**
      #准备转给哪一个服务(不知道的,可以去Eureka注册中心找)
      serviceId: SERVICE-CONSUMER9002

    #这个属性Key可以随便写(一般来说就是注册服务的名称,属性spring.application.name)
    SERVICE-CONSUMER9003:
      #你要映射的路径地址(/**代表后边可以有多级路径,/*只有一级路径)
      path: /consumer9003/**
      #准备转给哪一个服务(不知道的,可以去Eureka注册中心找)
      serviceId: SERVICE-CONSUMER9003

然后重新启动项目即可,启动后,输入以下地址进行测试:

地址1:http://localhost:5001/service-consumer9002/consumer/product/findAll

地址2:http://localhost:5001/service-consumer9003/consumer/product/findAll

2.5、简化自定义路由

虽然自定义路由功能很强大,可以映射指定路径,但是你不觉得写起来太麻烦了吗?有没有一种简化的写法,那自然是有的,你现在可以把之前的那一大段配置注释掉了,我们来看看全新的精炼的配置到底怎么配,如下:

zuul:
  ignored-services: '*'
  routes:
    #配置路由规则,key代表服务名称,value代表映射规则
    SERVICE-CONSUMER9002: /consumer9002/**
    SERVICE-CONSUMER9003: /consumer9003/**

写完后,请重新启动当前的项目,然后依次访问如下地址测试:

地址1:http://localhost:5001/consumer9002/consumer/product/findAll

地址2:http://localhost:5001/consumer9003/consumer/product/findAll

2.6、统一加路径前缀

如果你想要在所有请求前边加一个统一前缀,比如:/api,Zuul也支持这种设置,配置如下:

zuul:
  #统一添加路由前缀
  prefix: /api
  ignored-services: '*'
  routes:
    #配置路由规则,key代表服务名称,value代表映射规则
    SERVICE-CONSUMER9002: /consumer9002/**
    SERVICE-CONSUMER9003: /consumer9003/**

写完后,请重新启动当前的项目,然后依次访问如下地址测试:

地址1:http://localhost:5001/api/consumer9002/consumer/product/findAll

地址2:http://localhost:5001/api/consumer9003/consumer/product/findAll

2.7、路由规则通配符

通配符 含义 举例 说明
? 匹配任意单个字符 /api/consumer9003/? /api/consumer9003/a
/api/consumer9003/b
/api/consumer9003/c
以上三个路由规则都可以被匹配到


* 匹配任意多个字符
只能匹配一级地址
/api/consumer9003/* /api/consumer9003/aaa
/api/consumer9003/bbb
/api/consumer9003/ccc
以上三个路由规则都可以被匹配到
/api/consumer9003/a/b/c
以上一个路由规则不可以被匹配到




** 匹配任意多个字符
可以匹配多级地址
/api/consumer9003/** /api/consumer9003/aaa
/api/consumer9003/bbb
/api/consumer9003/ccc
以上三个路由规则都可以被匹配到
/api/consumer9003/a/b/c
以上一个路由规则也可以被匹配到




第三章 Zuul过滤功能

3.1、过滤器介绍

过滤器 (filter) 是Zuul的核心组件,Zuul大部分功能都是通过过滤器来实现的。

3.2、过滤器类型

Zuul中定义了4种标准过滤器类型,这些过滤器类型对应于请求的典型生命周期。

  • pre:这种过滤器在请求被路由之前调用。可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
  • routing:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或 Netfilx Ribbon请求微服务。
  • post:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
  • error:在其他阶段发生错误时执行该过滤器。

3.3、过滤器使用场景

  • 请求鉴权:一般放在pre类型,如果发现没有访问权限,直接就拦截了。
  • 异常处理:一般会在error类型和post类型过滤器中结合来处理。
  • 服务调用时长统计:pre和post结合使用。

3.4、过滤器生命周期

  • 正常流程:
    • 请求到达首先会经过pre类型过滤器,而后到达routing类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达post过滤器。而后返回响应。
  • 异常流程:
    • 整个过程中,pre或者routing过滤器出现异常,都会直接进入error过滤器,再error处理完毕后,会将请求交给post过滤器,最后返回给用户。
    • 如果是error过滤器自己出现异常,最终也会进入POST过滤器,而后返回。
    • 如果是post过滤器出现异常,会跳转到error过滤器,但是与pre和routing不同的时,请求不会再到达post过滤器了。

3.5、内置过滤器列表

3.6、自定义的过滤器

新建过滤器:com.caochenlei.filter.MyLogFilter

/**
 * 这个过滤器专门用来记录日志的
 *
 * @author CaoChenLei
 */
//标注当前过滤器是一个组件,需要被Spring管理,必须有
@Component
public class MyLogFilter extends ZuulFilter {
   
    
    //用来指定当前这个过滤器的执行类型,可以写字符串也可以写枚举值
    @Override
    public String filterType() {
   
    
        return FilterConstants.ROUTE_TYPE;
    }

    //用来指定当前这个过滤器的执行顺序,可以写数字也可以写枚举值
    @Override
    public int filterOrder() {
   
    
        return FilterConstants.PRE_DECORATION_FILTER_ORDER;
    }

    //用来指定当前这个过滤器是否执行,可以直接写死true/fasle,也可以自行判断
    @Override
    public boolean shouldFilter() {
   
    
        return true;
    }

    //用来指定当前这个过滤器的执行逻辑,这个return null没有什么意义,但是不能省略
    @Override
    public Object run() throws ZuulException {
   
    
        //获取当前请求的上下文对象
        RequestContext currentContext = RequestContext.getCurrentContext();
        //获取当前请求的请求对象
        HttpServletRequest request = currentContext.getRequest();
        //输出自定义的语句信息,也可以直接保存到数据库,这里只是测试
        System.out.println("===============日志记录开始===============");
        System.out.println("访问地址:" + request.getRequestURI());
        System.out.println("===============日志记录结束===============");
        return null;
    }
}

编写好自己的过滤器以后,重新启动当前这个项目,然后访问:http://localhost:5001/api/consumer9002/consumer/product/findAll

3.7、禁用指定过滤器

我们想要禁用某一个过滤器,只需要按照配置规则进行配置就好了,这里我们就禁用掉自己编写的过滤器,配置如下:

zuul:
  #统一添加路由前缀
  prefix: /api
  ignored-services: '*'
  routes:
    #配置路由规则,key代表服务名称,value代表映射规则
    SERVICE-CONSUMER9002: /consumer9002/**
    SERVICE-CONSUMER9003: /consumer9003/**
  #禁用指定的过滤器
  MyLogFilter:
      route: #代表过滤器类型
        disable: true

编写好自己的过滤器以后,重新启动当前这个项目,然后访问:http://localhost:5001/api/consumer9002/consumer/product/findAll

我们发现控制台并没有打印我们自己定义的日志信息。

3.8、处理全局的异常

Spring Cloud Zuul 对异常的处理是非常方便的,我们已经学过了在pre、routing、post的任意一个阶段如果抛异常了,则执行error过滤器,如果你想要统一处理Zuul内部出现的异常,Zuul内部帮我们处理了,但是它返回的是一个白页,我们也可以自己统一处理异常,我们只需要定义一个类型为error的过滤器替换掉默认的SendErrorFilter就能处理异常了,在处理的时候,可以使用json来统一返回错误信息,这样我们就看不到Spring Boot默认的错误白页了。

(1)禁用Zuul自带的异常过滤器,配置如下:

zuul:
  #统一添加路由前缀
  prefix: /api
  ignored-services: '*'
  routes:
    #配置路由规则,key代表服务名称,value代表映射规则
    SERVICE-CONSUMER9002: /consumer9002/**
    SERVICE-CONSUMER9003: /consumer9003/**
  #禁用指定的过滤器
  MyLogFilter:
      route: #代表过滤器类型
        disable: true
  #禁用Zuul自带的异常过滤器
  SendErrorFilter:
      error: #代表过滤器类型
        disable: true

(2)新建过滤器:com.caochenlei.filter.MyErrorFilter,然后替换掉自带的过滤器

@Component
public class MyErrorFilter extends ZuulFilter {
   
    
    @Override
    public String filterType() {
   
    
        return FilterConstants.ERROR_TYPE;
    }

    @Override
    public int filterOrder() {
   
    
        return FilterConstants.SEND_ERROR_FILTER_ORDER;
    }

    @Override
    public boolean shouldFilter() {
   
    
        return true;
    }

    @Override
    public Object run() throws ZuulException {
   
    
        try {
   
    
            RequestContext context = RequestContext.getCurrentContext();
            ZuulException exception = (ZuulException) context.getThrowable();
            HttpServletResponse response = context.getResponse();
            response.setContentType("application/json; charset=utf8");
            response.setStatus(exception.nStatusCode);
            PrintWriter writer = response.getWriter();
            writer.print("{\"code\":" + exception.nStatusCode + ",\"message\":\"" + exception.getMessage() + "\"}");
            writer.close();
        } catch (Exception e) {
   
     e.printStackTrace(); }
        return null;
    }
}

(3)再次新建一个过滤器(com.caochenlei.filter.MyThrowExceptionFilter),这个过滤器不做别的事情,就是在run中抛出一个异常,看看,咱们定义的全局过滤器能不能拦截到他:

@Component
public class MyThrowExceptionFilter extends ZuulFilter {
   
    
    @Override
    public String filterType() {
   
    
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
   
    
        return 0;
    }

    @Override
    public boolean shouldFilter() {
   
    
        return true;
    }

    @Override
    public Object run() {
   
    
        //模拟出现异常
        int i = 1 / 0;
        return null;
    }
}

(4)编写好自己的过滤器以后,重新启动当前这个项目,然后访问:http://localhost:5001/api/consumer9002/consumer/product/findAll

(5)既然可以处理自己定义过滤器的异常,那我们就没必要在留着MyThrowExceptionFilter过滤器了,将网关恢复正常,也就是禁止掉MyThrowExceptionFilter,具体的配置如下:

zuul:
  #统一添加路由前缀
  prefix: /api
  ignored-services: '*'
  routes:
    #配置路由规则,key代表服务名称,value代表映射规则
    SERVICE-CONSUMER9002: /consumer9002/**
    SERVICE-CONSUMER9003: /consumer9003/**
  #禁用指定的过滤器
  MyLogFilter:
      route: #代表过滤器类型
        disable: true
  #禁用Zuul自带的异常过滤器
  SendErrorFilter:
      error: #代表过滤器类型
        disable: true
  #禁用掉自定义的异常过滤器
  MyThrowExceptionFilter:
      pre:
        disable: true

(6)编写好自己的过滤器以后,重新启动当前这个项目,然后访问:http://localhost:5001/api/consumer9002/consumer/product/findAll

好了,到这里,过滤器这部分相信你已经学会了,开始学习其他部分了。

第四章 Zuul其他功能

4.1、负载均衡超时

Zuul中默认就已经集成了Ribbon负载均衡和Hystix熔断机制。但是所有的超时策略都是走的默认值,比如熔断超时时间只有1S,很容易就触发了。

因此建议我们手动进行配置:

zuul:
  #开启重试功能
  retryable: true
  #统一添加路由前缀
  prefix: /api
  ignored-services: '*'
  routes:
    #配置路由规则,key代表服务名称,value代表映射规则
    SERVICE-CONSUMER9002: /consumer9002/**
    SERVICE-CONSUMER9003: /consumer9003/**
  #禁用指定的过滤器
  MyLogFilter:
      route: #代表过滤器类型
        disable: true
  #禁用Zuul自带的异常过滤器
  SendErrorFilter:
      error: #代表过滤器类型
        disable: true
  #禁用掉自定义的异常过滤器
  MyThrowExceptionFilter:
      pre:
        disable: true

ribbon:
  ConnectTimeout: 500                  # 连接超时时间(ms)
  ReadTimeout: 2000                    # 通信超时时间(ms),超时时长不能超过hystrix的熔断时长
  OkToRetryOnAllOperations: true       # 是否对所有操作重试
  MaxAutoRetriesNextServer: 2          # 同一服务不同实例的重试次数
  MaxAutoRetries: 1                    # 同一实例的重试次数

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMillisecond: 6000  # 熔断超时时长:6000ms

ConnectTimeout(连接超时时长) + ReadTimeout(读取超时时长) 是2500,但是ribbon会进行一次重试,那么默认就是(ConnectTimeout+ReadTimeout) * 2 是真正的总超时时长,所以timeoutInMillisecond熔断的时长必须要超过这个时间,否则会报错或者把ribbon的MaxAutoRetries设为0,那么就不进行重试了。

4.2、服务降级熔断

Zuul本身就是一个代理服务,但如果被代理的服务突然断了,这个时候Zuul上面会有出错信息,例如,停止了被调用的微服务。一般服务方自己会进行服务的熔断降级,但对于Zuul本身,也应该进行Zuul的降级处理,我们需要有一个Zuul的降级,实现如下:

com.caochenlei.fallback.ZullFallback

@Component
public class ZullFallback implements FallbackProvider {
   
    
    @Override
    public String getRoute() {
   
    
        //对所有服务都降级,这里可以写指定路由
        return "*";
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
   
    
        //如果不知道该导入哪一个包,请参考配套代码...
        return new ClientHttpResponse() {
   
    
            @Override
            public HttpHeaders getHeaders() {
   
    
                HttpHeaders headers = new HttpHeaders();
                headers.set("Content-Type", "text/html; charset=UTF-8");
                return headers;
            }

            @Override
            public InputStream getBody() throws IOException {
   
    
                // 响应体
                return new ByteArrayInputStream("服务正在维护,请稍后再试.".getBytes());
            }

            @Override
            public HttpStatus getStatusCode() throws IOException {
   
    
                return HttpStatus.BAD_REQUEST;
            }

            @Override
            public int getRawStatusCode() throws IOException {
   
    
                return HttpStatus.BAD_REQUEST.value();
            }

            @Override
            public String getStatusText() throws IOException {
   
    
                return HttpStatus.BAD_REQUEST.getReasonPhrase();
            }

            @Override
            public void close() {
   
    
                //此处可以不用处理...
            }
        };
    }
}

重新启动当前这个项目,然后我们关闭service-consumer9002服务,然后访问:http://localhost:5001/api/consumer9002/consumer/product/findAll

4.3、网关缓存问题

默认情况下,所有的请求经过zuul网关的代理,默认会通过SpringMVC预先对请求进行处理缓存,普通请求并不会有什么影响,但是对于文件上传,就会造成不必要的网络负担,在高并发时,可能导致网络阻塞,Zuul网关不可用,这样我们的整个系统就瘫痪了。所以,我们上传文件的请求需要绕过请求的缓存,直接通过路由到达目标微服务,简单的意思就是在url前面加上 “/zuul” ,那么就会跳过缓存。如果,你上传文件的地址还用到了ribbon的负载均衡器,那么,你应该调大超时时间,否则也会出问题。

ribbon:
  ConnectTimeout: 300
  ReadTimeout: 60000

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMillisecond: 60000

我们需要在当前访问路径api前面加上/zuul,例如:http://localhost:5001/zuul/api/upload-service/upload/image

如果你使用了nginx对zuul网关进行了负载均衡,你可以对路径进行重写处理,我们需要修改到以/zuul为前缀,可以通过nginx的rewrite指令实现这一需求。

location /api/upload-service/ {
   
    
    rewrite "^/(.*)$" /zuul/$1 ;
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!