服务网关Gateway

为什么需要服务网关呢?

统一入口,收口所有外部请求,把鉴权、限流、路由、日志、跨域这些公共能力抽离到网关,下游微服务只专注业务,同时保护后端服务不暴露给外网。

为什么是Gateway?

ta使用了webflux中的reactor-netty响应式编程组件,底层使用了netty通讯框架。

Gateway三大核心功能:路由(route)、断言(predict)、过滤(Fliter)

技术选型--------为什么选择了Gateway而不是zuul?

Sringcloud 中所集成的 Zuul 版本,采用的是 Tomcat 容器,使用的是传统的 Servlet IO 处理模型。

Servlet 的生命周期:servlet 由 servlet container 进行生命周期管理。container 启动时构造 servlet 对象并调用 servlet init () 进行初始化;container 运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用 service ()。container 关闭时调用 servlet destory () 销毁 servlet;

上述模式的缺点:

servlet是一个简单的网络IO模型,当请求进入servlet container时,servlet container就会为其绑定一个线程,在并发不高的场景下这种模型是适用的。但是一旦高并发(比如抽风用jemeter压),线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。 在一些简单业务场景下,不希望为每个request分配一个线程,只需要1个或几个线程就能应对极大并发的请求,这种业务场景下servlet模型没有优势

所以Zuul 1.X是基于servlet之上的一个阻塞式处理模型,即spring实现了处理所有request请求的一个servlet(DispatcherServlet)并由该servlet阻塞式处理处理。所以Springcloud Zuul无法摆脱servlet模型的弊端

Gateway是基于 Spring Framework 5, Project Reactor 和 Spring Boot 2.0 进行构建的。

传统的Web框架,比如说:struts2,springmvc等都是基于Servlet API与Servlet容器基础之上运行的。 但是 在Servlet3.1之后有了异步非阻塞的支持。而WebFlux是一个典型非阻塞异步的框架,它的核心是基于Reactor的相关API实现的。相对于传统的web框架来说,它可以运行在诸如Netty,Undertow及支持Servlet3.1的容器上。非阻塞式+函数式编程(Spring5必须让你使用java8)

Spring WebFlux 是 Spring 5.0 引入的新的响应式框架,区别于 Spring MVC,它不需要依赖Servlet API,它是完全异步非阻塞的,并且基于 Reactor 来实现响应式流规范。

由于Zuul2迟迟未发布,且核心团队从网飞离职等原因,Spring团队自己操刀了Gateway。

Spring Cloud Gateway 具有如下特性:

  1. 动态路由:能够匹配任何请求属性; 可以对路由指定 Predicate(断言)和 Filter(过滤器);
  2. 集成Hystrix的断路器功能;
  3. 集成 Spring Cloud 服务发现功能; 易于编写的 Predicate(断言)和 Filter(过滤器);
  4. 请求限流功能;
  5. 支持路径重写。

三大核心功能

概念 说明
Route(路由) 路由是构建网关的基本模块,它由 ID、目标 URI、一系列的断言和过滤器组成,如断言为 true 则匹配该路由
Predicate(断言) 参考的是 Java 8 的 java.util.function.Predicate,开发人员可以匹配 HTTP 请求的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
Filter(过滤) 指的是 Spring 框架中 GatewayFilter 的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改

SpringCloud-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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.atguigu.springcloud</groupId>
        <artifactId>SpringCloud-01</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <groupId>com.atguigu</groupId>
    <artifactId>cloud-gateway-gateway9527</artifactId>

    <name>cloud-gateway-gateway9527</name>
    <description>cloud-gateway-gateway9527</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <!--gateway无需web和actuator-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!--监控-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--自定义api通用包-->
        <dependency>
            <groupId>com.atguigu.springcloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

注意:不要引入web-starter依赖,引入会报错提示删除

复制代码
server:
  port: 9527
