标准 专业
多元 极客

Spring Cloud Alibaba研究院(3)——Sentinel——服务注册

首先,我们回顾一下在接入spring-cloud-alibaba-sentinel时,我们使用了@SentinelResouece标记了需要Sentinel进行管控的资源,并且在控制台可以发现我们的上报的资源。

那么到底是如何进行服务注册的呢?

 

启动的时候,Sentinel在组件中的工作

Sentinel自动配置策略依赖于spring.cloud.sentinel.enabled

启动时,spring-boot的自动配置类SPI如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.alibaba.sentinel.SentinelWebAutoConfiguration,\
org.springframework.cloud.alibaba.sentinel.endpoint.SentinelEndpointAutoConfiguration,\
org.springframework.cloud.alibaba.sentinel.custom.SentinelAutoConfiguration,\
org.springframework.cloud.alibaba.sentinel.feign.SentinelFeignAutoConfiguration

因为目前暂时没有涉及到feign,我们可以将这部分略过,剖析其余三个bean的作用。

SentinelWebAutoConfiguration

用于对filter进行通配符配置和排序。

@Bean
@ConditionalOnProperty(name = "spring.cloud.sentinel.filter.enabled", matchIfMissing = true)
public FilterRegistrationBean sentinelFilter() {
    FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();

    SentinelProperties.Filter filterConfig = properties.getFilter();

    if (filterConfig.getUrlPatterns() == null
        || filterConfig.getUrlPatterns().isEmpty()) {
        List<String> defaultPatterns = new ArrayList<>();
        defaultPatterns.add("/*");
        filterConfig.setUrlPatterns(defaultPatterns);
    }

    registration.addUrlPatterns(filterConfig.getUrlPatterns().toArray(new String[0]));
    Filter filter = new CommonFilter();
    registration.setFilter(filter);
    registration.setOrder(filterConfig.getOrder());
    log.info("[Sentinel Starter] register Sentinel with urlPatterns: {}.",
             filterConfig.getUrlPatterns());
    return registration;
}

SentinelEndpointAutoConfiguration

众所周知,Spring Boot中的actuate是负责汇报应用内信息的,而SentinelEndpointAutoConfiguration就是为了注册Sentinel的汇报信息的。

	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnEnabledEndpoint
	public SentinelEndpoint sentinelEndPoint(SentinelProperties sentinelProperties) {
		return new SentinelEndpoint(sentinelProperties);
	}

我们继续看SentinelEndpoint

@Endpoint(id = "sentinel")
public class SentinelEndpoint {

	private final SentinelProperties sentinelProperties;

	public SentinelEndpoint(SentinelProperties sentinelProperties) {
		this.sentinelProperties = sentinelProperties;
	}

	@ReadOperation
	public Map<String, Object> invoke() {
		final Map<String, Object> result = new HashMap<>();
		if (sentinelProperties.isEnabled()) {

			result.put("appName", AppNameUtil.getAppName());
			result.put("logDir", LogBase.getLogBaseDir());
			result.put("logUsePid", LogBase.isLogNameUsePid());
			result.put("blockPage", WebServletConfig.getBlockPage());
			result.put("metricsFileSize", SentinelConfig.singleMetricFileSize());
			result.put("metricsFileCharset", SentinelConfig.charset());
			result.put("totalMetricsFileCount", SentinelConfig.totalMetricFileCount());
			result.put("consoleServer", TransportConfig.getConsoleServer());
			result.put("clientIp", TransportConfig.getHeartbeatClientIp());
			result.put("heartbeatIntervalMs", TransportConfig.getHeartbeatIntervalMs());
			result.put("clientPort", TransportConfig.getPort());
			result.put("coldFactor", sentinelProperties.getFlow().getColdFactor());
			result.put("filter", sentinelProperties.getFilter());
			result.put("datasource", sentinelProperties.getDatasource());

			final Map<String, Object> rules = new HashMap<>();
			result.put("rules", rules);
			rules.put("flowRules", FlowRuleManager.getRules());
			rules.put("degradeRules", DegradeRuleManager.getRules());
			rules.put("systemRules", SystemRuleManager.getRules());
			rules.put("authorityRule", AuthorityRuleManager.getRules());
			rules.put("paramFlowRule", ParamFlowRuleManager.getRules());
		}
		return result;
	}
}

