思月@开源^ ^

2流高手速成记(之九):基于SpringCloudGateway实现服务网关功能

咱们接上回 

上一节我们基于Sentinel实现了微服务体系下的限流和熔断,使得整个微服务架构的安全性和稳定性上升了一个台阶

篇尾我们引出了一个问题,众多的微服务节点,我们如何部署才能满足客户端简洁高效的访问需求?

—— 今天我们就来引入服务网关的概念

什么是服务网关?

服务网关是微服务体系下唯一的流量入口,对内实现内部架构统合,所有外来请求都要经由网关路由到对应的微服务节点,进而实现完整的业务逻辑

由于是每个外部请求的必经之路,因此除了路由之外,服务网关还可以胜任几乎所有的横切面功能,比如我们上一节提到的限流、熔断,以及统一的认证服务、日志监控等

使用服务网关的优势:

1. 客户端简化 —— 加入服务网关之后,客户端只需要知道服务网关的访问地址即可,而不再需要了解每个微服务的访问端口

2. 降低耦合度 —— 其他微服务节点有变动,我们只需要灵活调整服务网关的路由即可,不必每次都去修改客户端

3. 提升可维护性 —— 由服务网关统一实现路由、灰度发布、负载均衡、限流熔断等机制,开发人员更专注于业务实现

引入服务网关带来的弊端:

微服务架构讲求去中心化,而网关的引入使之变成了唯一的单点,其高可用性和可维护性变成了必须要解决的课题

如下是服务网关的基本功能以及常见的几种服务网关的对比:(图示来自:CSDN 张维鹏,感谢)

 我们本节重点关注 SpringCloudGateway 的用法,其他感兴趣大家可以自行了解

新建nacos-sentinel-gateway模块

我们首先创建一个新模块并引入如下依赖项:

很多依赖项我们已经很熟悉了,这里重新说明一下他们的用途:

Nacos Service Discovery —— 基于Nacos实现服务发现

Nacos Configuration —— 基于Nacos实现配置中心

Spring Cloud Alibaba Sentinel —— Sentinel限流熔断组件,我们上一节的主要内容

Cloud Bootstrap —— bootstrap.yml 加载机制,实现Nacos云配置的关键

Gateway —— 即 SpringCloudGateway,我们本节关注的重点

Spring Cloud Alibaba Sentinel Gateway —— Sentinel组件对于SpringCloudGateway的适配机制,本节后半部分我们要用到

值得注意的是:SpringCloudGateway内部直接包含了spring-boot-starter-web依赖,如果你的工程中同时包含二者的引用,会报冲突错误

解决的办法是添加spring-cloud-starter-gateway的同时排除掉spring-boot-starter-web

<!-- 引入gateway网关 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </exclusion>
    </exclusions>
</dependency>

由于本节中不牵扯spring-boot-starter-web的使用,所以不存在这个问题,如下是本节的设计到的各个依赖项在pom中的声明:

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

项目工程目录如下:

   

本节相较于之前第一个不同点来了,配置文件不再是.properties,而是变成了.yml,后者比前者的表达能力更强,且比xml更简洁

.yml是yaml的一个变种(基本完全一样),非常适合作为配置文件,它不但可以表示变量,还可以声明数组及字典

这里给大家提供一个可以在线将.properties转换为.yml的工具,非常的方便,感谢BeJSON站长

====================================

在线properties转yaml、yml工具 - BeJSON.com

====================================

如下是转换完毕后的bootstrap.yml

spring:
  cloud:
    nacos:
      config:
        username:nacos
        password:nacos
        contextPath:/nacos
        server-addr:127.0.0.1:8848

内容依然仅是nacos的声明,为便于测试效果,本节使用了本地的application.yml,因此bootstrap.yml中并未包含spring.application.name的声明

而如果我们打算将application.yml托管给nacos,则必须在bootstrap.yml中声明spring.application.name

然后我们来看application.yml的内容,这将是本节的重点

