Zuul服务网关二个功能请求的路由和过滤器使用

Zuul服务网关

1.理解Zuul

​ Zuul 是从设备和网站到应用程序后端的所有请求的前门。作为边缘服务应用程序,Zuul 旨在实现动
态路由,监视,弹性和安全性。Zuul 包含了对请求的路由和过滤两个最主要的功能。

Zuul 是 Netflix 开源的微服务网关,它可以和 Eureka、Ribbon、Hystrix 等组件配合使用。Zuul 的核心是一系列的过滤器,这些过滤器可以完成以下功能

  • 审查与监控:在边缘位置追踪有意义的数据和统计结果,从而带来精确的生产试图
  • 压力测试:逐渐增加只想集群的流量,以了解性能
  • 静态响应处理:在边缘位置直接建立部份响应,从而避免其转发到内部集群\
  • 化,以及让系统的边缘更贴近系统的使用者
  • 身份认证与安全:识别每个资源的验证要求,并拒绝那些与要求不符的请求
  • 动态路由:动态地将请求路由到不同的后端集群
  • 负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求
  • 多区域弹性:跨越AWS Region进行请求路由,旨在实现ELB(Elastic Load Balancing)使用的多样
2.什么是服务网关

API Gateway(APIGW / API 网关),顾名思义,是出现在系统边界上的一个面向 API 的、串行集中式的强管控服务,这里的边界是企业 IT 系统的边界,可以理解为 企业级应用防火墙 ,主要起到 隔离外部访问与内部系统的作用 。在微服务概念的流行之前,API 网关就已经诞生了,例如银行、证券等领域常见的前置机系统,它也是解决访问认证、报文转换、访问统计等问题的。

API 网关是一个服务器,是系统对外的唯一入口。API 网关封装了系统内部架构,为每个客户端提供
定制的 API。所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有非业务功能。API
网关并不是微服务场景中必须的组件,如下图,不管有没有 API 网关,后端微服务都可以通过 API 很好
地支持客户端的访问

但对于服务数量众多、复杂度比较高、规模比较大的业务来说,引入 API 网关也有一系列的好处

  • 聚合接口使得服务对调用者透明,客户端与后端的耦合度降低
  • 聚合后台服务,节省流量,提高性能,提升用户体验
  • 提供安全、流控、过滤、缓存、计费、监控等 API 管理功能
3.为什么要使用网关

**单体应用:**浏览器发起请求到单体应用所在的机器,应用从数据库查询数据原路返回给浏览器,对
于单体应用来说是不需要网关的。
**微服务:**微服务的应用可能部署在不同机房,不同地区,不同域名下。此时客户端(浏览器/手机/
软件工具)想要请求对应的服务,都需要知道机器的具体 IP 或者域名 URL,当微服务实例众多
时,这是非常难以记忆的,对于客户端来说也太复杂难以维护。此时就有了网关,客户端相关的请
求直接发送到网关,由网关根据请求标识解析判断出具体的微服务地址,再把请求转发到微服务实
例。这其中的记忆功能就全部交由网关来操作了。
在这里插入图片描述

总结

如果让客户端直接与各个微服务交互:

  • 客户端会多次请求不同的微服务,增加了客户端的复杂性
  • 存在跨域请求,在一定场景下处理相对复杂
  • 身份认证问题,每个微服务需要独立身份认证
  • 难以重构,随着项目的迭代,可能需要重新划分微服务
  • 某些微服务可能使用了防火墙/浏览器不友好的协议,直接访问会有一定的困难

因此,我们需要网关介于客户端与服务器之间的中间层,所有外部请求率先经过微服务网关,客户端只
需要与网关交互,只需要知道网关地址即可。这样便简化了开发且有以下优点

  • 易于监控,可在微服务网关收集监控数据并将其推送到外部系统进行分析
  • 易于认证,可在微服务网关上进行认证,然后再将请求转发到后端的微服务,从而无需在每个微服务中进行认证
  • 减少了客户端与各个微服务之间的交互次数
4.网关功能解决问题

在这里插入图片描述

网关具有身份认证与安全、审查与监控、动态路由、负载均衡、缓存、请求分片与管理、静态响应
处理等功能。当然最主要的职责还是与“外界联系”。

5.环境准备

eureka-server :注册中心
eureka-server02 :注册中心
provider-service :商品服务,提供了根据主键查询商品接口
http://localhost:7070/product/{id}
order-server :订单服务,提供了根据主键查询订单接口
http://localhost:9090/order/{id}

6.Nginx实现API网关
下载

官网:http://nginx.org/en/download.html 下载稳定版。为了方便学习,请下载 Windows 版本。

安装

解压文件后直接运行根路径下的 nginx.exe 文件即可。
Nginx 默认端口为 80,访问:http://localhost:80/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nS0EcQUD-1590394355468)(F:\A20200102\高级资源\微服务SpringCloud\img\nginx.png)]

配置路由规则

进入 Nginx 的 conf 目录,打开 nginx.conf 文件,配置路由规则

