# 中小厂,其实选这套网关就够用了。

作者:小傅哥
博客:https://bugstack.cn (opens new window)

沉淀、分享、成长,让自己和他人都能有所收获!😄

大家好,我是技术UP主小傅哥。

我发现了一个很有意思的缩写单词 gwwg,都是网关的意思。因为 gw = gatewaywg = wangguan,所以在各个系统开发中,既有 gw 也有 wg 的存在。而网关也是各个互联网中用于统一对外的核心系统,当然使用网关的手段也不同,有中大厂自研,也有中小厂使用开源的组件。所以小傅哥的这个系列会陆续的分享出各个类型的网关,让大家了解以及按需选择使用。

其实只要一个公司有拆分较多的微服务,有很多的应用都要对外提供接口,就需要引入网关系统。否则就会有非常多共性功能重复在各个系统开发。比如;负载、熔断、降级、限流、切量、统一登录、地址转发等功能。这些东西要是让每个系统实现一遍,后续是非常难管理的。

那么这么多开源网关选择哪个,或者如何自研网关呢,小傅哥会陆续的分享出各个网关的介绍和使用,以及自研的教程,让大家可以积累自己的知识体系以及做技术选型。

前面已经分享了一篇 Higress (opens new window) 今天分享的是 SpringCloud Gateway

Spring Cloud Gateway 是一套非常容易使用的网关服务,通过yml配置或者代码编程的方式实现网关功能。对于网关你可以理解在接收一个 http 请求后,通过网关的配置过滤、替换、拦截、转发等操作到指定的请求地址上去。

# 一、SpringCloud Gateway 介绍

Spring Cloud Gateway 是一个基于 Spring Framework 和 Spring Boot 提供的网关解决方案。可以帮助开发者轻松地构建出具有动态路由、限流、熔断等特性的 API 网关。

  1. 基于异步非阻塞模型:Spring Cloud Gateway 基于 Project Reactor 和 WebFlux,采用了异步非阻塞的 API,可以提供更高的吞吐量和更低的延迟。
  2. 动态路由:可以通过配置文件或者 API 动态地添加、修改或删除路由规则,不需要重启服务。
  3. 路由断言:可以根据 HTTP 请求的各种参数(如路径、头部、请求参数等)来匹配路由。
  4. 过滤器:提供了一系列内置的 GatewayFilter 工厂,可以在请求被路由前或后执行各种操作,如修改请求头、增加参数、限流等。同时也支持自定义过滤器。
  5. 集成与安全:可以与 Spring Cloud 的服务发现和断路器等组件无缝集成,同时也可以集成 Spring Security 实现安全控制。
  6. 限流与熔断:可以集成如 Resilience4J 等库来实现限流和熔断功能,保护后端服务不被过多的请求压垮。
  7. 监控与跟踪:可以与 Spring Cloud Sleuth 和 Zipkin 等组件集成,实现请求的跟踪和监控。

Spring Cloud Gateway 的工作原理是将进入的 HTTP 请求根据配置的路由规则转发到对应的后端服务。它在转发请求的过程中可以执行一系列的过滤器链,这些过滤器可以修改请求和响应,或者根据特定的逻辑决定是否继续处理请求。

# 二、环境部署

# 1. 测试工程

  • 注意;本机安装了 docker 或者云服务器安装了 docker + compose。在小傅哥的 bugstack.cn (opens new window) 路书系列教程中,有云服务器操作。
  • 在小傅哥提供的案例工程中,包括;环境配置(nacos - 注册中心、redis - 限流使用)、curl 测试访问网关地址、app 是网关配置、provider-01\02 是2个测试工程,提供了2个接口,方便验转发。
  • 最后的 webflux 是使用这项技术来模拟开发网关,让大家了解到 SpringCloud Gateway 简单运行机制。

# 2. 基础环境

  • 开发环境:JDK 17
  • 云服务器:2c4g 最低,我是用的 2c8g 体验的。https://yun.xfg.plus (opens new window)- 价格实惠。
  • 基础环境:Docker、Portainer、Git 【在小傅哥的 bugstack.cn 路书中都有讲解安装和使用】
  • docker 安装 mysql 会自动根据 docker compose 脚本配置,把 nacos 需要的 sql 导入进去。
  • Phpmyadmin - MySQL 后台管理工具、redis-admin - Redis 后台管理工具。

# 3. 生产接口

