API 网关 & Spring Cloud Zuul 限流 灰度发布

发布时间:2024年01月09日

第一节 API 网关介绍

1. API 网关是什么

API网关可以提供一个单独且统一的API入口用于访问内部一个或多个API。简单来说嘛就是一个统一入口,比如现在的支付宝或者微信的相关api服务一样,都有一个统一的api地址,统一的请求参数,统一的鉴权。

2. 为什么要使用 API 网关

API 网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。在未加入网关时,一般上会在服务外网架设一个负载均衡,如nginx等。此时,微服务的组成为:

此时,对于Open Service而言可能需要提供权限控制等和业务无关的能力,这样本身就破坏了微服务服务单一的原则。所以,一般上在Open Service之上,还有一层服务提供诸如通用的权限校验、参数校验等功能,此服务就是网关了。之后,对于内部微服务而言,只需要关心各自微服务提供的业务功能即可,无需去关心其他非业务相关的功能。

3. API 网关的选择

  • Spring Cloud Zuul:本身基于Netflix开源的微服务网关,可以和Eureka,Ribbon,Hystrix等组件配合使用。

  • Kong : 基于OpenResty的 API 网关服务和网关服务管理层。

  • Spring Cloud Gateway:是由spring官方基于Spring5.0,Spring Boot2.0,Project Reactor等技术开发的网关,提供了一个构建在Spring Ecosystem之上的API网关,旨在提供一种简单而有效的途径来发送API,并向他们提供交叉关注点,例如:安全性,监控/指标和弹性。目的是为了替换Spring Cloud Netfilx Zuul的。

在Spring cloud体系中,一般上选择zuul或者Gateway。当然,也可以综合自己的业务复杂性,自研一套或者改造一套符合自身业务发展的api网关的,最简单做法是做个聚合api服务,通过SpringBoot构建对外的api接口,实现统一鉴权、参数校验、权限控制等功能,说白了就是一个rest服务。

4. 网关的优缺点

  • 优点

    • 减少api请求次数

    • 限流

    • 缓存

    • 统一认证

    • 灰度发布

    • 路由转发

    • 降低微服务的复杂度

    • 支持混合通信协议(前端只和api通信,其他的由网关调用)

  • 缺点

    • 网关需高可用,可能产生单点故障

    • 管理复杂

5. Spring Cloud Zuul

  • 对验证与安全保障: 识别面向各类资源的验证要求并拒绝那些与要求不符的请求。

  • 审查与监控: 在边缘位置追踪有意义数据及统计结果,从而为我们带来准确的生产状态结论。

  • 动态路由: 以动态方式根据需要将请求路由至不同后端集群处。

  • 压力测试: 逐渐增加指向集群的负载流量,从而计算性能水平。

  • 负载分配: 为每一种负载类型分配对应容量,并弃用超出限定值的请求。

  • 静态响应处理: 在边缘位置直接建立部分响应,从而避免其流入内部集群。

  • 多区域弹性: 跨越AWS区域进行请求路由,旨在实现ELB使用多样化并保证边缘位置与使用者尽可能接近。

6. Spring Cloud Zuul 路由

6.1 创建子工程 api-gateway
<?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-demo</artifactId>
        <groupId>com.qf</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>api-gateway</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zuul</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>springloaded</artifactId>
        </dependency>
    </dependencies>
</project>
6.2 编写启动类
package com.qf.spring.cloud.api.gatewat;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@SpringBootApplication
@EnableZuulProxy //启用网关
public class ApiGateway {

    public static void main(String[] args) {
        SpringApplication.run(ApiGateway.class, args);
    }
}
6.3 yml 配置
server:
  port: 1700
spring:
  application:
    name: api-gateway
eureka:
  client:
    service-url:
      defaultZone: http://localhost:11000/eureka/
6.4 启动服务,进行测试
6.5 去掉微服务名访问

路径中的微服务名主要作用就是指向访问的微服务,如果能够将url访问地址跟微服务名匹配起来,那么访问的时候就可以将微服务名去掉了。

server:
  port: 14000
spring:
  application:
    name: api-gateway
eureka:
  client:
    service-url:
      defaultZone: http://localhost:11000/eureka/