server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
            index  index.html index.htm;
        }
		
		# 路由到商品服务
		location /api-product {
			proxy_pass http://localhost:7070/;
		} 
    #路由到订单服务
		location /api-order {
			proxy_pass http://localhost:9090/;
		}
    .....
        .....
        
}
访问

http://localhost/api-product/product/1

在这里插入图片描述
http://localhost/api-order/order/1
在这里插入图片描述

7.Zuul实现API网关
搭建网关服务
创建zuul-server 项目
添加依赖
<?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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>zuul-server</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!-- 继承父依赖 -->
    <parent>
        <groupId>com.example</groupId>
        <artifactId>zuul-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <properties>
        <!-- 项目打包和编译使用的编码字符集以及 jdk 版本 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <!-- 项目依赖 -->
    <dependencies>
        <!-- spring cloud netflix zuul 依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <!-- netflix eureka client 依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- spring cloud netflix hystrix dashboard 依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
        </dependency>
        <!-- spring cloud zuul ratelimit 依赖 -->
        <dependency>
            <groupId>com.marcosbarbero.cloud</groupId>
            <artifactId>spring-cloud-zuul-ratelimit</artifactId>
            <version>2.3.0.RELEASE</version>
        </dependency>
        <!-- spring boot data redis 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- commons-pool2 对象池依赖 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!-- spring retry 依赖 -->
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>
    </dependencies>

</project>

配置文件
spring:
  application:
    name: zuul-server # 应用名称
  # redis 缓存
  redis:
    timeout: 10000        # 连接超时时间
    host: 192.168.10.101  # Redis服务器地址
    port: 6379            # Redis服务器端口
    password: root        # Redis服务器密码
    database: 0           # 选择哪个库,默认0库
    lettuce:
      pool:
        max-active: 1024  # 最大连接数,默认 8
        max-wait: 10000   # 最大连接阻塞等待时间,单位毫秒,默认 -1
        max-idle: 200     # 最大空闲连接,默认 8
        min-idle: 5       # 最小空闲连接,默认 0

server:
  port: 9000          # 端口

# 路由规则
zuul:
  # 服务限流
  ratelimit:
    # 开启限流保护
    enabled: true
    # 限流数据存储方式
    repository: REDIS
    # default-policy-list 默认配置,全局生效
#    default-policy-list:
#      - limit: 3
#        refresh-interval: 60    # 60s 内请求超过 3 次,服务端就抛出异常,60s 后可以恢复正常请求
#        quota: 30               # 请求时间总和不得超过 30 秒
#        type:
#          - origin
#          - url
#          - user
    # policy-list 自定义配置,局部生效
    policy-list:
      # 指定需要被限流的服务名称
      order-service:
        - limit: 5
          refresh-interval: 60  # 60s 内请求超过 3 次,服务端就抛出异常,60s 后可以恢复正常请求
          quota: 30             # 请求时间总和不得超过 30 秒
          type:
            - origin
            - url
            - user
  # 禁用 Zuul 默认的异常处理 filter
  SendErrorFilter:
    error:
      disable: true
  #prefix: /api
  #ignored-patterns: /**/order/**  # URL 地址排除,排除所有包含 /order/ 的路径
  #ignored-services: order-service # 服务名称排除,多个服务逗号分隔,'*' 排除所有
  #routes:
    #product-service:              # 路由 id 自定义
      #path: /product-service/**   # 配置请求 url 的映射路径
      #url: http://localhost:7070/ # 映射路径对应的微服务地址
      #serviceId: product-service  # 根据 serviceId 自动从注册中心获取服务地址并转发请求

# 配置 Eureka Server 注册中心
eureka:
  instance:
    prefer-ip-address: true       # 是否使用 ip 地址注册
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
  client:
    service-url:                  # 设置服务注册中心地址
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/

# 度量指标监控与健康检查
management:
  endpoints:
    web:
      exposure:
        include: hystrix.stream

# Hystrix 超时时间设置
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 10000  # 线程池隔离,默认超时时间 1000ms

# Ribbon 超时时间设置:建议设置小于 Hystrix
ribbon:
  ConnectTimeout: 5000                    # 请求连接的超时时间: 默认 5000ms
  ReadTimeout: 5000                       # 请求处理的超时时间: 默认 5000ms
  # 重试次数
  MaxAutoRetries: 1                       # MaxAutoRetries 表示访问服务集群下原节点(同路径访问)
  MaxAutoRetriesNextServer: 1             # MaxAutoRetriesNextServer表示访问服务集群下其余节点(换台服务器)
  # Ribbon 开启重试
  OkToRetryOnAllOperations: true
启动类
@SpringBootApplication
// 开启 Zuul 注解
@EnableZuulProxy
public class ZuulServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZuulServerApplication.class, args);
    }

}
配置路由规则
URL地址路由
# 路由规则
zuul:
	routes:
		product-service: # 路由 id 自定义
			path: /product-service/** # 配置请求 url 的映射路径
			url: http://localhost:7070/ # 映射路径对应的微服务地址

通配符含义:

通配 符含义举例解释
?匹配任意单个字符/product service/?/product-service/a,/product service/b,…
*匹配任意数量字符不包括子 路径/product service/*/product-service/aa,/product service/bbb,…
**匹配任意数量字符包括所有 下级路径/product service/**/product-service/aa,/product service/aaa/b/ccc