里面包括了所有的注册信息,包括当前的服务信息和Sentinel网关功能信息。

SentinelAutoConfiguration

SentinelAutoConfiguration会进行设置关键信息的系统变量:

@PostConstruct
private void init() {
    // ① Sentinel系统变量设置
    if (StringUtils.isEmpty(System.getProperty(LogBase.LOG_DIR))
            && StringUtils.hasText(properties.getLog().getDir())) {
        System.setProperty(LogBase.LOG_DIR, properties.getLog().getDir());
    }
    if (StringUtils.isEmpty(System.getProperty(LogBase.LOG_NAME_USE_PID))
            && properties.getLog().isSwitchPid()) {
        System.setProperty(LogBase.LOG_NAME_USE_PID,
                String.valueOf(properties.getLog().isSwitchPid()));
    }
    ...
    // ② 默认URL处理方式配置
	urlBlockHandlerOptional.ifPresent(WebCallbackManager::setUrlBlockHandler);
    urlCleanerOptional.ifPresent(WebCallbackManager::setUrlCleaner);
    requestOriginParserOptional.ifPresent(WebCallbackManager::setRequestOriginParser);

    // ③ 是否进行加载
    if (properties.isEager()) {
        InitExecutor.doInit();
    }

}
  1. Sentinel在服务期间使用到的全局变量,设置到系统变量中。
  2. 默认的URL处理。
  3. 如果关闭懒加载,这个时候会进行Sentinel全局配置的加载。懒加载的开关是spring.cloud.sentinel.eager,默认为true。

上述动作均发生在Bean的初始化过程中。

数据源解析

负责数据源解析的类是SentinelDataSourceHandler,它在初始化单例Bean的回调中进行对数据源的解析。

public void afterSingletonsInstantiated() {
    // ① 遍历配置信息
    sentinelProperties.getDatasource()
            .forEach((dataSourceName, dataSourceProperties) -> {
                try {
                    List<String> validFields = dataSourceProperties.getValidField();
                    // ② 规则校验
                    if (validFields.size() != 1) {
                    ...
                    }
                    AbstractDataSourceProperties abstractDataSourceProperties = dataSourceProperties
                            .getValidDataSourceProperties();
                    // ③ 前置检查
                    abstractDataSourceProperties.preCheck(dataSourceName);
                    // ④ 注册为Bean
                    registerBean(abstractDataSourceProperties, dataSourceName
                            + "-sentinel-" + validFields.get(0) + "-datasource");
                }
                ...
            });
}

我们简单介绍一下SentinelProperties,它是一个存在于每个服务的全局配置属性文件。

我们在应用配置文件(application.yml)中配置的,前缀是spring.cloud.sentinel的所有属性,都会解析到这个类对象中。

而在此类的加持下,我们配置的所有数据源(datasource)信息,会被解析到Map<String, DataSourcePropertiesConfiguration>对象中,String就是我们配置数据源时采用的类型,DataSourcePropertiesConfiguration就是我们相应的配置。

截稿前为止,Sentinel目前仅支持一种配置规则方式。

对上面的源码进行以下解读:

  1. 遍历SentinelProperties中的数据源配置信息。
  2. 规则方式数量限制,目前仅支持一种配置规则方式。
  3. 属性配置的前置检查。比如文件配置方式会设置文件地址的绝对路径,Nacos配置方式会检查Nacos的服务端地址和一些需要与Nacos进行交互的节点信息的完备性。
  4. 将数据源属性注册为Bean。

我们接下来看SentinelDataSourceHandler#registerBean()方法:

private void registerBean(final AbstractDataSourceProperties dataSourceProperties,
        String dataSourceName) {
    // ① 将数据源配置中的私有属性字段设置为可使用,并放入到属性map中
    Map<String, Object> propertyMap = Arrays
            .stream(dataSourceProperties.getClass().getDeclaredFields())
            .collect(HashMap::new, (m, v) -> {
                try {
                    v.setAccessible(true);
                    m.put(v.getName(), v.get(dataSourceProperties));
                }
                catch (IllegalAccessException e) {
                    log.error("[Sentinel Starter] DataSource " + dataSourceName
                            + " field: " + v.getName() + " invoke error");
                    throw new RuntimeException(
                            "[Sentinel Starter] DataSource " + dataSourceName
                                    + " field: " + v.getName() + " invoke error",
                            e);
                }
            }, HashMap::putAll);
    // ② 继续添加额外信息
    propertyMap.put(CONVERTER_CLASS_FIELD, dataSourceProperties.getConverterClass());
    propertyMap.put(DATA_TYPE_FIELD, dataSourceProperties.getDataType());

    // ③ 准备构建BeanDefinition
    BeanDefinitionBuilder builder = BeanDefinitionBuilder
            .genericBeanDefinition(dataSourceProperties.getFactoryBeanName());

    propertyMap.forEach((propertyName, propertyValue) -> {
        Field field = ReflectionUtils.findField(dataSourceProperties.getClass(),
                propertyName);
        if (null == field) {
            return;
        }
        if (DATA_TYPE_FIELD.equals(propertyName)) {
            // ④ 获取数据类型
            String dataType = StringUtils.trimAllWhitespace(propertyValue.toString());
            if (CUSTOM_DATA_TYPE.equals(dataType)) {
                try {
                    if (StringUtils
                            .isEmpty(dataSourceProperties.getConverterClass())) {
                        throw new RuntimeException("[Sentinel Starter] DataSource "
                                + dataSourceName
                                + "dataType is custom, please set converter-class "
                                + "property");
                    }
                    // construct custom Converter with 'converterClass'
                    // configuration and register
                    String customConvertBeanName = "sentinel-"
                            + dataSourceProperties.getConverterClass();
                    if (!this.beanFactory.containsBean(customConvertBeanName)) {
                        this.beanFactory.registerBeanDefinition(customConvertBeanName,
                                BeanDefinitionBuilder
                                        .genericBeanDefinition(
                                                Class.forName(dataSourceProperties
                                                        .getConverterClass()))
                                        .getBeanDefinition());
                    }
                    builder.addPropertyReference("converter", customConvertBeanName);
                }
                catch (ClassNotFoundException e) {
                ...
                }
            }
            else {
                if (!dataTypeList.contains(
                        StringUtils.trimAllWhitespace(propertyValue.toString()))) {
                    throw new RuntimeException("[Sentinel Starter] DataSource "
                            + dataSourceName + " dataType: " + propertyValue
                            + " is not support now. please using these types: "
                            + dataTypeList.toString());
                }
                // ⑤ 添加默认转换器
                builder.addPropertyReference("converter",
                        "sentinel-" + propertyValue.toString() + "-"
                                + dataSourceProperties.getRuleType().getName()
                                + "-converter");
            }
        }
        else if (CONVERTER_CLASS_FIELD.equals(propertyName)) {
            return;
        }
        else {
            // ⑥ 直接注册转换器
            Optional.ofNullable(propertyValue)
                    .ifPresent(v -> builder.addPropertyValue(propertyName, v));
        }
    });
    // ⑦ 通过Bean工厂注册Bean
    this.beanFactory.registerBeanDefinition(dataSourceName,
            builder.getBeanDefinition());
    // ⑧ 实例化Bean
    AbstractDataSource newDataSource = (AbstractDataSource) this.beanFactory
            .getBean(dataSourceName);
    // ⑨ 记录并校验数据源中的规则类型
    logAndCheckRuleType(newDataSource, dataSourceName,
            dataSourceProperties.getRuleType().getClazz());

    // ⑩ 将规则注册到RuleManager中
    dataSourceProperties.postRegister(newDataSource);
}
  1. 将数据源配置中的私有属性字段设置为可访问,并将键值对放入到属性map中。
  2. 继续添加一些注册Bean时需要的额外信息。
  3. 准备构建BeanDefinition。
  4. 过滤属性信息,这时候做的是找到对应的转换器。
  5. 获取配置的文件类型。如果是自定义的,需要使用者提供自定义的转换器Bean。Sentinel也提供了两种默认的转换器策略:JSON和XML
  6. 如果不需要上面的处理,直接将属性放入到BeanDefinition中,这里的属性值得是除了dataTypeconverterClass的其他属性。
  7. 使用BeanDefintion注册Bean。
  8. 实例化Bean。
  9. 记录并校验数据源中的规则类型。
  10. 将规则注册到RuleManager中。