spring:
  application:
    name: cloud-gateway
  cloud:
    gateway: 
      routes:
        - id: payment_route # 路由的id,没有规定规则但要求 唯一 ,建议配合服务名
          #匹配后提供服务的路由地址
          uri: http://localhost:8001
          predicates:
            - Path=/payment/get/** # 断言,路径相匹配的进行路由

//拼起来就是lcoalhost:8001/payment/get/**
//predicates断言,与@Getmapping 写的路径相配合起来

        - id: payment_route2
          #uri: http://localhost:8001
          uri: lb://CLOUD-PAYMENT-SERVICE
          predicates:
            - Path=/payment/lb/** #断言,路径相匹配的进行路由
            - After=2020-03-09T22:40:37.365+08:00[Asia/Shanghai]
eureka:
  instance:
    hostname: cloud-gateway-service
  client:
    #表示向注册中心注册自己 默认为true
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true,单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      # 入驻地址
      # defaultZone: http://localhost:7001/eureka/
      #集群版
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/

代码注入RouteLocator的Bean进行路由配置

复制代码
@Configuration
public class GateWayConfig {

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder){
        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
        routes.route("path route atguigu",
                r ->r.path("/guonei").uri("https://www.baidu.com")).build();
        return routes.build();
    }
}

localhost:9527/guonei 就可访问百度的新闻主页。

动态路由

默认情况下Gateway会根据注册中心注册 的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能。

ps: 由网关来实现负载均衡的功能,但是考虑到后期要进行服务的扩容,所以不能够写死URL地址。

复制代码
server:
  port: 9527
spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名称进行路由
      routes:
        - id: payment_route # 路由的id,没有规定规则但要求唯一,建议配合服务名
          #匹配后提供服务的路由地址
          //采用在Eureka中注册的地址
          uri: lb://CLOUD-PAYMENT-SERVICE
          predicates:
            - Path=/payment/get/** # 断言,路径相匹配的进行路由
        - id: payment_route2
          //重点是下方url
          uri: lb://CLOUD-PAYMENT-SERVICE
          predicates:
            - Path=/payment/lb/** #断言,路径相匹配的进行路由
            
eureka:
  instance:
    hostname: cloud-gateway-service
  client:
    #表示向注册中心注册自己 默认为true
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true,单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      # 入驻地址
      # defaultZone: http://localhost:7001/eureka/
      #集群版
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/

断言

Spring Cloud Gateway将路由匹配作为Spring WebFlux HandlerMapping基础架构的一部分。 Spring Cloud Gateway包括许多内置的Route Predicate工厂。所有这些Predicate都与HTTP请求的不同属性匹配。多个Route Predicate工厂可以进行组合

Spring Cloud Gateway 创建 Route 对象时, 使用 RoutePredicateFactory 创建 Predicate 对象,Predicate 对象可以赋值给 Route。 Spring Cloud Gateway 包含许多内置的Route Predicate Factories。

所有这些谓词都匹配HTTP请求的不同属性。多种谓词工厂可以组合,并通过逻辑and。

以上为官网给予的解释。坦率的说断言就是在访问通过之前设置规则如果通过就允许访问。

断言的十一种规则。

复制代码
 cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名称进行路由
      routes:
        - id: payment_route # 路由的id,没有规定规则但要求唯一,建议配合服务名
          #匹配后提供服务的路由地址
          #uri: http://localhost:8001
          uri: lb://CLOUD-PAYMENT-SERVICE
          predicates:
            - Path=/payment/get/** # 断言,路径相匹配的进行路由
        - id: payment_route2
          #uri: http://localhost:8001
          uri: lb://CLOUD-PAYMENT-SERVICE
          predicates:
            - Path=/payment/lb/** #断言,路径相匹配的进行路由
            - After=2020-03-09T22:40:37.365+08:00[Asia/Shanghai]

0.curl

最简单的curl模板

复制代码
curl http://网关ip:端口/路径

1.After、2.before、3.between

After: 在设定的时间之后才允许访问。项目版本更新之类为适用场景

Before:在设定的时间之前允许访问。

between: 在设定的时间段内允许访问。

between需要同时写出两个时间戳

手搓时间戳容易出错,在test文件夹生成一个测试类,等待时间打印出来之后把时间粘贴到yml中

4.Cookie Route Predicate

含义: 请求必须携带 chocolate 这个 Cookie,并且它的值满足正则 ch.p ch.p 正则:ch 开头,中间任意一个字符,p 结尾.

如果把正则写成全匹配

复制代码
# 精确匹配:cookie名字叫token,值必须等于abc123
- Cookie=token, ^abc123$

完整写法(一般用不到):

复制代码
predicates:
  - name: Cookie
    args:
      name: token
      regexp: ^abc123$

不带cookie进行测试访问

带cookie进行测试访问

5.Header Route Predicate

6.Host Route Predicate

7.Method Route Predicate

8.Path Route Predicate

复制代码
 cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名称进行路由
      routes:
        - id: payment_route # 路由的id,没有规定规则但要求唯一,建议配合服务名
          #匹配后提供服务的路由地址
          #uri: http://localhost:8001
          uri: lb://CLOUD-PAYMENT-SERVICE
          predicates:
            - Path=/payment/lb/** # 断言,路径相匹配的进行路由
            - Method=GET

访问的URL地址必须有payment/lb,并且方法必须是Get方法进行请求

9.Query Route Predicate

10.RemoteAddr Route Predicate

作用:根据请求客户端的 IP 地址(Remote Address)来匹配路由,只放行指定网段 / IP 的请求。

参数:CIDR 表示法(IP / 掩码),不是单纯写一个 IP。 格式:ip地址/前缀长度,例如 192.168.1.0/24、127.0.0.1/32

复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: remoteaddr_route
          uri: https://example.org
          predicates:
            - RemoteAddr=192.168.1.1/24

含义:来源 IP 属于 192.168.1.0 ~ 192.168.1.255 这个网段,才匹配这条路由。

127.0.0.1/32 代表只允许本机 127.0.0.1 这一个 IP(32 位掩码,精确单个 IP)

常用案例1:仅允许本机访问

复制代码
predicates:
  - RemoteAddr=127.0.0.1/32

常用案例2:允许整个内网 192.168.3.x 网段

复制代码
predicates:
  - RemoteAddr=192.168.3.0/24

常用案例3:多个 IP / 网段(写多个 RemoteAddr):

复制代码
# 正确:允许 127.0.0.1 或者 192.168.1.0/24
predicates:
  - RemoteAddr=127.0.0.1/32,192.168.1.0/24

11.Weight Route Predicate权重路由断言工厂

作用:同一分组下的多条路由,按配置的权重比例随机分流流量,用来做灰度发布、流量染色Spring 参数:分组名(group), 权重数字(weight)

✅ 重点:权重是按同一个group分组计算,只有group名字相同的路由,才会一起参与流量分配。

官方标准示例

复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: weight_high
          uri: [https://weighthigh.org](https://weighthigh.org)
          predicates:
            - Weight=group1, 8   # group1组,权重8
        - id: weight_low
          uri: [https://weightlow.org](https://weightlow.org)
          predicates:
            - Weight=group1, 2   # group1组,权重2

总权重 = 8 + 2 = 10

  • 8/(8+2) = 80% 流量走 weighthigh.org
  • 2/(8+2) = 20% 流量走 weightlow.org 注意:是概率随机,不是严格精确均分,大量请求才会趋近这个比例,少量curl测试的时候波动会很大

业务实战案例(微服务灰度,最常用)

把90%流量打给老版本,10%流量给新版本灰度服务

复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: service-old
          uri: lb://service-order-old
          predicates:
            - Path=/order/**
            - Weight=order-group, 9
        - id: service-new
          uri: lb://service-order-new
          predicates:
            - Path=/order/**
            - Weight=order-group, 1

含义:访问 /order/** 的请求,大约90%到老版本,10%到新版本。

🧪 curl测试

直接循环curl多次观察返回结果,看不同后端响应:

复制代码
for i in {1..20};do curl [http://127.0.0.1:8080/order/info;echo;done](http://127.0.0.1:8080/order/info;echo;done)

坑:只发2、3次请求,很可能全落到同一个服务,随机概率问题,不是配置失效。

⚠️ 高频踩坑(面试常问)

  1. group不一样,互相不影响 group1 和 group2 是两套独立的权重池,不会互相计算比例。
  2. 必须配合其他断言(Path等)一起用 如果只写Weight,没有Path,会匹配所有请求,容易乱路由。
  3. 和LoadBalancer负载均衡不是一回事!
    • Weight断言:网关层面,把流量分到不同uri/不同服务(灰度、切版本)
    • lb负载均衡:同一个服务名下,多个实例之间分摊流量
  4. 无粘性!默认是随机,同一个用户连续请求可能跳到不同版本 如果想要用户固定走同一个版本(会话保持),Weight断言本身做不到,要配合Cookie断言实现灰度粘流。

进阶组合:Cookie + Weight(常用灰度方案)

复制代码
# 带cookie=gray的用户,全部走新版本;其余流量按权重分流
- id: gray-user
  uri: lb://service-order-new
  predicates:
    - Path=/order/**
    - Cookie=gray, ^true$

- id: normal-old
  uri: lb://service-order-old
  predicates:
    - Path=/order/**
    - Weight=order-group, 9

- id: normal-new
  uri: lb://service-order-new
  predicates:
    - Path=/order/**
    - Weight=order-group, 1

和前面几个Predicate对比小结

  • After:时间条件
  • Cookie:请求cookie匹配
  • RemoteAddr:客户端IP网段
  • Weight:流量概率分流(灰度专用)

Weight断言就是同组路由抽签分流,适合灰度放量;它不是负载均衡,是网关级别的流量切分。

过滤器Gateway------Flier

Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。

类似于servlet中的过滤器,也可以形成过滤器链。生命周期分为pre, post。种类划分为单一GatewayFliter 全局GlobalFiter

全局过滤器官网教学地址:GlobalFliter

自定义过滤器

复制代码
package com.atguigu.springcloud.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Date;

/**
 * 全局自定义过滤器
 *判断是否带有姓名,如果有名就允许通过第一个过滤器链,之后可以把exchange携带的参数传递给下一个链条
 */