访问:http://localhost:9000/product-service/product/1

服务名称路由

​ 微服务一般是由几十、上百个服务组成,对于 URL 地址路由的方式,如果对每个服务实例手动指定
一个唯一访问地址,这样做显然是不合理的。
​ Zuul 支持与 Eureka 整合开发,根据 serviceId 自动从注册中心获取服务地址并转发请求,这样做好处不仅可以通过单个端点来访问应用的所有服务,而且在添加或移除服务实例时不用修改 Zuul 的路
由配置。

添加依赖Eureka Client

<!-- netflix eureka client 依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

配置文件

# 路由规则
zuul:
	routes:
		product-service: # 路由 id 自定义
			path: /product-service/** # 配置请求 url 的映射路径
			serviceId: product-service #  自动从注册中心获取服务地址并转发请求


# 配置 Eureka Server 注册中心
eureka:
  instance:
    prefer-ip-address: true       # 是否使用 ip 地址注册
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
  client:
    service-url:                  # 设置服务注册中心地址
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/

启动类

@SpringBootApplication
// 开启 Zuul 注解
@EnableZuulProxy
// 开启 EurekaClient 注解,目前版本如果配置了 Eureka 注册中心,默认会开启该注解
//@EnableEurekaClient
public class ZuulServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZuulServerApplication.class, args);
    }
}

访问:http://localhost:9000/product-service/product/1

简化路由配置

Zuul 为了方便大家使用提供了默认路由配置:如果路由 id 和 微服务名称 一致的话,path 默认对
应 微服务名称/** ,比如以下配置就没必要再写了

# 路由规则
zuul:
	routes:
		product-service: # 路由 id 自定义
			path: /product-service/** # 配置请求 url 的映射路径
		

访问:http://localhost:9000/order-service/order/1

路由排除

我们可以通过路由排除设置不允许被访问的服务。允许被访问的服务可以通过路由规则进行设置。

URL地址排除
# 路由规则
zuul:
    ignored-patterns: /**/order/** # URL 地址排除,排除所有包含 /order/ 的路径
	# 不受路由排除影响
	routes:
		product-service: # 路由 id 自定义
			path: /product-service/** # 配置请求 url 的映射路
服务名称排除
# 路由规则
zuul:
    ignored-services: order-service # 服务名称排除,多个服务逗号分隔,'*' 排除所有
	# 不受路由排除影响
	routes:
		product-service: # 路由 id 自定义
			path: /product-service/** # 配置请求 url 的映射路
路由前缀
zuul:
	prefix: /api

访问:http://localhost:9000/api/product-service/product/1

8.网关过滤器

​ Zuul 包含了对请求的路由和过滤两个核心功能,其中路由功能负责将外部请求转发到具体的微服务
实例上,是实现外部访问统一入口的基础;而过滤器功能则负责对请求的处理过程进行干预,是实现请
求校验,服务聚合等功能的基础。然而实际上,路由功能在真正运行时,它的路由映射和请求转发都是
由几个不同的过滤器完成的。
​ 路由映射主要通过 pre 类型的过滤器完成,它将请求路径与配置的路由规则进行匹配,以找到需
要转发的目标地址;而请求转发的部分则是由 routing 类型的过滤器来完成,对 pre 类型过滤器
获得的路由地址进行转发。所以说,过滤器可以说是 Zuul 实现 API 网关功能最核心的部件,每一个进入
Zuul 的 http 请求都会经过一系列的过滤器处理链得到请求响应并返回给客户端。

关键名词
  • 类型:定义路由流程中应用过滤器的阶段。共 pre、routing、post、error 4 个类型。
  • 执行顺序:在同类型中,定义过滤器执行的顺序。比如多个 pre 类型的执行顺序。
  • 条件:执行过滤器所需的条件。true 开启,false 关闭。
  • 动作:如果符合条件,将执行的动作。具体操作。
过滤器类型

**pre:**请求被路由到源服务器之前执行的过滤器
​ 身份认证
​ 选路由
​ 请求日志
routing:处理将请求发送到源服务器的过滤器
**post:**响应从源服务器返回时执行的过滤器
​ 对响应增加 HTTP 头
​ 收集统计和度量指标
​ 将响应以流的方式发送回客户端
**error:**上述阶段中出现错误时执行的过滤器

入门案例
创建过滤器

Spring Cloud Netflix Zuul 中实现过滤器必须包含 4 个基本特征:过滤器类型,执行顺序,执行条
件,动作(具体操作)。这些步骤都是 ZuulFilter 接口中定义的 4 个抽象方法:

/**
 * 网关过滤器
 */
@Component
public class CustomFilter extends ZuulFilter {

    private Logger logger = LoggerFactory.getLogger(CustomFilter.class);

