如何实现 SpringCloud Gateway 整合 Sentinel 流控规则,并持久化到 Nacos

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

点击关注公众号,实用技术文章及时了解如何实现 SpringCloud Gateway 整合 Sentinel 流控规则,并持久化到 Nacos


开始

文档目的

原来想通过整合Sentinel,对spring cloud gateway请求进行流控;在Sentinel界面中修改和增加流控规则,同步到nacos。

百度有很多文章,但是实践下来没有一个能够实现我想要的结果,于是决定在前人的基础上研究,终于初步达成了目的。由于本人水平有限,有些概念没有深入了解,请见谅!

版本信息

name version desc
spring boot 2.7.3
spring cloud 2021.0.3
io.springfox 3.0.0
knife4j 3.0.2
com.alibaba.cloud 2021.1
nacos 2.1.0 cluster mode
sentinel 1.8.4
mysql 5.7.22

整合

nacos 集群模式配置(一台机器)

  • 配置3个nacos服务,目录如下:
如何实现 SpringCloud Gateway 整合 Sentinel 流控规则,并持久化到 Nacos
  • 配置端口和数据库连接
### nacos1
### Default web server port:
server.port=8840

### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=XXXX
db.password.0=XXXXXX

同样,将nacos2的server.port改为8850;nacos3的server.port改为8860

  • nacos1、nacos2、nacos3三个实例的cluster.conf修改
#2022-09-19T08:54:21.232
172.20.11.5:8840
172.20.11.5:8850
172.20.11.5:8860

注意:集群模式下一定要配置为真实的ip加端口,不能用127.0.0.1或者localhost代替;后面为spring cloud gateway配置nacos时候,一定要写真实的ip,否则会报以下的错误:

com.alibaba.nacos.api.exception.NacosException: failed to req API:/nacos/v1/ns/instance 
after all servers([http://127.0.0.1:8848])

由于机器资源限制,实际我只启动了nacos1一个实例。

spring cloud gateway网关搭建

server:
  port: 9000
spring:
  application:
    name: gateway-server
  main:
    allow-bean-definition-overriding: true
  config:
    activate:
      on-profile: ${SPRING_PROFILES_ACTIVE:dev}
  cloud:
    nacos:
      config:
        server-addr: ${NACOS_SERVER_ADDR:172.20.11.5}:8840  # 配置中心真实ip
        file-extension: yaml
        namespace: middle-${SPRING_PROFILES_ACTIVE:dev}
        group: ${SPRING_PROFILES_ACTIVE:dev}
        refresh-enabled: true
      discovery:
        server-addr: ${NACOS_SERVER_ADDR:172.20.11.5}:8840  #服务注册和发现中心,和配置中心使用同一个变量
                                                            #集群模式:localhost:8848,localhost:8849,localhost:8850
        namespace: middle-${SPRING_PROFILES_ACTIVE:dev}
        group: ${SPRING_PROFILES_ACTIVE:dev}
    gateway:
      globalcors:
        cors-configurations: #允许跨域请求
          '[**]':
            allowedOrigins: '*'
            allowedMethods: '*'
      discovery:
        locator:
          enabled: true
    sentinel: # sentinel 整合
      transport:
        dashboard: ${SENTINEL_DASHBOARD_ADDR:172.20.11.5}:8800 #sentinel控制台访问路径
      filter:
        enabled: false #心跳启动
      datasource:
        ds:
          nacos: # 整合nacos,把流控规则保存到nacos, sentinel控制台启动时候就能够读取这个配置
            server-addr: ${NACOS_SERVER_ADDR:172.20.11.5}:8840
            dataId: ${spring.application.name}-sentinel-flow # 为什么会这样设置,后面会讲到
            namespace: middle-${SPRING_PROFILES_ACTIVE:dev}
            groupId: ${SPRING_PROFILES_ACTIVE:dev}
            rule-type: gw_flow # 流控配置
      eager: true
management:
  endpoints:
    web:
      exposure:
        include: '*' #shutdown,health,info,loggers,gateway,sentinel
  endpoint:
    shutdown:
      enabled: true
    health: # 适用于k8s  LivenessProbe(健康检查) 和 ReadinessProbe(是否已经准备好接受请求)
      show-details: always #不显示详细信息

上述配置要注意几点:

  • nacos只启动了一个点,所以只配置一个节点
  • nacos既作为注册中心又作为配置中心
  • sentinel下面配置datasource为nacos时候,就是把sentinel下流控配置存储到nacos, 这里采用服务名加-sentinel-flow的后缀,对应于后面sentinel-dashboard源码的修改
  • 在网关启动时候,加入参数-Dcsp.sentinel.app.type=1,告诉sentinel这是一个网关类型,sentinel-bashboard控制台如下显示就对了:
如何实现 SpringCloud Gateway 整合 Sentinel 流控规则,并持久化到 Nacos

两个微服务开发,分别为demo和demo1, 这里就不用多讲了,见源码。

sentinel-dashboard改造

主要就是新建、修改和删除流控规则时候,调用nacos提供的接口,将相关流控信息推送给nacos, 保存到mysql.我参考了下面的这个文章,在他提供的源码上面进行了修改,实现了我的目的,非常感谢!

https://blog.csdn.net/q669239799/article/details/125777745

  • 参考上面的文章,下载文章中提供的sentinel-bashboard源码。在application.properties中修改端口和增加nacos配置
server.port=8800
nacos.address=172.20.11.5:8840
nacos.namespace=middle-dev
nacos.group=dev
  • 通过debug发现,最后其实是调用GatewayFlowRuleController类的addFlowRuleupdateFlowRuledeleteFlowRule对sentinel内存中的流控规则进行新增、修改和删除的。所以在对内存中的流控规则操作后,再调用nacos的接口,更改nacos里面的流控规则,达到保存到nacos的目的。这里以修改流控规则为例子:
    • 修改NacosConfig.java, 增加对于GatewayFlowRuleEntity的转换Bean
package com.alibaba.csp.sentinel.dashboard.config.nacos;

import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.DegradeRuleEntity;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.config.ConfigFactory;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;
import java.util.Properties;

/**
 * @author Eric Zhao
 * @since 1.4.0
 */

@Configuration
public class NacosConfig {
    @Value("${nacos.address}")
    private String addr;   //Nacos地址,对应于上面的设置

    @Value("${nacos.namespace}")
    private String namespace;   //Nacos命名空间,对应于上面的设置

    @Bean
    public Converter<List<DegradeRuleEntity>, String> degradeRuleEntityEncoder() {
        return JSON::toJSONString;
    }

    @Bean
    public Converter<String, List<DegradeRuleEntity>> degradeRuleEntityDecoder() {
        return s -> JSON.parseArray(s, DegradeRuleEntity.class);
    }

    // 添加对于GatewayFlowRuleEntity的转换
    @Bean
    public Converter<List<GatewayFlowRuleEntity>, String> gatewayFlowRuleEntityEncoder() {
        return JSON::toJSONString;
    }
    @Bean
    public Converter<String, List<GatewayFlowRuleEntity>> gatewayFlowRuleEntityDecoder() {
        return s -> JSON.parseArray(s, GatewayFlowRuleEntity.class);
    }
    // end
  
    @Bean
    public Converter<List<FlowRuleEntity>, String> flowRuleEntityEncoder() {
        return JSON::toJSONString;
    }

    @Bean
    public Converter<String, List<FlowRuleEntity>> flowRuleEntityDecoder() {
        return s -> JSON.parseArray(s, FlowRuleEntity.class);
    }

    @Bean
    public ConfigService nacosConfigService() throws Exception {
        Properties properties = new Properties();
        //nacos集群地址
        properties.put(PropertyKeyConst.SERVER_ADDR,addr);
        //namespace为空即为public
        properties.put(PropertyKeyConst.NAMESPACE,namespace);
        return ConfigFactory.createConfigService(properties);
    }
}

  • 新增GatewayFlowRuleNacosPublisher.java
package com.alibaba.csp.sentinel.dashboard.config.flow;

import com.alibaba.csp.sentinel.dashboard.config.nacos.NacosConfigUtil;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.csp.sentinel.util.AssertUtil;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.ConfigType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.List;
// 注意,不一样的名称
@Component("gatewayFlowRuleNacosPublisher")
public class GatewayFlowRuleNacosPublisher implements DynamicRulePublisher<List<GatewayFlowRuleEntity>> {
    @Autowired
    private ConfigService configService;

    @Value("${nacos.group}"// 对应于application.properties里面的配置
    private String group;
    // 使用到上面定义的Converter
    @Autowired
    private Converter<List<GatewayFlowRuleEntity>, String> converter;
    
    @Override
    public void publish(String app, List<GatewayFlowRuleEntity> rules) throws Exception {
        AssertUtil.notEmpty(app, "app name cannot be empty");
        if (rules == null) {
            return;
        }
        // 通过app服务名+后缀: 还记得我们前面说的,后缀是:-sentinel-flow, 这样就能找到nacos里面的配置信息
        // 如果没有设置group ,缺省就是DEFAULT_GROUP
        // 这儿,实际上是调用NacosConfigService的功能对nacos进行操作
        configService.publishConfig(app + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,
                group == null ? NacosConfigUtil.GROUP_ID : group, converter.convert(rules), ConfigType.JSON.getType());
    }

}

  • GatewayFlowRuleController添加注入刚才创建的类的实例
@Autowired
@Qualifier("gatewayFlowRuleNacosPublisher")
private DynamicRulePublisher<List<GatewayFlowRuleEntity>> rulePublisher;
  • GatewayFlowRuleController添加函数
private void publishRules(String appName) {
    List<GatewayFlowRuleEntity> rules = repository.findAllByApp(appName);
    try {
        rulePublisher.publish(appName, rules);
    } catch (Exception e) {
        logger.warn("public gateway flow rules to nacos fail");
    }
}
  • GatewayFlowRuleControllerupdateFlowRule函数最后,添加publishRules调用
// 原有代码,发送流控规则更新客户端,比如这里是我的网关服务
if (!publishRules(app, entity.getIp(), entity.getPort())) {
    logger.warn("publish gateway flow rules fail after update");
else {
    // 客户端成功,更新保存到nacos持久化
    publishRules(entity.getApp());
}
  • 到这儿修改流控规则持久化到nacos就完了,新增和删除按照上面的思路改一下就行。
  • 对sentinel-bashboard进行打包,然后运行进行测试。

事情还没有完...

实际在使用的时候,在sentinel更改流控规则进行更新时候,我的网关服务会出现NullPointerException的错误,导致sentinel-bashboard更新流控不成功,通过网关程序日志发现,问题出在阿里提供的GatewayRuleManager这个类的applyToConvertedParamMap函数里面:(GatewayRuleManager在阿里提供的sentinel适配于gateway的包中:sentinel-api-gateway-adapter-common-1.8.0.jar

ParameterMetricStorage.getParamMetricForResource(resource).clearForRule(rule);

问题出在这一行上面,ParameterMetricStorage.getParamMetricForResource(resource)获取为null, 所以报NullPointerExceptin; 大致看了一下,是获取网关相关指标的函数,这一块也没有精力研究,直接改为:

ParameterMetric parameterMetric = ParameterMetricStorage.getParamMetricForResource(resource);
if (parameterMetric != null) {
    parameterMetric.clearForRule(rule);
}

这儿没有完全深入研究这样写的影响,反正从代码上看是没有错的,以后有时间再深入评估。到此为止,代码已经能够顺利运行了。

最后...

实际上,我的操作顺序是这样的

1、在nacos写一个流控的json格式配置,主要他的data-id为:gateway-server-sentinel-flow(网关服务名+后缀)

[{
 "resourceMode"0,
 "resource""demo1",
 "grade"1,
 "count"3,
 "intervalSec"1,
 "controlBehavior"0,
 "burst"0
},{
 "resourceMode"0,
 "resource""demo",
 "grade"1,
 "count"2,
 "intervalSec"1,
 "controlBehavior"0,
 "burst"0
}]

2、启动sentinel-bashboard(自己打的jar)

3、启动网关程序,可以在sentinel-bashboard控制台上面能够看见流控规则:注意,我涂掉的黑色部分一开始是没有的。

如何实现 SpringCloud Gateway 整合 Sentinel 流控规则,并持久化到 Nacos

所以无论我怎么设置,从网关调用后台服务从来不会阻塞;后来才发现是resource不对,应该在原来的resource前面加上ReactiveCompositeDiscoveryClient_

[{
 "resourceMode"0,
 "resource""ReactiveCompositeDiscoveryClient_demo1",
 "grade"1,
 "count"3,
 "intervalSec"1,
 "controlBehavior"0,
 "burst"0
},{
 "resourceMode"0,
 "resource""ReactiveCompositeDiscoveryClient_demo",
 "grade"1,
 "count"2,
 "intervalSec"1,
 "controlBehavior"0,
 "burst"0
}]

这样,网关调用时候就能准确的进行流控了。

注:我利用http://localhost:9000/actuator/gateway/routes查看网关的路由信息,得到:

如何实现 SpringCloud Gateway 整合 Sentinel 流控规则,并持久化到 Nacos

这个带前缀的route_id实际就是在sentinelroute控制台看见的请求链路上的route_id(见下图), 所以上面的资源应该加上ReactiveCompositeDiscoveryClient_前缀。再查,这个前缀实际为DiscoveryLocatorProperties里面的routeIdPrefix属性,缺省为ReactiveCompositeDiscoveryClient_, 在网关服务的配置文件里面可以配置(最后一行):

spring:  
  cloud:    
    gateway:
      globalcors:
        cors-configurations: #允许跨域请求
          '[**]':
            allowedOrigins: '*'
            allowedMethods: '*'
      discovery:
        locator:
          enabled: true
          route-id-prefix: sentinel  # 前缀

4、实际上,应该先从网关调用,在sentinel控制面板的请求链路上面显示:

如何实现 SpringCloud Gateway 整合 Sentinel 流控规则,并持久化到 Nacos

应该从这儿添加流控规则最好,然后同步到网关服务和持久化到nacos,这才是正确的步骤。

推荐

Java面试题宝典

技术内卷群,一起来学习!!

如何实现 SpringCloud Gateway 整合 Sentinel 流控规则,并持久化到 Nacos

PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。“在看”支持我们吧!

如何实现 SpringCloud Gateway 整合 Sentinel 流控规则,并持久化到 Nacos

原文始发于微信公众号(Java知音):如何实现 SpringCloud Gateway 整合 Sentinel 流控规则,并持久化到 Nacos