zuul: # 网关
  routes: # 路由
    goods: # 这个名字可以随便取
      serviceId: goods-service-prod  #使用的微服务名
      path: /goods/** # 访问的url地址
      strip-prefix: false # 是否去掉前缀,默认值就是true

再次测试

  • 启动eureka

  • 启动 config-server

  • 启动微服务多台(同一个微服务名的服务),如果不启动多台,那么路由(不带微服务名)将失效

  • 访问测试

http://localhost:14000/goods/content

http://localhost:14000/goods-service-prod/goods/content

测试结果:使用微服务名和不使用微服务名均可访问

6.6 屏蔽微服务名称访问
server:
  port: 14000
spring:
  application:
    name: api-gateway
eureka:
  client:
    service-url:
      defaultZone: http://localhost:11000/eureka/
zuul:
  ignored-services: '*' # 忽略的微服务名,多个使用逗号分隔开,全部忽略使用*,但需要在*号两端加上单引号或者双引号
  routes:
    goods:
      serviceId: goods-service-prod
      path: /goods/**
      strip-prefix: false

启动服务,再次测试:http://localhost:14000/goods-service-prod/goods/content

测试发现不能访问

输入 http://localhost:14000/goods/content 访问成功

6.7 统一访问前缀
server:
  port: 14000
spring:
  application:
    name: api-gateway
eureka:
  client:
    service-url:
      defaultZone: http://localhost:11000/eureka/
zuul:
  ignored-services: '*' # 忽略的微服务名,多个使用逗号分隔开,全部忽略使用*,但需要在*号两端加上单引号或者双引号
  prefix: /qf #访问的url地址中必须有这个前缀,否则不能访问
  routes:
    goods:
      serviceId: goods-service-prod
      path: /goods/**
      strip-prefix: false

7. Spring Cloud Zuul 限流

7.1 添加依赖
<!--rate limit 限流包-->
<dependency>
    <groupId>com.marcosbarbero.cloud</groupId>
    <artifactId>spring-cloud-zuul-ratelimit</artifactId>
    <version>1.3.4.RELEASE</version>
</dependency>
7.2 yml配置
server:
  port: 14000
spring:
  application:
    name: api-gateway
eureka:
  client:
    service-url:
      defaultZone: http://localhost:11000/eureka/
zuul:
  ignored-services: '*' # 忽略的微服务名,多个使用逗号分隔开,全部忽略使用*,但需要在*号两端加上单引号或者双引号
  prefix: /qf #访问的url地址中必须有这个前缀,否则不能访问
  routes:
    goods:
      serviceId: goods-service-prod
      path: /goods/**
      strip-prefix: false
  ratelimit:
    enabled: true
    #IN_MEMORY 内存存储 使用的ConcurrentHashMap存储,默认值
    #REDIS redis存储
    #CONSUL 使用consul存储
    #JPA 使用数据库存储
#    repository: IN_MEMORY  #对应存储类型(用来存储统计信息)
    default-policy: #针对所有的路由配置的策略,除非特别配置了policies
      # 60 秒内只允许访问100次,这100次的请求总时间不能超过200秒
      limit: 100 #每个刷新时间窗口对应的请求数量限制
      quota: 200 #每个刷新时间窗口对应的请求时间限制(秒)
      refresh-interval: 60 # 刷新时间窗口的时间,默认值 (秒)
      type: #限流方式
        - user #针对特定用户群
        - origin #针对IP地址
        - url #针对URL请求地址
      policies:
        goods: #特定的路由,与路由中的配置有关
          limit: 100 #可选- 每个刷新时间窗口对应的请求数量限制
          quota: 200 #可选-  每个刷新时间窗口对应的请求时间限制(秒)
          refresh-interval: 60 # 刷新时间窗口的时间,默认值 (秒)
          type: #可选 限流方式
            - user
            - origin
            - url

启动服务进行测试:http://localhost:14000/qf/goods/content

反复刷新,测试限流效果

8 Spring Cloud Zuul 灰度发布

8.1 api-gateway 添加依赖
<!-- 修改Zuul的负载均衡策略,也就是进行灰度发布使用的 -->
<dependency>
    <groupId>io.jmnarloch</groupId>
    <artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId>
    <version>2.1.0</version>
</dependency>
8.2 编写灰度发布过滤器
package com.qf.spring.cloud.gateway.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import io.jmnarloch.spring.cloud.ribbon.support.RibbonFilterContextHolder;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.http.HttpServletRequest;

public class GrayReleaseFilter extends ZuulFilter {

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

    public int filterOrder() {
        return 0;
    }

    public boolean shouldFilter() {
        // 进行跨域请求的时候,并且请求头中有额外参数,比如token,客户端会先发送一个OPTIONS请求来探测后续需要发起的跨域POST请求是否安全可接受
        // 所以这个请求就不需要拦截,下面是处理方式
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        if (request.getMethod().equals(RequestMethod.OPTIONS.name())) {
            //OPTIONS请求不做拦截操作
            return false;
        }
        return true;
    }

    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String requestURI = request.getRequestURI();
        AntPathMatcher matcher = new AntPathMatcher();
        if (matcher.match("/goods/**", requestURI)){
            double rate = Math.random();
            if (rate <= 0.1) {
                //也就是百分之10的请求转发到forward=1的服务上去
                RibbonFilterContextHolder.getCurrentContext().add("grayRelease", "1");//这句话就代表将请求路由到metadata-map里grayRelease为1的那个服务
            } else {
                //百分之90的请求转发到forward=2的服务上去
                RibbonFilterContextHolder.getCurrentContext().add("grayRelease", "2");//这句话就代表将请求路由到metadata-map里grayRelease为2的那个服务
            }
        }
        return null;
    }
}
8.4 goods-service 改造

goods-service01 工程的 yml 配置

eureka:
  client: #eureka客户端
    service-url:
      #对外暴露的查询服务的地址,供服务订阅者获取服务使用
      defaultZone: http://localhost:11000/eureka/
  instance:
  	instance-id: goods-service01

goods-service02 工程的 yml 配置

eureka:
  client: #eureka客户端
    service-url:
      #对外暴露的查询服务的地址,供服务订阅者获取服务使用
      defaultZone: http://localhost:11000/eureka/
  instance:
    instance-id: goods-service02
8.5 定义灰度发布条件
package com.qf.spring.cloud.gateway.gray;

import com.netflix.appinfo.InstanceInfo;
import com.netflix.loadbalancer.AbstractServerPredicate;
import com.netflix.loadbalancer.PredicateKey;
import com.netflix.loadbalancer.Server;
import com.netflix.niws.loadbalancer.DiscoveryEnabledServer;

import java.util.Arrays;
import java.util.List;

public class GrayReleasePredicate extends AbstractServerPredicate{

    //这里就是要进行灰度发布的实例ID,生产环境可以使用定时器任务将灰度发布信息缓存自redis,然后从redis中获取
    private List<String> grayReleaseServiceInstanceIds = Arrays.asList("goods-service02");

    @Override
    public boolean apply(PredicateKey predicateKey) { //包含负载均衡使用的key和一个Server实例的实体
        Server server = predicateKey.getServer();
        String instanceId;
        //DiscoveryEnabledServer表示能够被发现且具有元数据信息的Server
        if(server instanceof DiscoveryEnabledServer){
            InstanceInfo instanceInfo = ((DiscoveryEnabledServer) server).getInstanceInfo();
            System.out.println(instanceInfo);
            instanceId = instanceInfo.getInstanceId().toUpperCase();
        } else {
            System.out.println(server);
            instanceId = server.getId().toUpperCase();
        }
        if(grayReleaseServiceInstanceIds.stream().anyMatch(id->id.toUpperCase().equals(instanceId))){
            double rate = Math.random();
            System.out.println(rate + instanceId);
            return rate < 0.4;
        } else {
            return true;
        }
    }
}
8.6 定义灰度发布规则
package com.qf.spring.cloud.gateway.gray;

import com.netflix.loadbalancer.AbstractServerPredicate;
import com.netflix.loadbalancer.PredicateBasedRule;

public class GrayReleaseRule extends PredicateBasedRule {

    private GrayReleasePredicate grayReleasePredicate;
    //这个构造方法必须有,由系统来调用
    public GrayReleaseRule(){}

    //这个是我们编写的规则
    public GrayReleaseRule(GrayReleasePredicate grayReleasePredicate){
        this.grayReleasePredicate = grayReleasePredicate;
    }

    @Override
    public AbstractServerPredicate getPredicate() {
        return grayReleasePredicate;
    }
}

文章来源:https://blog.csdn.net/m0_68933188/article/details/135454829
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。