sentinel无法读取nacos配置问题分析
- 1.spring-cloud-gateway整合sentinel
- 2.问题现象
- 3.原因猜测
- 4.源码分析
- [4. 结语](#4. 结语)
最近公司需要上线一个集约项目,虽然为内网项目,但曾经有过内网被攻破,导致内部系统被攻击的案例,且集约系统同时在线人数较多,所以需要对系统整体进行流控。市面上的流控方案有很多,不过新系统已经集成了sprin-cloud-alibaba-nacos,所以技术选型就选择了阿里的流控系统sentinel。
1.spring-cloud-gateway整合sentinel
-
springcloud项目整合sentinel需要引入sentinel相关组件
xml<!-- SpringCloud Alibaba Sentinel --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <!-- SpringCloud Alibaba Sentinel Gateway --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId> </dependency>
-
因为我们选择将流控规则持久化到nacos中,所以还需要引入sentinel的nacos数据库插件依赖
xml<!-- Sentinel Datasource Nacos --> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency>
-
配置sentinel控制台地址及nacos相关信息
ymlspring: cloud: nacos: username: your-name password: your-pass discovery: # 服务注册地址 server-addr: your-addr namespace: your-namespace config: # 配置中心地址 server-addr: your-addr namespace: your-namespace # 配置文件格式 file-extension: yml sentinel: # 取消控制台懒加载 eager: true transport: # 控制台地址 dashboard: your-dashboard-addr # nacos配置持久化 datasource: ds1: nacos: server-addr: ${spring.cloud.nacos.discovery.server-addr} namespace: ${spring.cloud.nacos.discovery.namespace} username: ${spring.cloud.nacos.username} password: ${spring.cloud.nacos.password} # 限流规则的nacos配置名称 dataId: sentinel-gateway groupId: DEFAULT_GROUP data-type: json # 规则类型:网关限流 # 这是枚举类,其他类型可以在com.alibaba.cloud.sentinel.datasource.RuleType这个枚举类中查看 rule-type: gw-flow
-
下载sentinel-dashboard的jar包【传送门】;执行启动命令:
shelljava -Dserver.port=8088 -Dcsp.sentinel.dashboard.server=localhost:8088 -Dcsp.sentinel.app.type=1 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.8.jar
还可以指定用户名及密码,通过
-Dsentinel.dashboard.auth.username
和-Dsentinel.dashboard.auth.password
配置,不配置默认用户名密码都是sentinel。 -
启动网关及sentinel控制台,查看网关是否已经注册,查看控制台是否已经有服务注册
2.问题现象
- 上面一通操作之后,本地启动了个nacos,gateway和sentinel控制台看了一下,嗯,没问题,提交,发布测试
- 测试环境使用k8s部署,部署完网关及sentinel控制台服务后,访问控制台。网关成功注册到nacos及sentinel控制台,再查看流控规则,居然没有读取到nacos中持久化的流控策略
3.原因猜测
-
问题一出肯定要排查呀,百度了一下有没有人遇到相似的问题,百度的结果很多,但是都不符合我的情况。
-
猜测问题应该出现在环境问题上。测试环境jdk和本地版本相同,nacos版本相同,sentinel-dashboard版本相同。可能问题出现在配置上。
-
还记得我们上面怎么配置sentinel datasource的吗
ymldatasource: ds1: nacos: server-addr: ${spring.cloud.nacos.discovery.server-addr} namespace: ${spring.cloud.nacos.discovery.namespace} username: ${spring.cloud.nacos.username} password: ${spring.cloud.nacos.password} # 限流规则的nacos配置名称 dataId: sentinel-gateway groupId: DEFAULT_GROUP data-type: json # 规则类型:网关限流 # 这是枚举类,其他类型可以在com.alibaba.cloud.sentinel.datasource.RuleType这个枚举类中查看 rule-type: gw-flow
因为我们是部署在k8s中,nacos的访问地址密码等都配置在环境变量中,相当于bootstrap.yml的配置的配置其实一直是本地的配置,而 ${spring.cloud.nacos.discovery.server-addr}这种配置方式是直接读取bootstrap.yml中的nacos地址,其他的属性也是如此,相当于读取的全部都是本地环境的nacos配置,导致sentinel读取配置文件失败。
-
解决方法:springboot支持直接读取系统环境变量,配置方法也是
${系统变量名}
,并开启了sentinel日志配置,配置如下:ymlsentinel: # 取消控制台懒加载 eager: true transport: # 控制台地址 dashboard: 127.0.0.1:8088 # nacos配置持久化 ds1: datasource: nacos: server-addr: ${NacosServerAddr} namespace: ${ConfigNamespace} username: ${username} password: ${password} dataId: sentinel-iwos-gateway groupId: DEFAULT_GROUP data-type: json rule-type: gw-flow log: dir: /usr/local/logs/iwos-gateway
再次发布版本启动网关,查看sentinel控制台发现已经能够从nacos中读取持久化流控策略。问题到这里已经解决了,如果你和我一样好奇sentinel是如何从nacos配置文件中读取配置文件的话,那就继续往下看吧
4.源码分析
-
sentinel控制台是从哪里获取的配置文件
-
sentinel控制台有访问页面,既然有页面那么可以从页面查看获取数据入口
-
sentinel控制台是调用了http://127.0.0.1:8088/gateway/flow/list.json?app=mygateway\&ip=127.0.0.1\&port=8720获取了配置的流控规则。到git上把sentinel源码拉下来,我们来看看这个接口:
java@GetMapping("/list.json") @AuthAction(AuthService.PrivilegeType.READ_RULE) public Result<List<GatewayFlowRuleEntity>> queryFlowRules(String app, String ip, Integer port) { // 上面是一些参数校验,不重要,可以不看 try { // 这里是获取配置的核心代码 // 简单就是网关将自己注册到sentinel时,携带自身的ip和端口(这里可以在启动项配置固定值,如果不配置就是默认本地的ip和网关sentinel端口) // sentinel控制台访问网关集成的sentinel开放的端口,查询持久化在配置中心的配置文件 List<GatewayFlowRuleEntity> rules = sentinelApiClient.fetchGatewayFlowRules(app, ip, port).get(); repository.saveAll(rules); return Result.ofSuccess(rules); } catch (Throwable throwable) { logger.error("query gateway flow rules error:", throwable); return Result.ofThrowable(-1, throwable); } }
所以其实配置文件不是dashboard控制台去读取的,而是从网关的sentinel开放的restapi中拉取。
javapublic CompletableFuture<List<GatewayFlowRuleEntity>> fetchGatewayFlowRules(String app, String ip, int port) { if (StringUtil.isBlank(ip) || port <= 0) { return AsyncUtils.newFailedFuture(new IllegalArgumentException("Invalid parameter")); } try { // FETCH_GATEWAY_FLOW_RULE_PATH: gateway/getRules // 这里就是通过rest请求获取配置文件 return executeCommand(ip, port, FETCH_GATEWAY_FLOW_RULE_PATH, false) .thenApply(r -> { List<GatewayFlowRule> gatewayFlowRules = JSON.parseArray(r, GatewayFlowRule.class); List<GatewayFlowRuleEntity> entities = gatewayFlowRules.stream().map(rule -> GatewayFlowRuleEntity.fromGatewayFlowRule(app, ip, port, rule)).collect(Collectors.toList()); return entities; }); } catch (Exception ex) { logger.warn("Error when fetching gateway flow rules", ex); return AsyncUtils.newFailedFuture(ex); } }
控制台是请求网关gateway/getRules这个路径来获取配置文件的,完整的url为:
http://ip:port/gateway/getRules
。来看看网关sentinel这个api是怎么工作的。
java@CommandMapping( name = "gateway/getRules", desc = "Fetch all gateway rules" ) public class GetGatewayRuleCommandHandler implements CommandHandler<String> { public GetGatewayRuleCommandHandler() { } // 获取配置文件 public CommandResponse<String> handle(CommandRequest request) { // 有效代码就是GatewayRuleManager.getRules() return CommandResponse.ofSuccess(JSON.toJSONString(GatewayRuleManager.getRules())); } }
看一下GatewayRuleManager.getRules()
javapublic static Set<GatewayFlowRule> getRules() { Set<GatewayFlowRule> rules = new HashSet(); // 这里是读的内存中的一个map,往下看这个map是GatewayRulePropertyListener在监听器中进行加载和更新的 Iterator var1 = GATEWAY_RULE_MAP.values().iterator(); while(var1.hasNext()) { Set<GatewayFlowRule> ruleSet = (Set)var1.next(); rules.addAll(ruleSet); } return rules; }
GatewayRulePropertyListener中的逻辑简单看下
javaprivate static final class GatewayRulePropertyListener implements PropertyListener<Set<GatewayFlowRule>> { private GatewayRulePropertyListener() { } public void configUpdate(Set<GatewayFlowRule> conf) { this.applyGatewayRuleInternal(conf); RecordLog.info("[GatewayRuleManager] Gateway flow rules received: {}", new Object[]{GatewayRuleManager.GATEWAY_RULE_MAP}); } public void configLoad(Set<GatewayFlowRule> conf) { this.applyGatewayRuleInternal(conf); RecordLog.info("[GatewayRuleManager] Gateway flow rules loaded: {}", new Object[]{GatewayRuleManager.GATEWAY_RULE_MAP}); } }
这里是通过监听器调用configLoad方法来初始化加载配置规则,但是哪里创建监听器,创建的什么监听器,从这里已经无法再看出来了。线索中断了。。。。。要放弃吗?不!小小监听器,拿下。既然这个方向已经查不到什么信息了,那么我们换一个思路,来看看网关这边是在哪里读取nacos配置信息,然后顺藤摸瓜应该就能找出到底是哪里读取的文件。
-
-
sentinel是如何读取你的配置文件
想要知道哪个类读取的你的配置文件,这里,使用IDEA的朋友们可以按住control,同时单击配置项,就能快速定位到引用配置文件。
下面是我们定位到的地方:
java@ConfigurationProperties( // 读取spring.cloud.sentinel下的所有属性配置 prefix = "spring.cloud.sentinel" ) public class SentinelProperties { // 省略其他代码... private Map<String, DataSourcePropertiesConfiguration> datasource; // 省略其他代码 // 我们的配置文件就是映射到Map<String, DataSourcePropertiesConfiguration>中 // map整体结构就是{"ds1:":{"nacos":{....[配置的nacos属性]}}} public void setDatasource(Map<String, DataSourcePropertiesConfiguration> datasource) { this.datasource = datasource; } }
如果不是用IDEA开发的要怎么看呢,其实也有个通用的方法,
去看resources中META-INFO目录下spring自动装配的类。sentinel入口jar包是
spring-cloud-starter-alibaba-sentinel-2021.0.6.0.jar
。META-INFO下有一个spring.factories
文件,spring会在启动的时候加载这个文件中的工厂类。看看这个文件里有哪些:shellorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.alibaba.cloud.sentinel.SentinelWebAutoConfiguration,\ com.alibaba.cloud.sentinel.SentinelWebFluxAutoConfiguration,\ com.alibaba.cloud.sentinel.endpoint.SentinelEndpointAutoConfiguration,\ com.alibaba.cloud.sentinel.custom.SentinelAutoConfiguration,\ com.alibaba.cloud.sentinel.feign.SentinelFeignAutoConfiguration
加载用户配置的是
SentinelAutoConfiguration
类。java@Configuration(proxyBeanMethods = false) @ConditionalOnProperty(name = "spring.cloud.sentinel.enabled", matchIfMissing = true) @EnableConfigurationProperties(SentinelProperties.class) public class SentinelAutoConfiguration { @Value("${project.name:${spring.application.name:}}") private String projectName; @Autowired private SentinelProperties properties; // 其他代码省略 .... }
可以看到这里在创建bean的时候注入了
SentinelProperties
,也就是刚才我们通过IDEA定位到的配置类。两种方法都了解之后我们继续看
SentinelProperties
中的DataSourcePropertiesConfiguration
。javapublic class DataSourcePropertiesConfiguration { // 省略其他数据源的代码配置... // 刚才的setDatasource就是拿到map中的value找到对应的属性类,也就是{"nacos":{...[配置的nacos属性]}} // 从这里可以看出datasource的key是无所谓写什么值得,也就是配置类中的ds1.nacos可以替换成任意key.nacos // 测试了一下改成datasource.nacos也完全没有问题 private NacosDataSourceProperties nacos; }
nacos配置类
javapublic class NacosDataSourceProperties extends AbstractDataSourceProperties { // 这些都是sentinel可以配置的nacos配置项 private String serverAddr; private String contextPath; private String username; private String password; @NotEmpty private String groupId = "DEFAULT_GROUP"; @NotEmpty private String dataId; private String endpoint; private String namespace; private String accessKey; private String secretKey; public NacosDataSourceProperties() { super(NacosDataSourceFactoryBean.class.getName()); } // 预先检查,如果地址为空就设置一个默认的nacos地址 @Override public void preCheck(String dataSourceName) { if (StringUtils.isEmpty(serverAddr)) { serverAddr = this.getEnv().getProperty( "spring.cloud.sentinel.datasource.nacos.server-addr", "127.0.0.1:8848"); } } }
nacos配置类就是我们常规的配置,还记得sentinel是在哪个key下找到nacos的配置吗,可以回去看看
sentinelProperties
。它从spring.cloud.sentinel
下读取一个map作为数据源。这个map的结构是Map<String, DataSourcePropertiesConfiguration> datasource
。也就是spring会把spring.cloud.sentinel.任意key
下的配置全部映射到DataSourcePropertiesConfiguration
中。如果你配置的是nacos数据源,那么就需要将nacos配置在spring.cloud.sentinel.任意key.nacos
下,spring会将属性值自动映射到名为nacos的成员变量上,这个成员变量的类就是NacosDataSourceProperties
。
4. 结语
好了,到这里sentinel已经全部拿到nacos的配置信息了,后面就是调用nacos rpc接口进行认证及获取配置文件。如果觉得有用就点个赞吧