    /**
     * 过滤器类型
     *      pre
     *      routing
     *      post
     *      error
     * @return
     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * 过滤器执行顺序,数字越小优先级越高
     * @return
     */
    @Override
    public int filterOrder() {
        return 0;
    }

    /**
     * 过滤器是否启用,true 开启 false 关闭
     * @return
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 过滤器具体行为
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        System.out.println("pre过滤器被执行...");
        // 获取请求上下文
        RequestContext rc = RequestContext.getCurrentContext();
        HttpServletRequest request = rc.getRequest();
        logger.info("CustomFilter...method={}, url={}",
                request.getMethod(),
                request.getRequestURL().toString());
        return null;
    }

}

**filterType :**该函数需要返回一个字符串代表过滤器的类型,而这个类型就是在 http 请求过程
中定义的各个阶段。在 Zuul 中默认定义了 4 个不同的生命周期过程类型,具体如下:
pre:请求被路由之前调用
routing: 路由请求时被调用
​ **post:**routing 和 error 过滤器之后被调用
​ **error:**处理请求时发生错误时被调用
filterOrder :通过 int 值来定义过滤器的执行顺序,数值越小优先级越高。
shouldFilter :返回一个 boolean 值来判断该过滤器是否要执行。
**run :**过滤器的具体逻辑。在该函数中,我们可以实现自定义的过滤逻辑,来确定是否要拦截当
前的请求,不对其进行后续路由,或是在请求路由返回结果之后,对处理结果做一些加工等。

访问:http://localhost:9000/product-service/product/1

权限验证案例

接下来我们在网关过滤器中通过 token 判断用户是否登录,完成一个权限验证案例

创建过滤器
/**
 * 权限验证过滤器
 */
@Component
public class AccessFilter extends ZuulFilter {

    private Logger logger = LoggerFactory.getLogger(AccessFilter.class);

    @Override
    public String filterType() {
        return "pre";
    }

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

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

    /**
     * 业务逻辑
     *
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        // Integer.parseInt("zuul");
        // 获取请求上下文
        RequestContext rc = RequestContext.getCurrentContext();
        HttpServletRequest request = rc.getRequest();
        // 获取表单中的 token
        String token = request.getParameter("token");
        // 业务逻辑处理
        if (null == token) {
            logger.warn("token is null...");
            // 请求结束,不在继续向下请求。
            rc.setSendZuulResponse(false);
            // 响应状态码,HTTP 401 错误代表用户没有访问权限
            rc.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
            // 响应类型
            rc.getResponse().setContentType("application/json; charset=utf-8");
            PrintWriter writer = null;
            try {
                writer = rc.getResponse().getWriter();
                // 响应内容
                writer.print("{\"message\":\"" + HttpStatus.UNAUTHORIZED.getReasonPhrase() + "\"}");
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (null != writer)
                    writer.close();
            }
        } else {
            // 使用 token 进行身份验证
            logger.info("token is OK!");
        }
        return null;
    }

}

访问:http://localhost:9000/product-service/product/1

http://localhost:9000/product-service/product/1?token=abc123

9.Zuul请求的生命周期
  1. HTTP 发送请求到 Zuul 网关
  2. Zuul 网关首先经过 pre filter
  3. 验证通过后进入 routing filter,接着将请求转发给远程服务,远程服务执行完返回结果,如果出
    错,则执行 error filter
  4. 继续往下执行 post filter
  5. 最后返回响应给 HTTP 客户端
10.网关过滤器异常统一处理
创建过滤器
/**
 * 异常过滤器
 */
@Component
public class ErrorFilter extends ZuulFilter {

    private Logger logger = LoggerFactory.getLogger(ErrorFilter.class);

    @Override
    public String filterType() {
        return "error";
    }

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

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

