Spring Boot + Spring Cloud Gateway + Alibaba Cloud Sentinel + Alibaba Nacos 持久化 限流 熔断降级 失效 重启后规则消失

小明 2025-05-02 13:42:46 6

上一���文章主要介绍了 Gateway 如何使用 Sentinle实现 限流、熔断,不熟悉这一部分的可以先看一下上一篇文章 [传送门] 。本文主要在前一篇文章所搭建的测试项目基础上,进行设置 Sentinle 的规则持久化至 Nacos 的介绍,以及在此过程中会遇到的限流、熔断不生效,重启后规则消失等问题进行记录。

​ 声明:本篇及后续文章所描述的 Sentinel 所遇到的问题,均为本人日常开发中由于个人新增的代码所导致的,与 Alibaba Sentinel ,Alibaba Nacos ,Spring Cloud Gateway 本身没有关系,非常感谢这些开源组件的背后开发人员。

限流规则持久化

1、修改 sentinel-dashboard pom.xml

sentinel-dashboard 的 pom.xml 文件中 默认是配置了 Nacos 持久化的依赖的,但是默认作用域为 test ,所以需要将作用域注释或删除掉,修改如下:


	com.alibaba.csp
	sentinel-datasource-nacos
	

2、新增 Nacos 命名空间

打开Nacos 控制台,进入左侧菜单:命名空间 ==> 新建命名空间,命名空间名与描述均为 gateway,点击确定即可完成创建,并会自动生成命名空间 ID:

3、新增 application.properties 配置项

在 application.properties 新增 Nacos 配置,namespace为刚刚创建名为 gateway 的命名空间的命名空间ID,若是使用 public 作为命名空间,sentinel.nacos.namespace 配置项目的值需要缺省为空,或者不填写 sentinel.nacos.namespace 这个配置项。

sentinel.nacos.address=localhost:8848
sentinel.nacos.namespace=292baf88-31c3-4cd8-8253-8c94bf6c8d09
sentinel.nacos.username=nacos
sentinel.nacos.password=nacos 
4、新增 NacosProperties

​ 新增 NacosProperties 类 用于读取 application.properties 中 Nacos 有关的配置项