@Component
@Slf4j
public class MyLogGatewayFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("come in global filter: {}", new Date());

        ServerHttpRequest request = exchange.getRequest();
        String uname = request.getQueryParams().getFirst("uname");
        if (uname == null) {
            log.info("用户名为null,非法用户");
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }
        // 放行
        return chain.filter(exchange);
    }

    /**
     * 过滤器加载的顺序 越小,优先级别越高
     *
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}
相关推荐
灿飞屋37 分钟前
Kotlin与Java虚拟机四:函数
java·python·kotlin·安卓·android jetpack
分布式存储与RustFS1 小时前
OpenSearch 的索引快照别只留本地:用 repository-s3 存进 RustFS
运维·云原生·开源·对象存储·分布式存储·s3·性能基准
阡陌数智2 小时前
Claude API新版本重大变更与安全升级指南
java·linux·安全
瑞克-桑切斯2 小时前
docker Harbor 单机部署手册
大数据·运维·elasticsearch·docker·容器
奶茶树2 小时前
【Linux】环境开发工具(vim,gcc,g++,Makefile)
linux·运维·vim
机 _ 长2 小时前
05 智算运维|8 张卡怎么才算「捆成一张」:NCCL 实测与拓扑
运维·人工智能·算法·智算运维
学长毕设3 小时前
基于SpringBoot的野生动物公益保护平台设计与实现-附源码
java·spring boot·后端
andxe3 小时前
工业级 PoE 交换机选型指南:破解边缘部署四大核心难题
运维·网络·安全·光通信·交换机
Decay__4 小时前
14_MySQL 高可用与容灾架构 —— MGR / InnoDB Cluster / Keepalived+VIP / 双主 / 读写分离 / 两地三中心
运维·数据库·sql·mysql·架构