    /**
     * 业务逻辑
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        RequestContext rc = RequestContext.getCurrentContext();
        ZuulException exception = this.findZuulException(rc.getThrowable());
        logger.error("ErrorFilter..." + exception.errorCause, exception);

        HttpStatus httpStatus = null;
        if (429 == exception.nStatusCode)
            httpStatus = HttpStatus.TOO_MANY_REQUESTS;

        if (500 == exception.nStatusCode)
            httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;

        // 响应状态码
        rc.setResponseStatusCode(httpStatus.value());
        // 响应类型
        rc.getResponse().setContentType("application/json; charset=utf-8");
        PrintWriter writer = null;
        try {
            writer = rc.getResponse().getWriter();
            // 响应内容
            writer.print("{\"message\":\"" + httpStatus.getReasonPhrase() + "\"}");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != writer)
                writer.close();
        }
        return null;
    }

    private ZuulException findZuulException(Throwable throwable) {
        if (throwable.getCause() instanceof ZuulRuntimeException)
            return (ZuulException) throwable.getCause().getCause();

        if (throwable.getCause() instanceof ZuulException)
            return (ZuulException) throwable.getCause();

        if (throwable instanceof ZuulException)
            return (ZuulException) throwable;
        return new ZuulException(throwable, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null);
    }
}
模拟异常

在 pre 过滤器中添加模拟异常代码。

// 模拟异常
Integer.parseInt("zuul");
配置文件

禁用 Zuul 默认的异常处理 filter: SendErrorFilter

zuul:
	# 禁用 Zuul 默认的异常处理 filter
	SendErrorFilter:
		error:
			disable: true

访问:http://localhost:9000/product-service/product/1

11.Zuul和Hystrix无缝结合

​ 在 Spring Cloud 中,Zuul 启动器中包含了 Hystrix 相关依赖,在 Zuul 网关工程中,默认是提供了
Hystrix Dashboard 服务监控数据的(hystrix.stream),但是不会提供监控面板的界面展示。在 Spring Cloud中,Zuul 和 Hystrix 是无缝结合的,我们可以非常方便的实现网关容错处理。

网关服务监控

​ Zuul 的依赖中包含了 Hystrix 的相关 jar 包,所以我们不需要在项目中额外添加 Hystrix 的依赖。
但是需要开启数据监控的项目中要添加 dashboard 依赖

<!-- spring cloud netflix hystrix dashboard 依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
配置文件

在配置文件中开启 hystrix.stream 端点

# 度量指标监控与健康检查
management:
	endpoints:
		web:
			exposure:
				include: hystrix.stream
启动类

在需要开启数据监控的项目启动类中添加 @EnableHystrixDashboard 注解

@SpringBootApplication
// 开启 Zuul 注解
@EnableZuulProxy
// 开启数据监控注解
@EnableHystrixDashboard
public class ZuulServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZuulServerApplication.class, args);
    }

访问:http://localhost:9000/hystrix

http://localhost:9000/actuator/hystrix.stream

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S96DPMrM-1590394355475)(F:\A20200102\高级资源\微服务SpringCloud\img\hystrix.png)]

网关服务降级

​ 在 Edgware 版本之前,Zuul 提供了接口 ZuulFallbackProvider 用于实现 fallback 处理。从
Edgware 版本开始,Zuul 提供了接口 FallbackProvider 来提供 fallback 处理。
Zuul 的 fallback 容错处理逻辑,只针对 timeout 异常处理,当请求被 Zuul 路由后,只要服务有返回(包括异常),都不会触发 Zuul 的 fallback 容错逻辑。

代码示例

ProductProviderFallback.java

/**
 * 对商品服务做服务容错处理
 */
@Component
public class ProductProviderFallback implements FallbackProvider {

    /**
     * return - 返回 fallback 处理哪一个服务。返回的是服务的名称。
     * 推荐 - 为指定的服务定义特性化的 fallback 逻辑。
     * 推荐 - 提供一个处理所有服务的 fallback 逻辑。
     * 好处 - 某个服务发生超时,那么指定的 fallback 逻辑执行。如果有新服务上线,未提供 fallback 逻辑,有一个通用的。
     */
    @Override
    public String getRoute() {
        return "product-service";
    }

    /**
     * 对商品服务做服务容错处理
     *
     * @param route 容错服务名称
     * @param cause 服务异常信息
     * @return
     */
    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        return new ClientHttpResponse() {
            /**
             * 设置响应的头信息
             * @return
             */
            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders header = new HttpHeaders();
                header.setContentType(new MediaType("application", "json", Charset.forName("utf-8")));
                return header;
            }

            /**
             * 设置响应体
             * Zuul 会将本方法返回的输入流数据读取,并通过 HttpServletResponse 的输出流输出到客户端。
             * @return
             */
            @Override
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream("{\"message\":\"商品服务不可用,请稍后再试。\"}".getBytes());
            }

            /**
             * ClientHttpResponse 的 fallback 的状态码 返回 HttpStatus
             * @return
             */
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.INTERNAL_SERVER_ERROR;
            }

            /**
             * ClientHttpResponse 的 fallback 的状态码 返回 int
             * @return
             */
            @Override
            public int getRawStatusCode() throws IOException {
                return this.getStatusCode().value();
            }

            /**
             * ClientHttpResponse 的 fallback 的状态码 返回 String
             * @return
             */
            @Override
            public String getStatusText() throws IOException {
                return this.getStatusCode().getReasonPhrase();
            }

            /**
             * 回收资源方法
             * 用于回收当前 fallback 逻辑开启的资源对象。
             */
            @Override
            public void close() {
            }
        };
    }
}

关闭商品服务,访问:http://localhost:9000/product-service/product/1?token=abc123

网关服务限流

​ Zuul 网关组件也提供了限流保护。当请求并发达到阀值,自动触发限流保护,返回错误结果。只要
提供 error 错误处理机制即可。

添加依赖

Zuul 的限流保护需要额外依赖 spring-cloud-zuul-ratelimit 组件,限流数据采用 Redis 存储所以还要
添加 Redis 组件。

RateLimit 官网文档:https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit

 <!-- spring cloud zuul ratelimit 依赖 -->
        <dependency>
            <groupId>com.marcosbarbero.cloud</groupId>
            <artifactId>spring-cloud-zuul-ratelimit</artifactId>
            <version>2.3.0.RELEASE</version>
        </dependency>
        <!-- spring boot data redis 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- commons-pool2 对象池依赖 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