/**
 * 

* 用于读取 application.properties 中 Nacos 连接信息 *

* * @author LiAo * @since 2023-03-08 */ @Component @ConfigurationProperties(prefix = "sentinel.nacos") public class NacosProperties { /** nacos地址 */ private String address; /** nacos命名空间 */ private String namespace; /** nacos用户名 */ private String username; /** nacos用户密码 */ private String password; public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getNamespace() { return namespace; } public void setNamespace(String namespace) { this.namespace = namespace; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
5、流控规则持久化

​ test 文件夹下 com.alibaba.csp.sentinel.dashboard.rule.nacos 中包含了流控规则持久化到 Nacos 测试函数,我们可以直接拷贝到 main 文件夹下使用, FlowRuleNacosPublisher 重命名为 GatewayFlowRuleNacosPublisher:

相关类的说明如下:

GatewayFlowRuleController			网关流控规则请求拦截、业务处理
GatewayFlowRuleNacosPublisher		网关流控规则推送
NacosConfig							Nacos 连接实例
NacosConfigUtil						持久化后 Nacos dataId、group id 命名规则

​ 本篇主要针对 Gatewat 模式下,Sentinel 的流控、熔断等规则的持久化,所以需要对 从 test 文件夹下拷贝的测试类进行的测试类进行一些修改,修改如下:

NacosConfig

修改 ConfigService 的创建参数,连接参数改为 NacosProperties 的连接信息,新增 GatewayFlowRule 的序列化与反序列化方法,代码如下:

@Configuration
public class NacosConfig {
    // 注入nacos配置文件
    @Autowired
    private NacosProperties nacosProperties;
    /**
     * 网关流控规则序列化方法
     *
     * @return JSON 字符串
     */
    @Bean
    public Converter gatewayFlowRuleEntityEncoder() {
        return JSON::toJSONString;
    }
    /**
     * 网关流控规则反序列化方法
     *
     * @return 网关流控规则 集合
     */
    @Bean
    public Converter gatewayFlowRuleEntityDecoder() {
        return s -> JSON.parseArray(s, GatewayFlowRule.class);
    }
    @Bean
    public ConfigService nacosConfigService() throws Exception {
        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, nacosProperties.getAddress());
        properties.put(PropertyKeyConst.NAMESPACE, nacosProperties.getNamespace());
        properties.put(PropertyKeyConst.USERNAME, nacosProperties.getUsername());
        properties.put(PropertyKeyConst.PASSWORD, nacosProperties.getPassword());
        return ConfigFactory.createConfigService(properties);
    }
}
NacosConfigUtil

新增 网关流控规则 Nacos 持久化 Data Id 后缀常量:

public final class NacosConfigUtil {
    ...
    // 网关流控规则 data Id 命名后缀
    public static final String GATEWAY_FLOW_DATA_ID_POSTFIX = "-gateway-flow-rules";
    ...
}
GatewayFlowRuleNacosPublisher

​ 网关流控规则持久化推送,修改 GatewayFlowRuleNacosPublisher 两个类的序列化类型、Component 名称参数,修改 publish 中 持久化的 Data ID 命名后缀:

@Component("gatewayFlowRuleNacosPublisher")
public class GatewayFlowRuleNacosPublisher implements DynamicRulePublisher {
    @Autowired
    private ConfigService configService;
    @Autowired
    private Converter converter;
    @Override
    public void publish(String app, List rules) throws Exception {
        AssertUtil.notEmpty(app, "app name cannot be empty");
        if (rules == null) {
            return;
        }
        configService.publishConfig(app + NacosConfigUtil.GATEWAY_FLOW_DATA_ID_POSTFIX,
                NacosConfigUtil.GROUP_ID, converter.convert(rules));
    }
}

​ 默认的 限流规则 存放在 Nacos 中的命名规则为 app + NacosConfigUtil.GATEWAY_FLOW_DATA_ID_POSTFIX 即为:app + -gateway-flow-rules,如之前 gateway-service 服务中的 spring.application.name: gateway-service,那么 gateway-service 的 网关限流规则的 dataId 则为: gateway-service-gateway-flow-rules 。

​ 默认的 GROUP_ID 为:NacosConfigUtil.GROUP_ID :SENTINEL_GROUP

GatewayFlowRuleController

​ 默认的 GatewayFlowRuleController 只有对 内存中的限流规则的操作,需要新增 GatewayFlowRuleNacosPublisher 对象的注入,并在 addFlowRule、updateFlowRule、deleteFlowRule 这三个函数中新增 **网关流控规则 ** 持久化到 Naco 中的操作。

​ 对象注入:

	 // 规则推送
    @Autowired
    @Qualifier("gatewayFlowRuleNacosPublisher")
    private DynamicRulePublisher publisher;

新增持久化操作函数:

​ 此处新增了将内存中的规则持久化到 Nacos 的操作,其中有一段将 GatewayFlowRuleEntity 集合对象转化为 GatewayFlowRule 结合的操作,

这段代码是解决上述文章中提到的 网关流控规则 中 intervalSec 属性值为1导致的流控没有达到预期效果的问题,关于这部分的说明将在末尾解释。

	/**
     * 读取内存中的规则覆盖到 Nacos,完成持久化
     *
     * @param app appName
     */
    private void publishRules(String app) {
        List gatewayFlowRuleEntities = repository.findAllByApp(app);
        // 格式化对象为 GatewayFlowRule
        List gatewayFlowRules  = gatewayFlowRuleEntities.stream()
                .map(r -> r.toGatewayFlowRule()).collect(Collectors.toList());
        try {
            publisher.publish(app, gatewayFlowRules);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

addFlowRule :

	try {
            entity = repository.save(entity);
			// 新增持久化操作
            publishRules(entity.getApp());
        } catch (Throwable throwable) {
            logger.error("add gateway flow rule error:", throwable);
            return Result.ofThrowable(-1, throwable);
        }

updateFlowRule:

	try {
            entity = repository.save(entity);
			// 新增持久化操作
            publishRules(entity.getApp());
        } catch (Throwable throwable) {
            logger.error("update gateway flow rule error:", throwable);
            return Result.ofThrowable(-1, throwable);
        }

deleteFlowRule:

	@PostMapping("/delete.json")
    @AuthAction(AuthService.PrivilegeType.DELETE_RULE)
    public Result deleteFlowRule(@RequestParam("id") Long id, @RequestParam("app") String app) {
        if (id == null) {
            return Result.ofFail(-1, "id can't be null");
        }
        GatewayFlowRuleEntity oldEntity = repository.findById(id);
        if (oldEntity == null) {
            return Result.ofSuccess(null);
        }
        try {
            repository.delete(id);
			// 新增持久化操作
            publishRules(app);
        } catch (Throwable throwable) {
            logger.error("delete gateway flow rule error:", throwable);
            return Result.ofThrowable(-1, throwable);
        }
        if (!publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) {
            logger.warn("publish gateway flow rules fail after delete");
        }
        return Result.ofSuccess(id);
    }

​ 至此 Sentinel Dashboard 中关于对网关流控规则的持久化的代码修改工作就已经完成了。

gateway-service 网关流控规则监听

​ 当规 sentinel datasource 对规则进行操作后,会通过 SentinelApiClient 这个类通知 注册到 Sentinel Dashboard 中的网关服务,此时规则就会加载进网关中,从而实现 流控 和 熔断等操作。但是,当网关重启 或者 用户手动修改过存储在 Nacos 控制台中的规则后,网关服务中的规则不会拉取,此时,网关就不会实现预期的 流控 与 熔断,甚至规则不生效,所以我们需要对 网关服务 gateway-service 进行相应代码修改:

pom.xml

	com.alibaba.csp
	sentinel-datasource-nacos

application.yml

spring.cloud.sentinel.datasource.ds.nacos.namespace 配置项的值要与 Sentinel Dashboard 中 的 sentinel.nacos.namespace 配置项的值保持一致,配置如下:

spring:
  application:
    name: gateway-service
  cloud:
    sentinel:
      datasource:
        ds:
          nacos:
            server-addr: localhost:8848
            username: nacos
            password: nacos
            namespace: 292baf88-31c3-4cd8-8253-8c94bf6c8d09
            group-id: SENTINEL_GROUP
            data-id: ${spring.application.name}-gateway-flow-rules
            data-type: json
            rule-type: gw-flow

流控持久化规则测试

​ 至此我们就完成了 Sentinel 规则的持久化,以及网关服务的监听,现在重新启动 Nacos、Sentinel Dashboard、gateway-service、producer-service 服务,进行测试,首先测试 Gateway 的转发服务,证明上述新增代码没有影响原有功能:

curl http://localhost/producer_service/hello
Hello

​ 通过测试可以看到,上述新增代码没有影响原有功能,打开 Sentinel Dashboard 控制台,新增 流控规则:

此时打开 Nacos 控制台 配置列表,可以看到 gatewat 命名空间下有一个 名为 gateway-service-gateway-flow-rules 的配置文件,点击查看内容如下:

现在进行测试流控规则是否生效:

curl http://localhost/producer_service/hello
Hello
curl http://localhost/producer_service/hello
{"code":429,"message":"Blocked by Sentinel: ParamFlowException"}

现在重启 Sentine Dashboard、gateway-service 两个服务,打开 Sentinle Dashboard 控制台,发现规则依然存在,重新测试流控规则,发现依然生效,测试如下:

curl http://localhost/producer_service/hello
Hello
curl http://localhost/producer_service/hello
{"code":429,"message":"Blocked by Sentinel: ParamFlowException"}

intervalSec 为 1 解决

​ 在 GatewayFlowRuleController 类中新增了 将 List 转为 List 然后进行持久化,是解决网关重启后,加载的规则为 intervalSec 值为 1 导致的流控失效的问题。下面代码使用 stream 中的 map 函数,将 gatewayFlowRules 集合中的元素执行 toGatewayFlowRule() 函数,该函数是将 GatewayFlowRuleEntity 对象转为 GatewayFlowRule , 其中有一个步骤是 rule.setIntervalSec(calIntervalSec(interval, intervalUnit)); 是将 GatewayFlowRuleEntity 对象中 interval 和 intervalUnit 计算出 QPS 值,然后存入Nacos。

​ 若是直接存入 GatewayFlowRuleEntity 类型的规则,由于 GatewayFlowRuleEntity 类没有 intervalSec 这个属性,这就会导致,当网关启动读取 Nacos中的规则时,是使用的 com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule 这个类,这个类中的 intervalSec 变量默认值为 1 ,由于 Nacos 中的规则没有 intervalSec 这个属性,当反序列化 Nacos 中的规则时,就会出现 由于 intervalSec 值为1 导致的限流规则达不到预期效果的问题

List gatewayFlowRules  = gatewayFlowRuleEntities.stream()
                .map(r -> r.toGatewayFlowRule()).collect(Collectors.toList());

熔断规则持久化

​ 熔断规则没有根据 Sentinel 是否为网关模式进行区分,所以只需要在 sentinel dashboard 新增熔断规则相关的持久化函数就可以了,新增如下:

NacosConfigUtil

新增熔断规则 Nacos 持久化 Data Id 后缀常量:

// 熔断规则 data Id 命名后缀
public static final String DEGRADE_DATA_ID_POSTFIX = "-degrade-rules";
NacosConfig

新增熔断规则序列化与反序列化方法:

/**
     * 熔断规则序列化方法
     *
     * @return JSON 字符串
     */
    @Bean
    public Converter degradeRuleEntityEncoder() {
        return JSON::toJSONString;
    }
    /**
     * 熔断规则反序列化方法
     *
     * @return 网关流控规则 集合
     */
    @Bean
    public Converter degradeRuleEntityDecoder() {
        return s -> JSON.parseArray(s, DegradeRuleEntity.class);
    }
DegradeRuleNacosPublisher

新建名为 com.alibaba.csp.sentinel.dashboard.rule.nacos.degrade 的包,新建名为 DegradeRuleNacosPublisher 的类,用于将流控规则持久化到 Nacos 中:

@Component("degradeRuleNacosPublisher")
public class DegradeRuleNacosPublisher implements DynamicRulePublisher {
    @Autowired
    private ConfigService configService;
    @Autowired
    private Converter converter;
    @Override
    public void publish(String app, List rules) throws Exception {
        AssertUtil.notEmpty(app, "app name cannot be empty");
        if (rules == null) {
            return;
        }
        configService.publishConfig(app + NacosConfigUtil.DEGRADE_DATA_ID_POSTFIX,
                NacosConfigUtil.GROUP_ID, converter.convert(rules));
    }
}
DegradeController
对象注入
	// 规则推送
    @Autowired
    @Qualifier("degradeRuleNacosPublisher")
    private DynamicRulePublisher publisher;
熔断规则持久化函数
	/**
     * 读取内存中的规则覆盖到 Nacos,完成持久化
     *
     * @param app appName
     */
    private void publishRules(String app) {
        List rules = repository.findAllByApp(app);
        try {
            publisher.publish(app, rules);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
apiAddRule

新增对熔断规则持久化函数调用:

		try {
            entity = repository.save(entity);
        	// 新增持久化操作
            publishRules(entity.getApp());
        } catch (Throwable t) {
            logger.error("Failed to add new degrade rule, app={}, ip={}", entity.getApp(), entity.getIp(), t);
            return Result.ofThrowable(-1, t);
        }
apiUpdateRule

新增对熔断规则持久化函数调用:

		try {
            entity = repository.save(entity);
            // 新增持久化操作
            publishRules(entity.getApp());
        } catch (Throwable t) {
            logger.error("Failed to save degrade rule, id={}, rule={}", id, entity, t);
            return Result.ofThrowable(-1, t);
        }
delete

新增对熔断规则持久化函数调用:

	@DeleteMapping("/rule/{id}")
    @AuthAction(PrivilegeType.DELETE_RULE)
    public Result delete(@PathVariable("id") Long id, @RequestParam("app") String app) {
        if (id == null) {
            return Result.ofFail(-1, "id can't be null");
        }
        DegradeRuleEntity oldEntity = repository.findById(id);
        if (oldEntity == null) {
            return Result.ofSuccess(null);
        }
        try {
            repository.delete(id);
            // 新增持久化操作
            publishRules(app);
        } catch (Throwable throwable) {
            logger.error("Failed to delete degrade rule, id={}", id, throwable);
            return Result.ofThrowable(-1, throwable);
        }
        if (!publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) {
            logger.warn("Publish degrade rules failed, app={}", oldEntity.getApp());
        }
        return Result.ofSuccess(id);
    }

gateway-service 熔断规则监听

application.yml
spring:
  application:
    name: gateway-service
  cloud:
    sentinel:
      datasource:
        ds1:
          nacos:
            server-addr: localhost:8848
            username: nacos
            password: nacos
            namespace: 292baf88-31c3-4cd8-8253-8c94bf6c8d09
            group-id: SENTINEL_GROUP
            data-id: ${spring.application.name}-degrade-rules
            data-type: json
            rule-type: degrade

熔断规则持久化测试

​ 至此我们就完成了 Sentinel 规则的持久化,以及网关服务的监听,现在重新启动 Nacos、Sentinel Dashboard、gateway-service、producer-service 服务,进行测试,首先测试 Gateway 的转发服务,证明上述新增代码没有影响原有功能:

curl http://localhost/producer_service/hello
Hello

​ 通过测试可以看到,上述新增代码没有影响原有功能,打开 Sentinel Dashboard 控制台,新增熔断规则:

此时打开 Nacos 控制台 配置列表,可以看到 gatewat 命名空间下有一个 名为 gateway-service-degrade-rules 的配置文件,点击查看内容如下:

现在进行测试熔断规则是否生效:

curl http://localhost/producer_service/hello
Hello
curl http://localhost/producer_service/hello
{"code":429,"message":"Blocked by Sentinel: DegradeException"}

现在重启 Sentine Dashboard、gateway-service 两个服务,打开 Sentinle Dashboard 控制台,发现规则依然存在,重新测试熔断规则,发现依然生效,测试如下:

curl http://localhost/producer_service/hello
Hello
curl http://localhost/producer_service/hello
{"code":429,"message":"Blocked by Sentinel: DegradeException"}
The End
微信