spring:
  application:
    name: nacos-sentinel-gateway
  cloud:
    gateway:
      enabled: true
      routes:
        - id: dubbo-nacos-consumer
          uri: http://127.0.0.1:8080
          order: 1
          predicates:
            - Path=/consumer/**
          filters:
            - StripPrefix=1
server:
  port: 8081

到这里,网关服务就可以直接跑起来了

啥?还没写代码呢!是的,一句代码不用写,来一个配置文件,你的服务网关就已经搭建起来了,非常方便对不?

我们分析下gateway的相关配置,不难看出其中的端倪:

1. route(路由)是gateway的基本配置单元,说白了就是搭建网关从设定路由规则开始

2. 每个route都包含四部分:

id —— 路由标识,自行命名,不重复即可

uri —— 请求转发的真实目标地址

order —— 优先级,数字越小代表优先级越高

predicates —— 断言(数组),用于判定转发规则是否成立

filters —— 过滤器(数组),请求url ---> 过滤器处理 --->目标url,定义了url的变换规则

我们以此来解读一下上述的网关路由到底定义了一个怎样的转发规则

id —— 和我们之前构建的dubbo-nacos-consumer同名,那下述转发规则自然与其有关

uri —— dubbo-nacos-consumer监听8080端口,因此我们打算将请求转发给dubbo-nacos-consumer服务

order —— 1代表高优先级

predicates —— Path=/consumer/**,代表请求路径中需要包含/consumer/的前缀,也就说我们会将路径中包含/consumer/的请求转发给dubbo-nacos-consumer服务

filters —— StripPrefix=1,是我们以后经常会遇到的一种过滤器,含义为过滤一级前缀,这里的前缀就是上边提到的/consumer前缀

这是什么意思?

我们网关监听的端口是8081,

假定我们的请求为 http://127.0.0.1:8081/consumer/person/select ,

因为满足包含前缀/consumer/的设定,请求被转发到 http://127.0.0.1:8080/consumer/person/select ,

而过滤器会过滤掉/consumer前缀,于是请求路径变为 http://127.0.0.1:8080/person/select

而这正是dubbo-nacos-consumer中的PersonController.select方法的有效映射

我们依次启动先前创建的 dubbo-nacos-provider 、dubbo-nacos-consumer 以及我们刚创建好的 nacos-sentinel-gateway

打开post执行请求验证下结果

结果证实了我们的猜想,这样一来我们就简单实现了服务网关的路由功能

虽然功能是实现了,但是你有没有发现,我们是直接指定了转发的目标地址,这种模式配置的路由规则存在几个非常大的弊端:

1. 网关必须知道所有微服务的地址,并且逐一配置

2. 微服务地址有任何变动,网关必须做同步更改

3. 无法实现负载均衡

那么有没有其他配置方式可以避开这些不足?当然是有的!

基于Nacos服务发现实现负载均衡

我们将app.yml的内容做如下改动:

spring:
  application:
    name: nacos-sentinel-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    gateway:
      enabled: true
      routes:
        - id: dubbo-nacos-consumer
          uri: lb://dubbo-nacos-consumer
          predicates:
            - Path=/consumer/**
          filters:
            - StripPrefix=1
server:
  port: 8081

相比于前一个版本,我们有两个地方的改动

首先,我们加入了基于Nacos的服务发现配置:

  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

其次,我们原有固定的转发地址改为了另一种形式:uri: lb://dubbo-nacos-consumer

这种模式遵循的固定格式为:lb://service-name

lb —— load balancing的缩写,即负载均衡

service-name —— Nacos中注册的服务名称,注意这里是名称,而并非ip

lb://dubbo-nacos-consumer 代表如果断言成立,则请求将均衡的分发至各个dubbo-nacos-consumer的服务实例

我们先在post中执行验证下结果:

 

结果跟前一个版本是一样的,印证了这种配置模式是有效的

但是由于我们的dubbo-nacos-cosumer服务只有一个实例,所以看不出来负载均衡的效果

幸运的是,在idea之下我们可以非常方便的配置另一个启动实例

 

 

为了便于对比,我们先把consumer的application.properties配置文件迁移回本地,并删除nacos的云端配置

而后我们创建另一个新的配置文件application-anotherr.properties,内容如下:

# 应用名称
spring.application.name=dubbo-nacos-consumer
# dubbo 协议
dubbo.protocol.id=dubbo
dubbo.protocol.name=dubbo
# dubbo 协议端口( -1 表示自增端口,从 20880 开始)
dubbo.protocol.port=-1
# Dubbo 消费端订阅服务端的应用名,多个服务提供者用逗号分隔
dubbo.cloud.subscribed-services=dubbo-nacos-provider
# dubbo 服务扫描基准包
dubbo.scan.base-packages=com.example.dubbonacosconsumer
# Nacos帮助文档: https://nacos.io/zh-cn/docs/concepts.html
# Nacos认证信息
spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
# Nacos 服务发现与注册配置,其中子属性 server-addr 指定 Nacos 服务器主机和端口
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# 注册到 nacos 的指定 namespace,默认为 public
spring.cloud.nacos.discovery.namespace=public

# 应用服务 WEB 访问端口
server.port=8098

# sentinel看板配置
spring.cloud.sentinel.transport.dashboard = 127.0.0.1:9999
# 开启对sentinel看板的饥饿式加载。sentinel默认是懒加载机制,只有访问过一次的资源才会被监控,通过关闭懒加载,在项目启动时就连接sentinel控制台
spring.cloud.sentinel.eager = true

内容与application.properties基本相同,区别仅在于

# 应用服务 WEB 访问端口
server.port=8098

我们知道,同一台机器,同一个监听端口智能使用一次,因此如果想借用idea直接在开发环境下起另一个服务实例,则需要开启另一个不同的端口

然后我们创建一个新的启动配置

 

 

 

 

最关键的一句配置:

--spring.profiles.active=another

代表我们指定程序的启动配置文件为:application-anotherr.properties

为了与原始的consumer做区分,我们先启动consumer,然后改一下consumer的PersonController内容,再启动another-consumer

    @GetMapping("/select")
    @SentinelResource(value = "person/select", blockHandler = "selectBlock")
    public SelectRetVo select() {
        SelectRetVo vo = new SelectRetVo();
        vo.setPersons(service.select());
        vo.setError("ok-another"); // another的区别
        return vo;
    }

此刻我们打开Nacos的服务中心,会看到dubbo-nacos-consumer存在两个运行中的实例

 

然后我们再次通过Post访问网关验证结果

 

 

返回结果中的 ok 和 ok-another 会交替出现

看到效果了吗?原始consumer和another-consumer都是动态启动的,而基于Nacos服务发现机制,

gateway在无感知的情况下,完全不做任何改动,就实现了多个consumer实例的动态负载均衡,是不是非常便捷?

那除了 lb://service-name 这种方式,还有没有更简单的办法?当然是有的!

基于Naocs的全自动路由配置

我们再次改动gateway的app.yml配置文件内容

spring:
  application:
    name: nacos-sentinel-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    gateway:
      enabled: true
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
server:
  port: 8081

这一次我们没有特别指定下游节点的ip,也没有配置相关的负载均衡,那这样写实现了什么效果?

我先给大家看Post的测试结果:

 

我们来和前边的负载均衡示例做下对比:

前次的访问地址:http://localhost:8081/consumer/person/select

本地的访问地址:http://localhost:8081/dubbo-nacos-consumer/person/select

看到区别了吗?这样写相当于 lb://默认spring.application.name

仅是这样吗?其实还不止!仔细看,本次配置中我们并没有指定特定的service-name开负载均衡!

也就是说这种模式之下,所有注册到Nacos服务中心的外部可访问Web节点实例均启动负载均衡设定!

不信你可以仿照刚刚的consumer,自己再起一组支持web访问的实例,随便写几个controller测试方法,结果一试便知,gateway同样满足无感知访问

 

我们在最一开始分析引入网关的利弊时,曾提到了非常关键的一点 —— 网关是流量的唯一入口,也是整个微服务体系中的“单点”,其稳定性的保障之于整个体系的重要性而言自然不言而喻

那我们能否像上一节一样,基于Sentinel的限流机制对其稳定性实现一个很好的保护呢?当然是可以的!

基于Sentinel实现SpringCloudGateway限流保护

首先,我们像上一节一样,在application.yml中引入Sentinel相关配置

spring:
  application:
    name: nacos-sentinel-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    sentinel:
      transport:
        dashboard: 127.0.0.1:9999
      eager: true
    gateway:
      enabled: true
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      routes:
        - id: dubbo-nacos-consumer
          uri: lb://dubbo-nacos-consumer
          predicates:
            - Path=/consumer/**
          filters:
            - StripPrefix=1
server:
  port: 8081

这是本节application.yml的最终版本,大家可以详细看下完整的内容

这样一来我们直接启动gateway,是否就能像上一节一样,直接在sentinel-dashboard中对其进行限流设定了呢?是的,但是我们这里要多加一步设置

我们需要对项目的启动文件稍加改动:

package com.example.nacossentinelgateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class NacosSentinelGatewayApplication {

    public static void main(String[] args) {
        System.setProperty("csp.sentinel.app.type", "1");
        SpringApplication.run(NacosSentinelGatewayApplication.class, args);
    }

}

这里多出了一句代码

System.setProperty("csp.sentinel.app.type", "1");

这句代码的意义在于显式声明该应用为网关类型,这样做的目的是什么?我们打开sentinel-dashboard的面板便一目了然

  

 

 

左侧为我们上一节用到的consumer,右侧为网关应用类型的gateway,看到区别了吗?

Sentinel针对SpringCloudGateway提供了更加有效便捷的链路流控管理,以满足gateway在整个微服务体系下“单点”的定位

Sentinel从1.6版本开始对SpringCloudGateway提供了适配支持,并提供两种维度的资源限流:

route维度 —— 路由限流一般针对的是整个体系中的某一类微服务,所以这属于一种粗粒度的限流方式

自定义api分组 —— 针对某一类uri,按照实现定义好的规则进行匹配限流,可以横跨多个微服务,这种模式结合了gateway的匹配规则和sentinel的安全机制两大优势

我们先来看第一种,基于路由的限流模式,打开流控规则板块:

 

 

大部分的设置与上一节中讲到的限流设定基本如出一辙,我们来看不一样的地方:

API名称 —— 路由限流模式下,这里直接填路由ID即可,比如第二种lb模式下我们定义的dubbo-nacos-consumer

参数属性及匹配 —— 更加详细的热点匹配规则,可以针对特定参数进行专属定制,这里我们不展开讨论,大家感兴趣自行尝试即可

这样以来,gateway便完成了对于下游consumer(负载均衡)的限流设定,效果等同于我们上一节consumer对于provider的访问限流,是不是非常方便?

接下来我们来看下自定义API分组的限流设置,我们打开API管理板块:

 

 

API名称 —— 自定义,方便辨识即可,后续使用API限流时根据名称进行选取

匹配模式包含三种:

精确 —— uri完全匹配

前缀 —— uri前缀匹配即可

正则 —— uri满足正则表达式

之后我们再打开流控规则板块:

 

 

类型我们选择API分组,名称直接下拉框选取我们刚刚新建的自定义API分组即可

这样gateway便可以根据我们的设定针对特定API分组进行精准限流

还余留下一个问题:流控异常直接默认返回block肯定是不满足业务要求的,那么我们如何自定义流控的异常信息呢?

gateway没有任何代码怎么办?别担心,同样是靠配置实现:

spring:
  cloud:
    sentinel:
      #配置限流之后的响应内容
      scg:  
        fallback:
          # 两种模式:一种是response返回文字提示信息,一种是redirect,重定向跳转,需要同时配置redirect(跳转的uri)
          mode: response
          # 响应的状态
          response-status: 426
          # 响应体
          response-body: '{"code": 426,"message": "限流了,稍后重试!"}'

这样一来,一旦网关限流,则返回内容将为我们自定义的Json串:

{
    "code": 426,
    "message": "限流了,稍后重试!"
}

重定向一样很好实现:

spring:
  cloud:
    sentinel:
      #配置限流之后的响应内容
      scg:
        fallback:
          ## 两种模式,一种是response返回文字提示信息,一种是redirect,重定向跳转,需要同时配置redirect(跳转的uri)
          mode: redirect
          ## 跳转的URL
          redirect: http://www.baidu.com

网关一旦限流将自动跳转到百度首页

到这里,如何基于Sentinel对SpringCloudGateway实施限流保护就讲完了

 

本节中我们对SpringCloudGateway分别进行了 从特定ip配置,到指定服务的负载均衡,再到全自动动态负载均衡 的一个统一的介绍,最后结合上一节讲到的Sentinel系统讲述了SpringCloudGateway体系的限流保护

其实SpringCloudGateway的使用还远不止这些,对 断言 和 过滤器 更加细化的定制还可以帮我们过滤掉各种场景下的外部非法访问,从而对整个内部集群的稳定性起到一个更加的有效的防护作用

这里给大家推荐几篇文章,各位感兴趣可以参考:

Spring Cloud Gateway 服务网关的部署与使用详细介绍_张维鹏的博客-CSDN博客_spring cloud gateway

Spring Cloud Gateway 整合 sentinel 实现流控熔断_张维鹏的博客-CSDN博客_gateway sentinel

感谢原创作者!

 

接下来我们仍然要思考一个问题 —— 网关限流,微服务体系下外部的不稳定因素被截断了,这样就安全了吗?答案显然不是!

我们面临的业务需求是丰富多样的,集群内部同样可能出现性能消耗的热点

那么当问题发生在微服务体系内部时,我们如何实现问题及热点的精准定位呢?请看下回 —— 基于Spring Cloud Sleuth的链路追踪,敬请期待~

 

posted @ 2022-11-22 12:29  14号程序员  阅读(649)  评论(0编辑  收藏  举报