全局限流配置

使用全局限流配置,Zuul 会对代理的所有服务提供限流保护。

spring:
  application:
    name: zuul-server # 应用名称
  # redis 缓存
  redis:
    timeout: 10000        # 连接超时时间
    host: 192.168.10.101  # Redis服务器地址
    port: 6379            # Redis服务器端口
    password: root        # Redis服务器密码
    database: 0           # 选择哪个库,默认0库
    lettuce:
      pool:
        max-active: 1024  # 最大连接数,默认 8
        max-wait: 10000   # 最大连接阻塞等待时间,单位毫秒,默认 -1
        max-idle: 200     # 最大空闲连接,默认 8
        min-idle: 5       # 最小空闲连接,默认 0

server:
  port: 9000          # 端口

# 路由规则
zuul:
  # 服务限流
  ratelimit:
    # 开启限流保护
    enabled: true
    # 限流数据存储方式
    repository: REDIS
    # default-policy-list 默认配置,全局生效
    default-policy-list:
      - limit: 3
        refresh-interval: 60    # 60s 内请求超过 3 次,服务端就抛出异常,60s 后可以恢复正常请求
        quota: 30               # 请求时间总和不得超过 30 秒
        type:
          - origin
          - url
          - user
 
# 配置 Eureka Server 注册中心
eureka:
  instance:
    prefer-ip-address: true       # 是否使用 ip 地址注册
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
  client:
    service-url:                  # 设置服务注册中心地址
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/

Zuul-RateLimiter 基本配置项:

配置项可选值说明
enabledtrue/false是否启用限流
repositoryREDIS:基于 Redis,使用时必须引入 Redis 相关依赖 CONSUL:基于 Consul JPA:基于 SpringDataJPA,需要用到数据 库 使用 Java 编写的基于令牌桶算法的限流 库: BUCKET4J_JCACHE BUCKET4J_HAZELCAST BUCKET4J_IGNITE BUCKET4J_INFINISPAN限流数据的存储方式,无默认值 必填项
key-prefixString限流 key 前缀
default policy-listList of Policy默认策略
policy-listMap of Lists of Policy自定义策略
post-filter order-postFilter 过滤顺序
pre-filter order-preFilter 过滤顺序

Bucket4j 实现需要相关的 bean @Qualifier(“RateLimit”):
JCache - javax.cache.Cache
Hazelcast - com.hazelcast.core.IMap
Ignite - org.apache.ignite.IgniteCache
Infinispan - org.infinispan.functional.ReadWriteMap

Policy 限流策略配置项说明:

说明
limit单位时间内请求次数限制
quota单位时间内累计请求时间限制(秒),非必要参数
refresh interval单位时间(秒),默认 60 秒
type限流方式: ORIGIN:访问 IP 限流 URL:访问 URL 限流 USER:特定用户或用户组限流(比如:非会员用户限制每分钟只允许下载一个 文件) URL_PATTERN ROLE HTTP_METHOD

访问:http://localhost:9000/product-service/product/1?token=abc123

局部限流配置

使用局部限流配置,Zuul 仅针对配置的服务提供限流保护。全局配置和局部配置可同时存在,局部
优先级高于全局。

# 路由规则
zuul:
  # 服务限流
  ratelimit:
    # 开启限流保护
    enabled: true
    # 限流数据存储方式
    repository: REDIS
    # default-policy-list 默认配置,全局生效
#    default-policy-list:
#      - limit: 3
#        refresh-interval: 60    # 60s 内请求超过 3 次,服务端就抛出异常,60s 后可以恢复正常请求
#        quota: 30               # 请求时间总和不得超过 30 秒
#        type:
#          - origin
#          - url
#          - user
    # policy-list 自定义配置,局部生效
    policy-list:
      # 指定需要被限流的服务名称
      order-service:
        - limit: 5
          refresh-interval: 60  # 60s 内请求超过 3 次,服务端就抛出异常,60s 后可以恢复正常请求
          quota: 30             # 请求时间总和不得超过 30 秒
          type:
            - origin
            - url
            - user
自定义限流策略
/**
 * 自定义限流策略
 */
@Component
public class RateLimitKeyGenerator extends DefaultRateLimitKeyGenerator {

    public RateLimitKeyGenerator(RateLimitProperties properties, RateLimitUtils rateLimitUtils) {
        super(properties, rateLimitUtils);
    }

    /**
     * 限流逻辑
     *
     * @param request
     * @param route
     * @param policy
     * @return
     */
    @Override
    public String key(HttpServletRequest request, Route route, RateLimitProperties.Policy policy) {
        // 对请求参数中相同的 token 值进行限流
        return super.key(request, route, policy) + ":" + request.getParameter("token");
    }

}

多次访问:http://localhost:9000/product-service/product/1?token=abc123

错误处理

配置 error 类型的网关过滤器进行处理即可。修改之前的 ErrorFilter 让其变的通用

/**
 * 异常过滤器
 */
@Component
public class ErrorFilter extends ZuulFilter {