我们继续来看下SentinelDataSourceHandler#logAndCheckRuleType()方法,这里面有一些我们配置规则时,需要注意到的地方:

private void logAndCheckRuleType(AbstractDataSource dataSource, String dataSourceName,
        Class<? extends AbstractRule> ruleClass) {
    Object ruleConfig;
    try {
        // ① 加载配置
        ruleConfig = dataSource.loadConfig();
    }
    catch (Exception e) {
        log.error("[Sentinel Starter] DataSource " + dataSourceName
                + " loadConfig error: " + e.getMessage(), e);
        return;
    }
    if (ruleConfig instanceof List) {
        List convertedRuleList = (List) ruleConfig;
        if (CollectionUtils.isEmpty(convertedRuleList)) {
            log.warn("[Sentinel Starter] DataSource {} rule list is empty.",
                    dataSourceName);
            return;
        }
        // ② 规则类型校验
        if (convertedRuleList.stream()
                .noneMatch(rule -> rule.getClass() == ruleClass)) {
            ...
        }
        else if (!convertedRuleList.stream()
                .allMatch(rule -> rule.getClass() == ruleClass)) {
            log.warn("[Sentinel Starter] DataSource {} all rules are not {} type.",
                    dataSourceName, ruleClass.getSimpleName());
        }
        else {
            log.info("[Sentinel Starter] DataSource {} load {} {}", dataSourceName,
                    convertedRuleList.size(), ruleClass.getSimpleName());
        }
    }
    // ③ 非List的规则集合不予解析
    else {
        ...
    }
}
  1. 加载配置,并且使用指定的解析器进行解析。比如文件配置方式,就会从文件中读取配置信息;Nacos方式就会从Nacos端拉取配置信息。
  2. 规则类型校验。集合中类型必须满足有且仅有指定规则类型。

规则集合必须是List。

接下来我们看AbstractDataSourceProperties#postRegister()方法:

public void postRegister(AbstractDataSource dataSource) {
    switch (this.getRuleType()) {
    case FLOW:
        FlowRuleManager.register2Property(dataSource.getProperty());
        break;
    case DEGRADE:
        DegradeRuleManager.register2Property(dataSource.getProperty());
        break;
    case PARAM_FLOW:
        ParamFlowRuleManager.register2Property(dataSource.getProperty());
        break;
    case SYSTEM:
        SystemRuleManager.register2Property(dataSource.getProperty());
        break;
    case AUTHORITY:
        AuthorityRuleManager.register2Property(dataSource.getProperty());
        break;
    default:
        break;
    }
}

它会根据规则类型,将属性注册到对应的RuleManager中。

每种类型的RuleManager#register2Property()代码类似:

    AssertUtil.notNull(property, "property cannot be null");
    synchronized (LISTENER) {
        RecordLog.info("[DegradeRuleManager] Registering new property to degrade rule manager");
        currentProperty.removeListener(LISTENER);
        property.addListener(LISTENER);
        currentProperty = property;
    }

使用互斥锁的方式,生成用于监听配置变化的监听器。

看起来索然无味,但是却内涵深机。

在添加监听器时,我们会进行如下操作,DynamicSentinelProperty#addListener()

@Override
public void addListener(PropertyListener<T> listener) {
    listeners.add(listener);
    listener.configLoad(value);
}
  1. 向监听器列表中添加对应的监听器。
  2. 使用监听器触发一次属性加载。

也就是说,我们的所有动态规则配置,会在此时此刻,通过监听器初始化的方式,进行一次规则加载

我们完成了对Sentinel服务注册和规则的解析。

赞(1) 投币

评论 抢沙发

慕勋的实验室慕勋的研究院

码字不容易,路过请投币

支付宝扫一扫

微信扫一扫