zoukankan      html  css  js  c++  java
  • SpringCloud 之 Netflix Zuul 服务网关

    本文较大篇幅引用https://www.mrhelloworld.com/zuul/,相关内容版权归该文章作者所有

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

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

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

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

      API 网关的流行,源于近几年来移动应用与企业间互联需求的兴起。移动应用、企业互联,使得后台服务支持的对象,从以前单一的 Web 应用,扩展到多种使用场景,且每种使用场景对后台服务的要求都不尽相同。这不仅增加了后台服务的响应量,还增加了后台服务的复杂性。随着微服务架构概念的提出,API 网关成为了微服务架构的一个标配组件

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

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

    • 聚合接口使得服务对调用者透明,客户端与后端的耦合度降低
    • 聚合后台服务,节省流量,提高性能,提升用户体验
    • 提供安全、流控、过滤、缓存、计费、监控等 API 管理功能
    • 单体应用:浏览器发起请求到单体应用所在的机器,应用从数据库查询数据原路返回给浏览器,对于单体应用来说是不需要网关的。
    • 微服务:微服务的应用可能部署在不同机房,不同地区,不同域名下。此时客户端(浏览器/手机/软件工具)想要请求对应的服务,都需要知道机器的具体 IP 或者域名 URL,当微服务实例众多时,这是非常难以记忆的,对于客户端来说也太复杂难以维护。此时就有了网关,客户端相关的请求直接发送到网关,由网关根据请求标识解析判断出具体的微服务地址,再把请求转发到微服务实例。这其中的记忆功能就全部交由网关来操作了。

    总结

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

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

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

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

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

      总结一下,网关应当具备以下功能:

    • 性能:API 高可用,负载均衡,容错机制。
    • 安全:权限身份认证、脱敏,流量清洗,后端签名(保证全链路可信调用),黑名单(非法调用的限制)。
    • 日志:日志记录,一旦涉及分布式,全链路跟踪必不可少。
    • 缓存:数据缓存。
    • 监控:记录请求响应数据,API 耗时分析,性能监控。
    • 限流:流量控制,错峰流控,可以定义多种限流规则。
    • 灰度:线上灰度部署,可以减小风险。
    • 路由:动态路由规则。

    Zuul 是 Netflix 公司开源的一个 API 网关组件,Spring Cloud 对其进行二次基于 Spring Boot 的注解式封装做到开箱即用。

    目前来说,结合 Sring Cloud 提供的服务治理体系,可以做到请求转发,根据配置或者默认的路由规则进行路由和 Load Balance,无缝集成 Hystrix。

    虽然可以通过自定义 Filter 实现我们想要的功能,但是由于 Zuul 本身的设计是基于单线程的接收请求和转发处理,是阻塞 IO,不支持长连接。

    目前来看 Zuul 就显得很鸡肋,随着 Zuul 2.x 一直跳票(2019 年 5 月发布了 Zuul 2.0 版本),Spring Cloud 推出自己的 Spring Cloud Gateway。

    大意就是:Zuul 已死,Spring Cloud Gateway 永生(手动狗头)。但是我们这里还是先学一下

    zuul-demo 聚合工程。SpringBoot 2.2.4.RELEASESpring Cloud Hoxton.SR1

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

    创建 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>
    
        <!-- 项目依赖 -->
        <dependencies>
            <!-- spring cloud netflix zuul 依赖 -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
            </dependency>
        </dependencies>
    
    </project>
    server:
      port: 9000 # 端口
    
    spring:
      application:
        name: zuul-server # 应用名称

    启动类需要开启 @EnableZuulProxy 注解。

    package com.example;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
    
    @SpringBootApplication
    // 开启 Zuul 注解
    @EnableZuulProxy
    public class ZuulServerApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(ZuulServerApplication.class, args);
        }
    
    }
    # 路由规则
    zuul:
      routes:
        product-service:              # 路由 id 自定义
          path: /product-service/**   # 配置请求 url 的映射路径
          url: http://localhost:7070/ # 映射路径对应的微服务地址

    通配符含义:

     访问:http://localhost:9000/product-service/product/1 结果如下:

    相当于访问http://localhost:7070/product/1

    服务名称路由

     微服务一般是由几十、上百个服务组成,对于 URL 地址路由的方式,如果对每个服务实例手动指定一个唯一访问地址,这样做显然是不合理的。

     Zuul 支持与 Eureka 整合开发,根据 serviceId 自动从注册中心获取服务地址并转发请求,这样做的好处不仅可以通过单个端点来访问应用的所有服务,而且在添加或移除服务实例时不用修改 Zuul 的路由配置。

    1.添加 Eureka Client 依赖

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

    2.配置注册中心和路由规则

    # 路由规则
    zuul:
      routes:
        product-service:              # 路由 id 自定义
          path: /product-service/**   # 配置请求 url 的映射路径
          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/

    3.启动类

    package com.example;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
    
    @SpringBootApplication
    // 开启 Zuul 注解
    @EnableZuulProxy
    // 开启 EurekaClient 注解,目前版本如果配置了 Eureka 注册中心,默认会开启该注解
    //@EnableEurekaClient
    public class ZuulServerApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(ZuulServerApplication.class, args);
        }
    
    }

    4.访问

     访问:http://localhost:9000/product-service/product/1 结果如下:

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

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

    访问

      此时我们并没有配置任何订单服务的路由规则,访问:http://localhost:9000/order-service/order/1 结果如下:

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

    # 路由规则
    zuul:
      ignored-patterns: /**/order/**  # URL 地址排除,排除所有包含 /order/ 的路径
      # 不受路由排除影响
      routes:
        product-service:              # 路由 id 自定义
          path: /product-service/**   # 配置请求 url 的映射路径
          serviceId: product-service  # 根据 serviceId 自动从注册中心获取服务地址并转发请求
    # 路由规则
    zuul:
      ignored-services: order-service # 服务名称排除,多个服务逗号分隔,'*' 排除所有
      # 不受路由排除影响
      routes:
        product-service:              # 路由 id 自定义
          path: /product-service/**   # 配置请求 url 的映射路径
          serviceId: product-service  # 根据 serviceId 自动从注册中心获取服务地址并转发请求
    zuul:
      prefix: /api

    访问

      访问:http://localhost:9000/api/product-service/product/1 结果如下:

    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 个抽象方法:

    package com.example.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;
    
    /**
     * 网关过滤器
     */
    @Component
    public class CustomFilter extends ZuulFilter {
    
        private static final 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 {
            // 获取请求上下文
            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 控制台输出如下:

    CustomFilter...method=GET, url=http://localhost:9000/product-service/product/1

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

    package com.example.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.http.HttpStatus;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.http.HttpServletRequest;
    import java.io.IOException;
    import java.io.PrintWriter;
    
    /**
     * 权限验证过滤器
     */
    @Component
    public class AccessFilter extends ZuulFilter {
    
        private static final Logger logger = LoggerFactory.getLogger(AccessFilter.class);
    
        @Override
        public String filterType() {
            return "pre";
        }
    
        @Override
        public int filterOrder() {
            return 1;
        }
    
        @Override
        public boolean shouldFilter() {
            return true;
        }
    
        @Override
        public Object run() throws ZuulException {
            // 获取请求上下文
            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 结果如下:

    • HTTP 发送请求到 Zuul 网关
    • Zuul 网关首先经过 pre filter
    • 验证通过后进入 routing filter,接着将请求转发给远程服务,远程服务执行完返回结果,如果出错,则执行 error filter
    • 继续往下执行 post filter
    • 最后返回响应给 HTTP 客户端
    package com.example.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.http.HttpStatus;
    import org.springframework.stereotype.Component;
    
    import java.io.IOException;
    import java.io.PrintWriter;
    
    /**
     * 异常过滤器
     */
    @Component
    public class ErrorFilter extends ZuulFilter {
    
        private static final Logger logger = LoggerFactory.getLogger(ErrorFilter.class);
    
        @Override
        public String filterType() {
            return "error";
        }
    
        @Override
        public int filterOrder() {
            return 0;
        }
    
        @Override
        public boolean shouldFilter() {
            return true;
        }
    
        @Override
        public Object run() throws ZuulException {
            RequestContext rc = RequestContext.getCurrentContext();
            Throwable throwable = rc.getThrowable();
            logger.error("ErrorFilter..." + throwable.getCause().getMessage(), throwable);
            // 响应状态码,HTTP 500 服务器错误
            rc.setResponseStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
            // 响应类型
            rc.getResponse().setContentType("application/json; charset=utf-8");
            PrintWriter writer = null;
            try {
                writer = rc.getResponse().getWriter();
                // 响应内容
                writer.print("{"message":"" + HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase() + ""}");
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (null != writer)
                    writer.close();
            }
            return null;
        }
    
    }

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

    // 模拟异常
    Integer.parseInt("zuul");

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

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

    访问:http://localhost:9000/product-service/product/1 结果如下:

    在 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 注解。

    package com.example;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
    import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
    
    @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/product-service/product/1?token=abc123 结果如下:

    在 Edgware 版本之前,Zuul 提供了接口 ZuulFallbackProvider 用于实现 fallback 处理。从 Edgware 版本开始,Zuul 提供了接口 FallbackProvider 来提供 fallback 处理。

      Zuul 的 fallback 容错处理逻辑,只针对 timeout 异常处理,当请求被 Zuul 路由后,只要服务有返回(包括异常),都不会触发 Zuul 的 fallback 容错逻辑。

    因为对于Zuul网关来说,做请求路由分发的时候,结果由远程服务运算。远程服务反馈了异常信息,Zuul 网关不会处理异常,因为无法确定这个错误是否是应用程序真实想要反馈给客户端的。

    ProductProviderFallback.java

    package com.example.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;
    import java.nio.charset.Charset;
    
    /**
     * 对商品服务做服务容错处理
     */
    @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 结果如下:

    顾名思义,限流就是限制流量,就像你宽带包有 1 个 G 的流量,用完了就没了。

    通过限流,我们可以很好地控制系统的 QPS,从而达到保护系统的目的。Zuul 网关组件也提供了限流保护。当请求并发达到阀值,自动触发限流保护,返回错误结果。只要提供 error 错误处理机制即可。

    比如 Web 服务、对外 API,这种类型的服务有以下几种可能导致机器被拖垮:

    • 用户增长过快(好事)
    • 因为某个热点事件(微博热搜)
    • 竞争对象爬虫
    • 恶意的请求

      这些情况都是无法预知的,不知道什么时候会有 10 倍甚至 20 倍的流量打进来,如果真碰上这种情况,扩容是根本来不及的

    从上图可以看出,对内而言:上游的 A、B 服务直接依赖了下游的基础服务 C,对于 A,B 服务都依赖的基础服务 C 这种场景,

    服务 A 和 B 其实处于某种竞争关系,如果服务 A 的并发阈值设置过大,当流量高峰期来临,有可能直接拖垮基础服务 C 并影响服务 B,即雪崩效应。

    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 会对代理的所有服务提供限流保护。

    server:
      port: 9000 # 端口
    
    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
    
    # 配置 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:
      # 服务限流
      ratelimit:
        # 开启限流保护
        enabled: true
        # 限流数据存储方式
        repository: REDIS
        # default-policy-list 默认配置,全局生效
        default-policy-list:
          - limit: 3
            refresh-interval: 60    # 60s 内请求超过 3 次,服务端就抛出异常,60s 后可以恢复正常请求
            type:
              - origin
              - url
              - user

    Zuul-RateLimiter 基本配置项:

    Bucket4j 实现需要相关的 bean @Qualifier(“RateLimit”):

    • JCache - javax.cache.Cache
    • Hazelcast - com.hazelcast.core.IMap
    • Ignite - org.apache.ignite.IgniteCache
    • Infinispan - org.infinispan.functional.ReadWriteMap

    Policy 限流策略配置项说明:

    访问

      访问:http://localhost:9000/product-service/product/1?token=abc123 控制台结果如下:

    ErrorFilter...com.netflix.zuul.exception.ZuulException: 429 TOO_MANY_REQUESTS

    使用局部限流配置,Zuul 仅针对配置的服务提供限流保护。

    zuul:
      # 服务限流
      ratelimit:
        # 开启限流保护
        enabled: true
        # 限流数据存储方式
        repository: REDIS
        # policy-list 自定义配置,局部生效
        policy-list:
          # 指定需要被限流的服务名称
          order-service:
            - limit: 5
              refresh-interval: 60  # 60s 内请求超过 5 次,服务端就抛出异常,60s 后可以恢复正常请求
              type:
                - origin
                - url
                - user

    访问:http://localhost:9000/order-service/order/1?token=abc123 控制台结果如下:

    ErrorFilter...com.netflix.zuul.exception.ZuulException: 429 TOO_MANY_REQUESTS

     如果希望自己控制限流策略,可以通过自定义 RateLimitKeyGenerator 的实现来增加自己的策略逻辑。

      修改商品服务控制层代码如下,添加 /product/single

    package com.example.controller;
    
    import com.example.pojo.Product;
    import com.example.service.ProductService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/product")
    public class ProductController {
    
        @Autowired
        private ProductService productService;
    
        /**
         * 根据主键查询商品
         *
         * @param id
         * @return
         */
        @GetMapping("/{id}")
        public Product selectProductById(@PathVariable("id") Integer id) {
            return productService.selectProductById(id);
        }
    
        /**
         * 根据主键查询商品
         *
         * @param id
         * @return
         */
        @GetMapping("/single")
        public Product selectProductSingle(Integer id) {
            return productService.selectProductById(id);
        }
    
    }

    自定义限流策略类。

    package com.example.ratelimit;
    
    import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimitUtils;
    import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties;
    import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.DefaultRateLimitKeyGenerator;
    import org.springframework.cloud.netflix.zuul.filters.Route;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.http.HttpServletRequest;
    
    /**
     * 自定义限流策略
     */
    @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) {
            // 对请求参数中相同的 id 值进行限流
            return super.key(request, route, policy) + ":" + request.getParameter("id");
        }
    
    }

    多次访问:http://localhost:9000/api/product-service/product/single?token=abc123&id=1 被限流后,马上更换 id=2 重新访问发现服务任然可用,再继续多次访问,发现更换过的 id=2 也被限流了。Redis 信息如下:

     
    127.0.0.1:6379> keys *
    1) "zuul-server:product-service:0:0:0:0:0:0:0:1:/product/single:anonymous:1"
    2) "zuul-server:product-service:0:0:0:0:0:0:0:1:/product/single:anonymous:2"

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

    使用 Zuul 的 Spring Cloud 微服务结构图:

    从上图中可以看出。整体请求逻辑还是比较复杂的,在没有 Zuul 网关的情况下,client 请求 service 的时候,也有请求超时的可能。那么当增加了 Zuul 网关的时候,请求超时的可能就更明显了。

      当请求通过 Zuul 网关路由到服务,并等待服务返回响应,这个过程中 Zuul 也有超时控制。Zuul 的底层使用的是 Hystrix + Ribbon 来实现请求路由。

     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                    # 请求连接的超时时间: 默认超时时间 1000ms
      ReadTimeout: 5000                       # 请求处理的超时时间: 默认超时时间 1000ms
      # 重试次数
      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 重试注解。

    package com.example;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
    import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
    import org.springframework.retry.annotation.EnableRetry;
    
    @SpringBootApplication
    // 开启 Zuul 注解
    @EnableZuulProxy
    // 开启 EurekaClient 注解,目前版本如果配置了 Eureka 注册中心,默认会开启该注解
    //@EnableEurekaClient
    // 开启数据监控注解
    @EnableHystrixDashboard
    // 开启重试注解
    @EnableRetry
    public class ZuulServerApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(ZuulServerApplication.class, args);
        }
    
    }

    商品服务模拟超时。

    package com.example.controller;
    
    import com.example.pojo.Product;
    import com.example.service.ProductService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @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 结果如下(触发了网关服务降级):

    配置后访问:http://localhost:9000/product-service/product/1?token=abc123 结果如下:

     至此 Zuul 服务网关所有的知识点就讲解结束了。

  • 相关阅读:
    cesium图形上加载图片
    cesium可视化空间数据2
    linux命令之用户和用户组
    YARN应用程序开发和设计流程
    Yarn中几个专用名称
    break、continue、return之间的区别与联系
    kafka的相关操作脚本
    scala函数进阶篇
    scala的基础部分
    视图
  • 原文地址:https://www.cnblogs.com/lusaisai/p/13340563.html
Copyright © 2011-2022 走看看