网关整合sentinel无法读取nacos配置问题分析

sentinel无法读取nacos配置问题分析

最近公司需要上线一个集约项目,虽然为内网项目,但曾经有过内网被攻破,导致内部系统被攻击的案例,且集约系统同时在线人数较多,所以需要对系统整体进行流控。市面上的流控方案有很多,不过新系统已经集成了sprin-cloud-alibaba-nacos,所以技术选型就选择了阿里的流控系统sentinel。

1.spring-cloud-gateway整合sentinel

  1. 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>
  2. 因为我们选择将流控规则持久化到nacos中,所以还需要引入sentinel的nacos数据库插件依赖

    xml 复制代码
         <!-- Sentinel Datasource Nacos -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>
  3. 配置sentinel控制台地址及nacos相关信息

    yml 复制代码
    spring:
      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
  4. 下载sentinel-dashboard的jar包【传送门】;执行启动命令:

    shell 复制代码
    java -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。

  5. 启动网关及sentinel控制台,查看网关是否已经注册,查看控制台是否已经有服务注册

2.问题现象

  • 上面一通操作之后,本地启动了个nacos,gateway和sentinel控制台看了一下,嗯,没问题,提交,发布测试
  • 测试环境使用k8s部署,部署完网关及sentinel控制台服务后,访问控制台。网关成功注册到nacos及sentinel控制台,再查看流控规则,居然没有读取到nacos中持久化的流控策略

3.原因猜测

  • 问题一出肯定要排查呀,百度了一下有没有人遇到相似的问题,百度的结果很多,但是都不符合我的情况。

  • 猜测问题应该出现在环境问题上。测试环境jdk和本地版本相同,nacos版本相同,sentinel-dashboard版本相同。可能问题出现在配置上。

  • 还记得我们上面怎么配置sentinel datasource的吗

    yml 复制代码
    	  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

    因为我们是部署在k8s中,nacos的访问地址密码等都配置在环境变量中,相当于bootstrap.yml的配置的配置其实一直是本地的配置,而 ${spring.cloud.nacos.discovery.server-addr}这种配置方式是直接读取bootstrap.yml中的nacos地址,其他的属性也是如此,相当于读取的全部都是本地环境的nacos配置,导致sentinel读取配置文件失败。

  • 解决方法:springboot支持直接读取系统环境变量,配置方法也是${系统变量名},并开启了sentinel日志配置,配置如下:

    yml 复制代码
        sentinel:
      # 取消控制台懒加载
      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.源码分析

  1. 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中拉取。

    java 复制代码
        public 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()

    java 复制代码
    public 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中的逻辑简单看下

    java 复制代码
    private 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配置信息,然后顺藤摸瓜应该就能找出到底是哪里读取的文件。

  2. 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会在启动的时候加载这个文件中的工厂类。看看这个文件里有哪些:

    shell 复制代码
    org.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

    java 复制代码
    public class DataSourcePropertiesConfiguration {
    	// 省略其他数据源的代码配置...
    	// 刚才的setDatasource就是拿到map中的value找到对应的属性类,也就是{"nacos":{...[配置的nacos属性]}}
    	// 从这里可以看出datasource的key是无所谓写什么值得,也就是配置类中的ds1.nacos可以替换成任意key.nacos
    	// 测试了一下改成datasource.nacos也完全没有问题
    	private NacosDataSourceProperties nacos;
    }

    nacos配置类

    java 复制代码
    public 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接口进行认证及获取配置文件。如果觉得有用就点个赞吧

相关推荐
考虑考虑2 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯3 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
青石路7 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
Java陈序员9 小时前
企业级!一个基于 Java 开发的开源 AI 应用开发平台!
spring boot·agent·mcp
像我这样帅的人丶你还10 小时前
Java 后端详解(五):Redis 缓存
java·后端·全栈
plainGeekDev12 小时前
GreenDAO → Room
android·java·kotlin
杨运交17 小时前
[041][公共模块]分布式唯一ID生成器设计与实现:一款灵活可扩展的雪花算法框架
spring boot
亦暖筑序17 小时前
Java 8老系统AI Workflow实战:把一次性AI对话升级成可恢复工作流
java·后端
敲代码的彭于晏17 小时前
Bean 生命周期完全图解:前端同学也能看懂的 Spring 核心机制
java·前端·后端
plainGeekDev19 小时前
ButterKnife → ViewBinding
android·java·kotlin