上一篇文章主要介绍了 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:
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_ID
:SENTINEL_GROUP
GatewayFlowRuleController
默认的 GatewayFlowRuleController 只有对 内存中的限流规则的操作,需要新增 GatewayFlowRuleNacosPublisher
对象的注入,并在 addFlowRule
、updateFlowRule
、deleteFlowRule
这三个函数中新增 **网关流控规则 ** 持久化到 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 控制台,新增 流控规则:
此时打开 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<GatewayFlowRuleEntity>
转为 List<GatewayFlowRule>
然后进行持久化,是解决网关重启后,加载的规则为 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<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"}