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

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

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

限流规则持久化

1、修改 sentinel-dashboard pom.xml

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

<!-- for Nacos rule publisher sample -->
<dependency>
	<groupId>com.alibaba.csp</groupId>
	<artifactId>sentinel-datasource-nacos</artifactId>
	<!--<scope>test</scope>-->
</dependency>
2、新增 Nacos 命名空间

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存失败,源站可能有防盗链机制,建议将图片保存下来直接上传下上传(i4epl1uVeUxU-1678348588855)(C:\Users\LiAo\AppData\Roaming\Typora\typora-user-images\image-20230308155418604.png)(C:\Users\LiAo\AppData\Roaming\Typora\typora-user-images\image-20230308155418604.png)]

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 有关的配置项

/**
 * <p>
 * 用于读取 application.properties 中 Nacos 连接信息
 * </p>
 *
 * @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<List<GatewayFlowRule>, String> gatewayFlowRuleEntityEncoder() {
        return JSON::toJSONString;
    }

    /**
     * 网关流控规则反序列化方法
     *
     * @return 网关流控规则 集合
     */
    @Bean
    public Converter<String, List<GatewayFlowRule>> 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<List<GatewayFlowRule>> {

    @Autowired
    private ConfigService configService;

    @Autowired
    private Converter<List<GatewayFlowRule>, String> converter;

    @Override
    public void publish(String app, List<GatewayFlowRule> 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_IDSENTINEL_GROUP

GatewayFlowRuleController

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

对象注入

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

新增持久化操作函数:

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

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

	/**
     * 读取内存中的规则覆盖到 Nacos,完成持久化
     *
     * @param app appName
     */
    private void publishRules(String app) {
        List<GatewayFlowRuleEntity> gatewayFlowRuleEntities = repository.findAllByApp(app);

        // 格式化对象为 GatewayFlowRule
        List<GatewayFlowRule> 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<Long> 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
<!--监听与拉取 Nacos 中的规则-->
<dependency>
	<groupId>com.alibaba.csp</groupId>
	<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
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 控制台,新增 流控规则:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K3J07YTH-1678348588857)(C:\Users\LiAo\AppData\Roaming\Typora\typora-user-images\image-20230308165043117.png)]

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TKFWi5hK-1678348588857)(C:\Users\LiAo\AppData\Roaming\Typora\typora-user-images\image-20230308165243229.png)]

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

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

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

List<GatewayFlowRule> 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<List<DegradeRuleEntity>, String> degradeRuleEntityEncoder() {
        return JSON::toJSONString;
    }

    /**
     * 熔断规则反序列化方法
     *
     * @return 网关流控规则 集合
     */
    @Bean
    public Converter<String, List<DegradeRuleEntity>> 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<List<DegradeRuleEntity>> {

    @Autowired
    private ConfigService configService;

    @Autowired
    private Converter<List<DegradeRuleEntity>, String> converter;

    @Override
    public void publish(String app, List<DegradeRuleEntity> 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<List<DegradeRuleEntity>> publisher;
熔断规则持久化函数
	/**
     * 读取内存中的规则覆盖到 Nacos,完成持久化
     *
     * @param app appName
     */
    private void publishRules(String app) {
        List<DegradeRuleEntity> 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<Long> 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"}
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值