    private Logger logger = LoggerFactory.getLogger(ErrorFilter.class);

    @Override
    public String filterType() {
        return "error";
    }

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

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

    /**
     * 业务逻辑
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        RequestContext rc = RequestContext.getCurrentContext();
        ZuulException exception = this.findZuulException(rc.getThrowable());
        logger.error("ErrorFilter..." + exception.errorCause, exception);

        HttpStatus httpStatus = null;
        if (429 == exception.nStatusCode)
            httpStatus = HttpStatus.TOO_MANY_REQUESTS;

        if (500 == exception.nStatusCode)
            httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;

        // 响应状态码
        rc.setResponseStatusCode(httpStatus.value());
        // 响应类型
        rc.getResponse().setContentType("application/json; charset=utf-8");
        PrintWriter writer = null;
        try {
            writer = rc.getResponse().getWriter();
            // 响应内容
            writer.print("{\"message\":\"" + httpStatus.getReasonPhrase() + "\"}");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != writer)
                writer.close();
        }
        return null;
    }

    private ZuulException findZuulException(Throwable throwable) {
        if (throwable.getCause() instanceof ZuulRuntimeException)
            return (ZuulException) throwable.getCause().getCause();

        if (throwable.getCause() instanceof ZuulException)
            return (ZuulException) throwable.getCause();

        if (throwable instanceof ZuulException)
            return (ZuulException) throwable;
        return new ZuulException(throwable, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null);
    }
}
网关性能调优

​ Zuul 中的 Hystrix 内部使用线程池隔离机制提供请求路由实现,其默认的超时时长为 1000 毫秒。
Ribbon 底层默认超时时长为 5000 毫秒。如果 Hystrix 超时,直接返回超时异常。如果 Ribbon 超时,同时 Hystrix 未超时,Ribbon 会自动进行服务集群轮询重试,直到 Hystrix 超时为止。如果 Hystrix 超时时长小于 Ribbon 超时时长,Ribbon 不会进行服务集群轮询重试。

配置文件

Zuul 中可配置的超时时长有两个位置:Hystrix 和 Ribbon

zuul:
	# 开启 Zuul 网关重试
	retryable: true

# Hystrix 超时时间设置
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 10000  # 线程池隔离,默认超时时间 1000ms

# Ribbon 超时时间设置:建议设置小于 Hystrix
ribbon:
  ConnectTimeout: 5000                    # 请求连接的超时时间: 默认 5000ms
  ReadTimeout: 5000                       # 请求处理的超时时间: 默认 5000ms
  # 重试次数
  MaxAutoRetries: 1                       # MaxAutoRetries 表示访问服务集群下原节点(同路径访问)
  MaxAutoRetriesNextServer: 1             # MaxAutoRetriesNextServer表示访问服务集群下其余节点(换台服务器)
  # Ribbon 开启重试
  OkToRetryOnAllOperations: true
添加依赖

Spring Cloud Netflix Zuul 网关重试机制需要使用 spring-retry 组件。

<!-- spring retry 依赖 -->
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>
启动类

启动类需要开启 @EnableRetry 重试注解

@SpringBootApplication
// 开启 EurekaClient 注解,目前版本如果配置了 Eureka 注册中心,默认会开启该注解
//@EnableEurekaClient
// 开启 Zuul 注解
@EnableZuulProxy
// 开启数据监控注解
@EnableHystrixDashboard
// 开启重试注解
@EnableRetry
public class ZuulServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZuulServerApplication.class, args);
    }

}
模拟超时

商品服务模拟超时

@RestController
@RequestMapping("/product")
public class ProductController {

    @Autowired
    private ProductService productService;

    /**
     * 根据主键查询商品
     *
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public Product selectProductById(@PathVariable("id") Integer id) {
       try {
           Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return productService.selectProductById(id);
    }
}

配置前访问:http://localhost:9000/product-service/product/1?token=abc123

12.Zuul和Sentinel整合
网关服务限流和降级
创建zuul-server-sentinel 项目
添加依赖

单独使用添加 sentinel-zuul-adapter 依赖即可。
若想跟 Sentinel Starter 配合使用,需要加上 spring-cloud-alibaba-sentinel-gateway 依赖,
同时需要添加 spring-cloud-starter-netflix-zuul 依赖来让 spring-cloud-alibaba-sentinelgateway 模块里的 Zuul 自动化配置类生效。
同时请将 spring.cloud.sentinel.filter.enabled 配置项置为 false(若在网关流控控制台
上看到了 URL 资源,就是此配置项没有置为 false)

<?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">
    <modelVersion>4.0.0</modelVersion>

    <artifactId>zuul-server-sentinel</artifactId>

    <!-- 继承父依赖 -->
    <parent>
        <groupId>com.example</groupId>
        <artifactId>zuul-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <properties>
        <!-- 项目打包和编译使用的编码字符集以及 jdk 版本 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <!-- 项目依赖 -->
    <dependencies>
        <!-- spring cloud netflix zuul 依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <!-- netflix eureka client 依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- 单独使用 -->
        <!-- sentinel zuul adapter 依赖 -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-zuul-adapter</artifactId>
        </dependency>
        <!-- 和 Sentinel Starter 配合使用 -->
        <!--
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </dependency>
        -->
    </dependencies>

</project>

配置文件
spring:
  application:
    name: zuul-server-sentinel # 应用名称
  cloud:
    sentinel:
      filter:
        enabled: false

server:
  port: 9001          # 端口

# 配置 Eureka Server 注册中心
eureka:
  instance:
    prefer-ip-address: true       # 是否使用 ip 地址注册
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
  client:
    service-url:                  # 设置服务注册中心地址
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
网关服务配置类

配置网关服务过滤器和网关限流规则

/**
 * 网关服务配置类
 */
@Configuration
public class ZuulConfig {