xfg-dev-tech-gateway-provider-01、xfg-dev-tech-gateway-provider-02,分别提供了2个生产的 http 接口。你可以启动服务后单独访问接口测试。我们这里主要的用途是通过网关来使用这2个接口。

📢 注意以下测试,都要先启动这2个接口提供者工程。

# 3.1 生产者01

@RestController()
@CrossOrigin("*")
@RequestMapping("/api/user/")
public class HiGatewayController {

    /**
     * curl http://127.0.0.1:8091/api/user/hi
     */
    @RequestMapping(value = "hi", method = RequestMethod.GET)
    public String hi() {
        return "hello gateway,provider 01";
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 3.1 生产者01

@RestController()
@CrossOrigin("*")
@RequestMapping("/api/user/")
public class HiGatewayController {

    /**
     * curl http://127.0.0.1:8092/api/user/hi
     */
    @RequestMapping(value = "hi", method = RequestMethod.GET)
    public String hi() {
        return "hello gateway,provider 02!";
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 三、模拟网关 - webflux

基于 webflux 开发 api网关,不要引入 spring web 组件,而是引入以下组件;

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.97.Final</version>
</dependency>
1
2
3
4
5
6
7
8
9

# 1. 简单路由

@Configuration
public class GatewayRouter {

    @Bean
    public RouterFunction<ServerResponse> routeToService() {
        return RouterFunctions
                .route(GET("/service1").and(RequestPredicates.accept(MediaType.APPLICATION_JSON)),
                        request -> ServerResponse.ok().bodyValue("Response from Service 1"))
                .andRoute(GET("/service2").and(RequestPredicates.accept(MediaType.APPLICATION_JSON)),
                        request -> ServerResponse.ok().bodyValue("Response from Service 2"));
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 这是一个请求地址和返回接口的简单路由。
  • 你可以启动服务后,访问地址:http://localhost:9091/service1 http://localhost:9091/service2

# 2. 接口路由

@Configuration
public class ApiGatewayConfiguration {

    private final WebClient.Builder webClientBuilder;

    public ApiGatewayConfiguration(WebClient.Builder webClientBuilder) {
        this.webClientBuilder = webClientBuilder;
    }

    /**
     * curl http://localhost:9091/wg/service1/8091
     * curl http://localhost:9091/wg/service2/8092
     * @return
     */
    @Bean
    public RouterFunction<ServerResponse> routerFunction() {
        return route(GET("/wg/service1/{id}"), this::service1Handler)
                .andRoute(GET("/wg/service2/{id}"), this::service2Handler);
    }

    public Mono<ServerResponse> service1Handler(ServerRequest request) {
        String id = request.pathVariable("id");
        Mono<String> response = webClientBuilder.build()
                .get()
                .uri("http://127.0.0.1:8091/api/user/hi?" + id)
                .retrieve()
                .bodyToMono(String.class);
        return ServerResponse.ok().body(response, String.class);
    }

    public Mono<ServerResponse> service2Handler(ServerRequest request) {
        String id = request.pathVariable("id");
        Mono<String> response = webClientBuilder.build()
                .get()
                .uri("http://127.0.0.1:8092/api/user/hi?" + id)
                .retrieve()
                .bodyToMono(String.class);
        return ServerResponse.ok().body(response, String.class);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
  • 通过 routerFunction 对不同的请求地址进行转发操作。
  • 如图 curl http://localhost:9091/wg/service1/8091curl http://localhost:9091/wg/service2/8092 可以得到不同的响应结果。

# 四、网关测试 - SpringCloud Gateway

# 1. yml配置

spring:
  redis:
    host: 127.0.0.1
    port: 16379
    database: 0
    lettuce:
      pool:
        max-active: 10
        max-wait: 1000
        max-idle: 5
        min-idle: 3
  application:
    name: xfg-dev-tech-springcloud-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        username: nacos
        password: nacos
        locator:
          enabled: true
    gateway:
      discovery:
        locator:
          enabled: true
      globalcors:
        cors-configurations:
          '[/**]':
            allowedOrigins: "*"
            allowedMethods: "*"
            alloedHeaders: "*"
      routes:
        - id: route_01
          uri: lb://provider-01
          order: 1
          predicates:
            - Path=/gw/**
            - Weight=group1, 1
          filters:
            - StripPrefix=1
        - id: route_02
          uri: lb://provider-02
          order: 1
          predicates:
            - Path=/gw/**
            - Weight=group1, 9
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                key-resolver: "#{@ipKeyResolver}" # 限流方式:Bean名称
                redis-rate-limiter.replenishRate: 1 # 生成令牌速率:个/秒
                redis-rate-limiter.burstCapacity: 3 # 令牌桶容量
                redis-rate-limiter.requestedTokens: 1 # 每次消费的Token数量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
  • 配置 Redis 是为了使用限流组件,同时要配置 RequestRateLimiter 类,配置对应的限流 bean 名称。
  • nacos 是注册中心,网关走的是 nacos 注册中心里的服务。这些服务是生产者接口配置了 nacos 注册到注册中心了。这样就可以通过 lb://provider-02 进行访问。lb = nacos,provider-02 是生产者配置的服务名称。
  • 在 yml 配置中,一组 id: route_01 下面是对应的网关配置,以这个距离,访问 Path=/gw/** 路径,filters 过滤掉 StripPrefix=1 1个路径 gw 其余的打到 provider-01 服务上,也就是可以访问具体的服务了。另外 Weight=group1, 1 是权重配置,group1 代表这一组的,1 表示权重比。如果你不用 nacos,uri 也可以配置一个具体的 http 地址测试
  • 这些内容在 SpringCloud Gateway 官网有对应的介绍,直接按照文档配置使用即可。

# 2. 代码配置

源码cn.bugstack.xfg.dev.tech.config.RouteConfiguration

@Bean
public RouteLocator route(RouteLocatorBuilder builder, UriConfiguration uriConfiguration) {
    String httpUri = uriConfiguration.getHttp();
    return builder.routes()
            .route(p -> p.path("/baidu").uri("https://www.baidu.com/"))
            .route(p -> p.path("/bugstack").uri("https://bugstack.cn/md/road-map/road-map.html"))
            .route(p -> p.path("/error").uri("forward:/fallback"))
            .route(p -> p.path("/get").filters(f -> f.addRequestHeader("Hello", "World")).uri(httpUri))
            .build();
}
1
2
3
4
5
6
7
8
9
10
  • 除了 yml 中的配置,还可以使用代码配置。有代码配置是非常重要的,这样就可以根据写到数据库中的数据,以及提供一个管理后台来操作网关的配置了,在网关启动的时候从数据库读取数据来动态实例化所有的配置内容。

# 3. 测试验证

启动服务后,访问地址:curl http://localhost:8090/gw/api/user/hicurl http://localhost:8090/error

  • 访问后,分别可以看到不通的响应结果。

# 五、网关学习

除了业务开发,小傅哥自己也是非常感兴趣于这样的网关技术组件的实现,所以在日常的工作中也积累了很多网关的设计。后来在22年做了一套轻量的网关系统,把核心的内核逻辑实现出来让大家学习。帮助了很多伙伴学习项目后找到了不错的工作。

img

整个API网关设计核心内容分为这么五块;

  • 第一块:是关于通信的协议处理,也是网关最本质的处理内容。这里需要借助 NIO 框架 Netty 处理 HTTP 请求,并进行协议转换泛化调用到 RPC 服务返回数据信息。
  • 第二块:是关于注册中心,这里需要把网关通信系统当做一个算力,每部署一个网关服务,都需要向注册中心注册一个算力。而注册中心还需要接收 RPC 接口的注册,这部分可以是基于 SDK 自动扫描注册也可以是人工介入管理。当 RPC 注册完成后,会被注册中心经过AHP权重计算分配到一组网关算力上进行使用。
  • 第三块:是关于路由服务,每一个注册上来的Netty通信服务,都会与他对应提供的分组网关相关联,例如:wg/(a/b/c)/user/... a/b/c 需要匹配到 Nginx 路由配置上,以确保不同的接口调用请求到对应的 Netty 服务上。PS:如果对应错误或者为启动,可能会发生类似B站事故。
  • 第四块:责任链下插件模块的调用,鉴权、授信、熔断、降级、限流、切量等,这些服务虽然不算是网关的定义下的内容,但作为共性通用的服务,它们通常也是被放到网关层统一设计实现和使用的。【这块内容可以自行扩展】
  • 第五块:管理后台,作为一个网关项目少不了一个与之对应的管理后台,用户接口的注册维护、mock测试、日志查询、流量整形、网关管理等服务。

项目学习地址:https://bugstack.cn/md/assembly/api-gateway/api-gateway.html