    // 底层继承了 ZuulFilter
    @Bean
    public ZuulFilter sentinelZuulPreFilter() {
        // We can also provider the filter order in the constructor.
        return new SentinelZuulPreFilter();
    }

    // 底层继承了 ZuulFilter
    @Bean
    public ZuulFilter sentinelZuulPostFilter() {
        return new SentinelZuulPostFilter();
    }

    // 底层继承了 ZuulFilter
    @Bean
    public ZuulFilter sentinelZuulErrorFilter() {
        return new SentinelZuulErrorFilter();
    }

    /**
     * Spring 容器初始化的时候执行该方法
     */
    @PostConstruct
    public void doInit() {
        // 注册 FallbackProvider
        ZuulBlockFallbackManager.registerProvider(new OrderBlockFallbackProvider());
        // 加载网关限流规则
        initGatewayRules();
    }

    /**
     * 网关限流规则
     */
    private void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();
        /*
            resource:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称
            count:限流阈值
            intervalSec:统计时间窗口,单位是秒,默认是 1 秒
         */
        rules.add(new GatewayFlowRule("order-service")
                .setCount(3) // 限流阈值
                .setIntervalSec(60)); // 统计时间窗口,单位是秒,默认是 1 秒
        // 加载网关限流规则
        GatewayRuleManager.loadRules(rules);
    }

}

多次访问:http://localhost:9001/order-service/order/1

自定义限流处理(网关服务降级)

发生限流之后的处理流程 :
发生限流之后可自定义返回参数,通过实现 ZuulBlockFallbackProvider 接口,默认的实现
DefaultBlockFallbackProvider
默认的 fallback route 的规则是 route ID 或自定义的 API 分组名称。

编写限流处理类
/**
 * 对订单服务做服务容错处理
 */
public class OrderBlockFallbackProvider implements ZuulBlockFallbackProvider {

    private Logger logger = LoggerFactory.getLogger(OrderBlockFallbackProvider.class);

    @Override
    public String getRoute() {
        return "order-service"; // 服务名称
    }

    @Override
    public BlockResponse fallbackResponse(String route, Throwable cause) {
        logger.error("{} 服务触发限流", route);
        if (cause instanceof BlockException) {
            return new BlockResponse(429, "服务访问压力过大,请稍后再试。", route);
        } else {
            return new BlockResponse(500, "系统错误,请联系管理员。", route);
        }
    }
}
将限流处理类注册至 Zuul 容器
 /**
     * Spring 容器初始化的时候执行该方法
     */
    @PostConstruct
    public void doInit() {
        // 注册 FallbackProvider
        ZuulBlockFallbackManager.registerProvider(new OrderBlockFallbackProvider());
        // 加载网关限流规则
        initGatewayRules();
    }

http://localhost:9001/order-service/order/1

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
在 Spring Cloud 中,可以使用 Zuul 作为 API 网关,通过 Zuul 进行请求转发和路由。而 Zuul 还提供了过滤器(Filter)机制,可以在请求路由到目标服务前、后进行一些预处理或后处理。 在 Zuul 中,过滤器分为四种类型: 1. Pre:在请求路由到目标服务前执行; 2. Post:在请求路由到目标服务后执行; 3. Route:用于将请求路由到目标服务的过程中执行; 4. Error:在请求发生错误时执行。 通过实现 Zuul过滤器接口,可以自定义过滤器,并指定过滤器的类型和执行顺序。在过滤器中,可以对请求或响应进行修改或者拦截。 对于过滤器的返回值拦截,可以在 Pre 和 Error 类型的过滤器中进行。在 Pre 类型的过滤器中,可以通过抛出异常的方式终止请求,并将异常信息返回给客户端。在 Error 类型的过滤器中,可以通过设置响应状态码、响应头信息等方式,修改响应内容。 例如,在 Pre 类型的过滤器中,可以通过以下代码实现返回拦截: ```java public class AuthFilter extends ZuulFilter { @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(); String authToken = request.getHeader("Authorization"); if (StringUtils.isBlank(authToken)) { ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value()); ctx.setResponseBody("Unauthorized"); return null; } return null; } } ``` 在该过滤器中,如果请求头中不存在 Authorization 字段,则设置返回状态码为 401,并设置响应内容为 "Unauthorized",从而实现了返回值拦截。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值