整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

Sentinel熔断降级/流控生产级改造

光闹钟app开发者,请关注我,后续分享更精彩!

坚持原创,共同进步!

前言

阿里开源Sentinel框架功能强大,实现了对服务的流控、熔断降级,并支持通过管理页面进行配置和监控。Client集成支持Java、Go等语言。但管理后台Dashboard,默认只适合开发和演示,要生产环境使用需要做相应源代码级改造。本文将介绍具体改造步骤,帮助大家更快实现在生产环境的部署应用。当然,如果预算充足,也可以直接使用阿里云开箱即用的Sentinel Dashboard服务。

整体架构

Sentinel Dashboard规则默认存储在内存中,一旦服务重启,规则将丢失。要实现生产环境的大规模应用,需要把规则做持久化存储,Sentinel支持把nacos/zookeeper/apollo/redis等中间件作为存储介质。本文以Nacos作为规则存储端进行介绍。

整体架构如上图,Sentinel Dashboard通过界面操作,添加资源的规则,保存时将信息推送到nacos中。nacos收到数据变更,并实时推送到应用中的Sentinel SDK客户端,Sentinel SDK应用规则实现对服务资源的流控/熔断/降级管控。

代码改造

Sentinel 使用nacos作为存储端,需要修改源代码。

1.源代码下载

git clone git@github.com:hcq0514/Sentinel.git

IDE工具打开sentinel-dashboard项目模块

注意:将项目切换到使用的sentinel tag稳定版本,这里为1.8.6版本

2.pom.xml添加依赖

sentinel-dashboard模块pom.xml文件中,把sentinel-datasource-nacos依赖的注释掉。

<!-- for Nacos rule publisher sample -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
    <!--<scope>test</scope>-->
</dependency>

3.前端页面修改

resources/app/scripts/directives/sidebar/sidebar.html文件,将dashboard.flowV1改成dashboard.flow

<li ui-sref-active="active" ng-if="!entry.isGateway">
  <!--<a ui-sref="dashboard.flowV1({app: entry.app})">-->
  <a ui-sref="dashboard.flow({app: entry.app})">
    <i class="glyphicon glyphicon-filter"></i>  流控规则</a>
</li>

4.Java代码修改

com.alibaba.csp.sentinel.dashboard.rule包中创建一个nacos包,用来存放Nacos相关代码类。

nacos包下创建NacosPropertiesConfiguration类,用于nacos server配置属性封装

package com.alibaba.csp.sentinel.dashboard.rule.nacos;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "sentinel.nacos")
public class NacosPropertiesConfiguration {
    private String namespace;
    private String serverAddr;
    private String username;
    private String password;
    //省略 属性 set/get 方法......
}

nacos包下创建NacosConfigUtil工具类

public final class NacosConfigUtil {

    public static final String GROUP_ID = "SENTINEL_GROUP";
    
    public static final String FLOW_DATA_ID_POSTFIX = "-flow-rules";
    public static final String DEGRADE_DATA_ID_POSTFIX = "-degrade-rules";
    public static final String PARAM_FLOW_DATA_ID_POSTFIX = "-param-rules";
    public static final String CLUSTER_MAP_DATA_ID_POSTFIX = "-cluster-map";

    /**
     * cc for `cluster-client`
     */
    public static final String CLIENT_CONFIG_DATA_ID_POSTFIX = "-cc-config";
    /**
     * cs for `cluster-server`
     */
    public static final String SERVER_TRANSPORT_CONFIG_DATA_ID_POSTFIX = "-cs-transport-config";
    public static final String SERVER_FLOW_CONFIG_DATA_ID_POSTFIX = "-cs-flow-config";
    public static final String SERVER_NAMESPACE_SET_DATA_ID_POSTFIX = "-cs-namespace-set";

    private NacosConfigUtil() {}
}

nacos包创建NacosConfiguration bean配置类

@EnableConfigurationProperties(NacosPropertiesConfiguration.class)
@Configuration
public class NacosConfiguration {

    @Bean
    @Qualifier("degradeRuleEntityEncoder")
    public Converter<List<DegradeRuleEntity>, String> degradeRuleEntityEncoder() {
        return JSON::toJSONString;
    }

    @Bean
    @Qualifier("degradeRuleEntityDecoder")
    public Converter<String, List<DegradeRuleEntity>> degradeRuleEntityDecoder() {
        return s -> JSON.parseArray(s, DegradeRuleEntity.class);
    }

    @Bean
    @Qualifier("flowRuleEntityEncoder")
    public Converter<List<FlowRuleEntity>, String> flowRuleEntityEncoder() {
        return JSON::toJSONString;
    }

    @Bean
    @Qualifier("flowRuleEntityDecoder")
    public Converter<String, List<FlowRuleEntity>> flowRuleEntityDecoder() {
        return s -> JSON.parseArray(s, FlowRuleEntity.class);
    }

    @Bean
    public ConfigService nacosConfigService(NacosPropertiesConfiguration nacosPropertiesConfiguration) throws NacosException {
        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, nacosPropertiesConfiguration.getServerAddr());

        if(StringUtils.isNotBlank(nacosPropertiesConfiguration.getNamespace())){
            properties.put(PropertyKeyConst.NAMESPACE, nacosPropertiesConfiguration.getNamespace());
        }

        if(StringUtils.isNotBlank(nacosPropertiesConfiguration.getUsername())){
            properties.put(PropertyKeyConst.USERNAME, nacosPropertiesConfiguration.getUsername());
        }
        if(StringUtils.isNotBlank(nacosPropertiesConfiguration.getPassword())){
            properties.put(PropertyKeyConst.PASSWORD, nacosPropertiesConfiguration.getPassword());
        }
        return ConfigFactory.createConfigService(properties);
    }
}

nacos包下创建流控的provider和publisher实现类

@Component("flowRuleNacosProvider")
public class FlowRuleNacosProvider implements DynamicRuleProvider<List<FlowRuleEntity>> {

    @Autowired
    private ConfigService configService;
    @Autowired
    @Qualifier("flowRuleEntityDecoder")
    private Converter<String, List<FlowRuleEntity>> converter;

    @Override
    public List<FlowRuleEntity> getRules(String appName) throws Exception {
        String rules = configService.getConfig(appName + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,
            NacosConfigUtil.GROUP_ID, 3000);
        if (StringUtil.isEmpty(rules)) {
            return new ArrayList<>();
        }
        return converter.convert(rules);
    }
}
@Component("flowRuleNacosPublisher")
public class FlowRuleNacosPublisher implements DynamicRulePublisher<List<FlowRuleEntity>> {

    @Autowired
    private ConfigService configService;
    @Autowired
    @Qualifier("flowRuleEntityEncoder")
    private Converter<List<FlowRuleEntity>, String> converter;

    @Override
    public void publish(String app, List<FlowRuleEntity> rules) throws Exception {
        AssertUtil.notEmpty(app, "app name cannot be empty");
        if (rules == null) {
            return;
        }
        configService.publishConfig(app + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,
            NacosConfigUtil.GROUP_ID, converter.convert(rules));
    }
}

nacos包下创建熔断/降级的provider和publisher实现类

@Component("degradeRuleNacosProvider")
public class DegradeRuleNacosProvider implements DynamicRuleProvider<List<DegradeRuleEntity>> {

    @Autowired
    private ConfigService configService;
    @Autowired
    @Qualifier("degradeRuleEntityDecoder")
    private Converter<String, List<DegradeRuleEntity>> converter;

    @Override
    public List<DegradeRuleEntity> getRules(String appName) throws Exception {
        String rules = configService.getConfig(appName + NacosConfigUtil.DEGRADE_DATA_ID_POSTFIX,
            NacosConfigUtil.GROUP_ID, 3000);
        if (StringUtil.isEmpty(rules)) {
            return new ArrayList<>();
        }
        return converter.convert(rules);
    }
}
@Component("degradeRuleNacosPublisher")
public class DegradeRuleNacosPublisher implements DynamicRulePublisher<List<DegradeRuleEntity>> {

    @Autowired
    private ConfigService configService;
    @Autowired
    @Qualifier("degradeRuleEntityEncoder")
    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));
    }
}

修改com.alibaba.csp.sentinel.dashboard.controller.v2.FlowControllerV2类。将注入的bean标识由下图右边替换左边的值。将原有的服务引用基于内存存储,替换为nacos存储的服务引用。

# 上面截图红框替换值
flowRuleDefaultProvider 替换为 flowRuleNacosProvider
flowRuleDefaultPublisher 替换为 flowRuleNacosPublisher

删除com.alibaba.csp.sentinel.dashboard.controller.DegradeController类, com.alibaba.csp.sentinel.dashboard.controller.v2包下新增DegradeControllerV2类

@RestController
@RequestMapping("/degrade")
public class DegradeControllerV2 {

    private final Logger logger = LoggerFactory.getLogger(DegradeControllerV2.class);

    @Autowired
    private RuleRepository<DegradeRuleEntity, Long> repository;

    @Autowired
    @Qualifier("degradeRuleNacosProvider")
    private DynamicRuleProvider<List<DegradeRuleEntity>> ruleProvider;
    @Autowired
    @Qualifier("degradeRuleNacosPublisher")
    private DynamicRulePublisher<List<DegradeRuleEntity>> rulePublisher;

    @GetMapping("/rules.json")
    @AuthAction(PrivilegeType.READ_RULE)
    public Result<List<DegradeRuleEntity>> apiQueryMachineRules(String app, String ip, Integer port) {
        if (StringUtil.isEmpty(app)) {
            return Result.ofFail(-1, "app can't be null or empty");
        }
        if (StringUtil.isEmpty(ip)) {
            return Result.ofFail(-1, "ip can't be null or empty");
        }
        if (port == null) {
            return Result.ofFail(-1, "port can't be null");
        }
        try {
//            List<DegradeRuleEntity> rules = sentinelApiClient.fetchDegradeRuleOfMachine(app, ip, port);
            List<DegradeRuleEntity> rules = ruleProvider.getRules(app);
            if (rules != null && !rules.isEmpty()) {
                for (DegradeRuleEntity entity : rules) {
                    entity.setApp(app);
                }
            }
            rules = repository.saveAll(rules);
            return Result.ofSuccess(rules);
        } catch (Throwable throwable) {
            logger.error("queryApps error:", throwable);
            return Result.ofThrowable(-1, throwable);
        }
    }

    @PostMapping("/rule")
    @AuthAction(PrivilegeType.WRITE_RULE)
    public Result<DegradeRuleEntity> apiAddRule(@RequestBody DegradeRuleEntity entity) {
        Result<DegradeRuleEntity> checkResult = checkEntityInternal(entity);
        if (checkResult != null) {
            return checkResult;
        }
        Date date = new Date();
        entity.setGmtCreate(date);
        entity.setGmtModified(date);
        try {
            entity = repository.save(entity);
        } catch (Throwable t) {
            logger.error("Failed to add new degrade rule, app={}, ip={}", entity.getApp(), entity.getIp(), t);
            return Result.ofThrowable(-1, t);
        }
        if (!publishRules(entity.getApp(), entity.getIp(), entity.getPort())) {
            logger.warn("Publish degrade rules failed, app={}", entity.getApp());
        }
        return Result.ofSuccess(entity);
    }

    @PutMapping("/rule/{id}")
    @AuthAction(PrivilegeType.WRITE_RULE)
    public Result<DegradeRuleEntity> apiUpdateRule(@PathVariable("id") Long id,
                                                     @RequestBody DegradeRuleEntity entity) {
        if (id == null || id <= 0) {
            return Result.ofFail(-1, "id can't be null or negative");
        }
        DegradeRuleEntity oldEntity = repository.findById(id);
        if (oldEntity == null) {
            return Result.ofFail(-1, "Degrade rule does not exist, id=" + id);
        }
        entity.setApp(oldEntity.getApp());
        entity.setIp(oldEntity.getIp());
        entity.setPort(oldEntity.getPort());
        entity.setId(oldEntity.getId());
        Result<DegradeRuleEntity> checkResult = checkEntityInternal(entity);
        if (checkResult != null) {
            return checkResult;
        }

        entity.setGmtCreate(oldEntity.getGmtCreate());
        entity.setGmtModified(new Date());
        try {
            entity = repository.save(entity);
        } catch (Throwable t) {
            logger.error("Failed to save degrade rule, id={}, rule={}", id, entity, t);
            return Result.ofThrowable(-1, t);
        }
        if (!publishRules(entity.getApp(), entity.getIp(), entity.getPort())) {
            logger.warn("Publish degrade rules failed, app={}", entity.getApp());
        }
        return Result.ofSuccess(entity);
    }

    @DeleteMapping("/rule/{id}")
    @AuthAction(PrivilegeType.DELETE_RULE)
    public Result<Long> delete(@PathVariable("id") Long id) {
        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);
        } 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);
    }

    private boolean publishRules(String app, String ip, Integer port) {
        /*List<DegradeRuleEntity> rules = repository.findAllByMachine(MachineInfo.of(app, ip, port));
        return sentinelApiClient.setDegradeRuleOfMachine(app, ip, port, rules);*/
        List<DegradeRuleEntity> rules = repository.findAllByApp(app);
        try {
            rulePublisher.publish(app, rules);
        } catch (Exception e) {
            logger.error("Failed to publish nacos, app={}", app);
            logger.error("error is : ",e);
        }
        return true;
    }

    private <R> Result<R> checkEntityInternal(DegradeRuleEntity entity) {
        if (StringUtil.isBlank(entity.getApp())) {
            return Result.ofFail(-1, "app can't be blank");
        }
        if (StringUtil.isBlank(entity.getIp())) {
            return Result.ofFail(-1, "ip can't be null or empty");
        }
        if (entity.getPort() == null || entity.getPort() <= 0) {
            return Result.ofFail(-1, "invalid port: " + entity.getPort());
        }
        if (StringUtil.isBlank(entity.getLimitApp())) {
            return Result.ofFail(-1, "limitApp can't be null or empty");
        }
        if (StringUtil.isBlank(entity.getResource())) {
            return Result.ofFail(-1, "resource can't be null or empty");
        }
        Double threshold = entity.getCount();
        if (threshold == null || threshold < 0) {
            return Result.ofFail(-1, "invalid threshold: " + threshold);
        }
        Integer recoveryTimeoutSec = entity.getTimeWindow();
        if (recoveryTimeoutSec == null || recoveryTimeoutSec <= 0) {
            return Result.ofFail(-1, "recoveryTimeout should be positive");
        }
        Integer strategy = entity.getGrade();
        if (strategy == null) {
            return Result.ofFail(-1, "circuit breaker strategy cannot be null");
        }
        if (strategy < CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType()
            || strategy > RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT) {
            return Result.ofFail(-1, "Invalid circuit breaker strategy: " + strategy);
        }
        if (entity.getMinRequestAmount()  == null || entity.getMinRequestAmount() <= 0) {
            return Result.ofFail(-1, "Invalid minRequestAmount");
        }
        if (entity.getStatIntervalMs() == null || entity.getStatIntervalMs() <= 0) {
            return Result.ofFail(-1, "Invalid statInterval");
        }
        if (strategy == RuleConstant.DEGRADE_GRADE_RT) {
            Double slowRatio = entity.getSlowRatioThreshold();
            if (slowRatio == null) {
                return Result.ofFail(-1, "SlowRatioThreshold is required for slow request ratio strategy");
            } else if (slowRatio < 0 || slowRatio > 1) {
                return Result.ofFail(-1, "SlowRatioThreshold should be in range: [0.0, 1.0]");
            }
        } else if (strategy == RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO) {
            if (threshold > 1) {
                return Result.ofFail(-1, "Ratio threshold should be in range: [0.0, 1.0]");
            }
        }
        return null;
    }
}

5.maven打包

重新打包sentinel-dashboard模块

mvn clean package

6.配置文件修改

application.properties添加nacos配置项

#nacos服务地址
sentinel.nacos.serverAddr=127.0.0.1:8848
#nacos命名空间
sentinel.nacos.namespacec=
#sentinel.nacos.username=
#sentinel.nacos.password=

应用端集成

1.引入依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

如果是maven多模块工程,父pom中添加alibaba的父pom依赖

<dependencyManagement>
    <dependencies>
        <!-- https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E   -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2.2.6.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>    

2.注解支持

Sentinel 提供了 @SentinelResource 注解用于定义资源,实现资源的流控/熔断降级处理。

注意:注解方式埋点不支持 private 方法。

@Service
@Slf4j
public class TestService {
    /**
     * 定义sayHello资源的方法,流控/熔断降级处理逻辑
     * @param name
     * @return
     */
    @SentinelResource(value = "sayHello",blockHandler = "sayHelloBlockHandler",fallback = "sayHelloFallback")
    public String sayHello(String name) {
        if("fallback".equalsIgnoreCase(name)){
            throw new RuntimeException("fallback");
        }
        return "Hello, " + name;
    }

    /**
     * 被流控后处理方法。
     * 1. @SentinelResource 注解blockHandler设置,值为函数方法名
     * 2. blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。
     * 3. blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
     * @param name
     * @param blockException
     * @return
     */
    public String sayHelloBlockHandler(String name, BlockException blockException){
        log.warn("已开启流控",blockException);
        return "流控后的返回值";
    }

    /**
     * 被降级后处理方法
     * 注意!!!:如果blockHandler和fallback都配置,熔断降级规则生效,触发熔断,在熔断时长定义时间内,只有blockHandler生效
     * 1. @SentinelResource 注解fallback设置,值为函数方法名
     * 2. 用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理
     * 3. 返回值类型必须与原函数返回值类型一致;
     * 4. 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
     * 5. allback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
     * @param name
     * @param throwable
     * @return
     */
    public String sayHelloFallback(String name, Throwable throwable){
        log.warn("已降级",throwable);
        return "降级后的返回值";
    }
}

流控和降级处理逻辑定义详见代码注释。

@SentinelResource还包含其他可能使用的属性:

  • exceptionsToIgnore(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
  • exceptionsToTrace:用于指定处理的异常。默认为Throwable.class。可设置自定义需要处理的其他异常类。

3.添加配置

application.yml文件添加以下配置

spring:
  application:
    name: demo-sentinel
  cloud:
    sentinel: #阿里sentinel配置项
      transport:
        # spring.cloud.sentinel.transport.port 端口配置会在应用对应的机器上启动一个 Http Server,
        # 该 Server 会与 Sentinel 控制台做交互。比如 Sentinel 控制台添加了一个限流规则,会把规则数据 push 给这个 Http Server 接收,
        # Http Server 再将规则注册到 Sentinel 中。
#        port: 8719
        dashboard: localhost:8080
      datasource:
        ds1: # 数据源标识key,可以随意命名,保证唯一即可
          nacos: #nacos 数据源-流控规则配置
            server-addr: localhost:8848
#            username: nacos
#            password: nacos
            data-id: ${spring.application.name}-flow-rules
            group-id: SENTINEL_GROUP
            data-type: json
            #flow、degrade、param-flow
            rule-type: flow
        ds2:
          nacos: #nacos 数据源-熔断降级规则配置
            server-addr: localhost:8848
#            username: nacos
#            password: nacos
            data-id: ${spring.application.name}-degrade-rules
            group-id: SENTINEL_GROUP
            data-type: json
            #flow、degrade、param-flow
            rule-type: degrade

验证

启动sentinel-dashboard。

应用端集成Sentinel SDK,访问添加了降级逻辑处理的接口地址。

访问sentinel-dashboard web页面: http://localhost:8080/ 。在界面,添加相应流控和熔断规则。

Nacos管理界面查看:http://localhost:8848/nacos 。 可以看到自动生成了两个配置界面,分别对应流控和熔断配置

点击规则详情,查看规则信息

规则json对象信息如下:

entinel Dashboard(控制台)默认情况下,只能将配置规则保存到内存中,这样就会导致 Sentinel Dashboard 重启后配置规则丢失的情况,因此我们需要将规则保存到某种数据源中,Sentinel 支持的数据源有以下这些:

然而,默认情况下,Sentinel 和数据源之间的关系是单向数据通讯的,也就是只能先在数据源中配置规则,然后数据源会被规则推送至 Sentinel Dashboard 和 Sentinel 客户端,但是在 Sentinel Dashboard 中修改规则或新增规则是不能反向同步到数据源中的,这就是单向通讯。


所以,今天我们就该修改一下 Sentinel 的源码,让其可以同步规则至数据源,改造之后的交互流程如下图所示:

Sentinel 同步规则至数据源,例如将 Sentinel 的规则,同步规则至 Nacos 数据源的改造步骤很多,但整体实现难度不大,下面我们一起来看吧。

1.下载Sentinel源码

下载地址:https://github.com/alibaba/Sentinel

PS:本文 Sentinel 使用的版本是 1.8.6。

下载源码之后,使用 idea 打开里面的 sentinel-dashboard 项目,如下图所示:

2.修改pom.xml

将 sentinel-datasource-nacos 底下的 scope 注释掉,如下图所示:

PS:因为官方提供的 Nacos 持久化实例,是在 test 目录下进行单元测试的,而我们是用于生产环境,所以需要将 scope 中的 test 去掉。

3.移动单元测试代码

将 test/com.alibaba.csp.sentinel.dashboard.rule.nacos 下所有文件复制到 src/main/java/com.alibaba.csp.sentinel.dashboard.rule 目录下,如下图所示:

4.新建NacosPropertiesConfiguration文件

在 com.alibaba.csp.sentinel.dashboard.rule 下创建 Nacos 配置文件的读取类,实现代码如下:

package com.alibaba.csp.sentinel.dashboard.rule;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@ConfigurationProperties(prefix = "sentinel.nacos")
@Configuration
public class NacosPropertiesConfiguration {
    private String serverAddr;
    private String dataId;
    private String groupId;
    private String namespace;
    private String username;
    private String password;
    // 省略 Getter/Setter 代码
}

5.修改NacosConfig文件

只修改 NacosConfig 中的 nacosConfigService 方法,修改后的代码如下:

@Bean
public ConfigService nacosConfigService(NacosPropertiesConfiguration nacosPropertiesConfiguration) throws Exception {
    Properties properties = new Properties();
    properties.put(PropertyKeyConst.SERVER_ADDR, nacosPropertiesConfiguration.getServerAddr());
    properties.put(PropertyKeyConst.NAMESPACE, nacosPropertiesConfiguration.getNamespace());
    properties.put(PropertyKeyConst.USERNAME,nacosPropertiesConfiguration.getUsername());
    properties.put(PropertyKeyConst.PASSWORD,nacosPropertiesConfiguration.getPassword());
    return ConfigFactory.createConfigService(properties);
//        return ConfigFactory.createConfigService("localhost"); // 原代码
}

6.修改FlowControllerV2文件

修改 com.alibaba.csp.sentinel.dashboard.controller.v2 目录下的 FlowControllerV2 文件:

修改后代码:

@Autowired
@Qualifier("flowRuleNacosProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleNacosPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;

PS:此操作的目的是开启 Controller 层操作 Nacos 的开关。

如下图所示:

7.修改配置信息

在 application.properties 中设置 Nacos 连接信息,配置如下:

sentinel.nacos.serverAddr=localhost:8848
sentinel.nacos.username=nacos
sentinel.nacos.password=nacos
sentinel.nacos.namespace=
sentinel.nacos.groupId=DEFAULT_GROUP
sentinel.nacos.dataId=sentinel-dashboard-demo-sentinel

8.修改sidebar.html

修改 webapp/resources/app/scripts/directives/sidebar/sidebar.html 文件:

搜索“dashboard.flowV1”改为“dashboard.flow”,如下图所示:

9.修改identity.js

identity.js 文件有两处修改,它位于 webapp/resources/app/scripts/controllers/identity.js 目录。

9.1 第一处修改

将“FlowServiceV1”修改为“FlowServiceV2”,如下图所示:

9.2 第二处修改

搜索“/dashboard/flow/”修改为“/dashboard/v2/flow/”,如下图所示:

PS:修改 identity.js 文件主要是用于在 Sentinel 点击资源的“流控”按钮添加规则后将信息同步给 Nacos。

小结

Sentinel Dashboard 默认情况下,只能将配置规则保存到内存中,这样就会程序重启后配置规则丢失的情况,因此我们需要给 Sentinel 设置一个数据源,并且要和数据源之间实现双向通讯,所以我们需要修改 Sentinel 的源码。源码的改造步骤虽然很多,但只要逐一核对和修改就可以实现 Sentinel 生成环境的配置了。看完记得收藏哦,防止以后用的时候找不到。

本文已收录到我的面试小站 www.javacn.site,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。

好,这里是codetrend专栏“SpringCloud2023实战”。

本文简单介绍SpringCloud2023中使用Sentinel进行限流管理。

前言

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。

Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。

Sentinel 同时提供系统维度的自适应保护能力。

防止雪崩,是系统防护中重要的一环。当系统负载较高的时候,如果还持续让请求进入,可能会导致系统崩溃,无法响应。在集群环境下,网络负载均衡会把本应这台机器承载的流量转发到其它的机器上去。

如果这个时候其它的机器也处在一个边缘状态的时候,这个增加的流量就会导致这台机器也崩溃,最后导致整个集群不可用。

Spring Cloud Alibaba 默认为 Sentinel 整合了 Servlet、RestTemplate、FeignClient 和 Spring WebFlux。

Sentinel 在 Spring Cloud 生态中,不仅补全了 Hystrix 在 Servlet 和 RestTemplate 这一块的空白,而且还完全兼容了 Hystrix 在 FeignClient 中限流降级的用法,并且支持运行时灵活地配置和调整限流降级规则。

Sentinel工作机制

Sentinel 的使用可以分为两个部分:

  • 核心库(Java 客户端):不依赖任何框架/库,能够运行于 Java 8 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持(见 主流框架适配)。
  • 控制台(Dashboard):Dashboard 主要负责管理推送规则、监控、管理机器信息等。

Sentinel 的主要工作机制如下:

  • 对主流框架提供适配或者显示的 API,来定义需要保护的资源,并提供设施对资源进行实时统计和调用链路分析。
  • 根据预设的规则,结合对资源的实时统计信息,对流量进行控制。同时,Sentinel 提供开放的接口,方便您定义及改变规则。
  • Sentinel 提供实时的监控系统,方便您快速了解目前系统的状态。

Sentinel特点

Sentinel 具有以下特征:

  • 丰富的应用场景: Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、实时熔断下游不可用应用等。
  • 完备的实时监控: Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
  • 广泛的开源生态: Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
  • 完善的 SPI 扩展点: Sentinel 提供简单易用、完善的 SPI 扩展点。您可以通过实现扩展点,快速的定制逻辑。例如定制规则管理、适配数据源等。

Sentinel集成之控制台(Dashboard)

Sentinel 控制台提供一个轻量级的控制台,它提供机器发现、单机资源实时监控、集群资源汇总,以及规则管理的功能。您只需要对应用进行简单的配置,就可以使用这些功能。

  • 获取控制台。从 release 页面 - https://github.com/alibaba/Sentinel/releases 下载最新版本的控制台 jar 包。
  • 启动控制台。 Sentinel 控制台是一个标准的 Spring Boot 应用,以 Spring Boot 的方式运行 jar 包即可。
java -Dserver.port=10300 -Dcsp.sentinel.dashboard.server=localhost:10300 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
# 如若8080端口冲突,可使用 -Dserver.port=新端口 进行设置。
  • 配置控制台信息,application.yml 。
spring:
  cloud:
    sentinel:
      transport:
        port: 10201
        dashboard: localhost:10300

这里的 spring.cloud.sentinel.transport.port 端口配置会在应用对应的机器上启动一个 Http Server,该 Server 会与 Sentinel 控制台做交互。比如 Sentinel 控制台添加了一个限流规则,会把规则数据 push 给这个 Http Server 接收,Http Server 再将规则注册到 Sentinel 中。

Sentinel集成之普通服务

引入pom.xml

  • 引入Sentinel主要是引入 spring-cloud-starter-alibaba-sentinel 。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <dependencies>
        <!-- sentinel核心包引入 -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-core</artifactId>
        </dependency>
        <!-- sentinel-springboot引入 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
    </dependencies>
</project>

修改配置

  • 新增配置文件 application.yml,配置主要是 spring.cloud.sentinel 下面的配置。
spring.application.name: banana-client4-sentinel
spring:
  cloud:
    sentinel:
      transport:
        port: 10201
        dashboard: localhost:10300
    zookeeper:
      connect-string: localhost:2181

接口demo

  • 通过访问 http://localhost:10200/client4/swagger-ui.html ,输入账号密码 yulin/123yl. 就可以访问到最新的接口文档,验证限流是否开启成功。
@Service
public class TestService {

    @SentinelResource(value = "sayHello")
    public String sayHello(String name) {
        return "Hello, " + name;
    }
}

@RestController
public class TestController {

    @Autowired
    private TestService service;

    @GetMapping(value = "/hello/{name}")
    public String apiHello(@PathVariable String name) {
        return service.sayHello(name);
    }
}
  • 在控制面板设置 http://localhost:10200/client4/hello 的QPS为5。通过如下脚本验证限流是否成功:
import requests
token_list =[]
for j in range(10):
    token_list.append(str(j)+'hello')
    response = requests.get('http://localhost:10200/client4/hello/'+str(j)+'hello','hello')
    print(response.text)

输出文档如下:

Hello, 0hello
Hello, 1hello
Hello, 2hello
Hello, 3hello
Hello, 4hello
Blocked by Sentinel (flow limiting)
Blocked by Sentinel (flow limiting)
Blocked by Sentinel (flow limiting)
Blocked by Sentinel (flow limiting)
Blocked by Sentinel (flow limiting)

Sentinel集成之OpenFeign

pom.xml和配置修改

Feign 适配整合在 Spring Cloud Alibaba 中,可以参考 Spring Cloud Alibaba Sentinel 文档 进行接入。

Sentinel 适配了 Feign 组件。如果想使用,除了引入 spring-cloud-starter-alibaba-sentinel 的依赖外还需要 2 个步骤:

  • 配置文件打开 Sentinel 对 Feign 的支持:feign.sentinel.enabled=true
  • 加入 spring-cloud-starter-openfeign 依赖使 Sentinel starter 中的自动化配置类生效:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

demo验证代码

@FeignClient(name = "service-provider", fallback = EchoServiceFallback.class, configuration = FeignConfiguration.class)
public interface EchoService {
    @RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
    String echo(@PathVariable("str") String str);
}

class FeignConfiguration {
    @Bean
    public EchoServiceFallback echoServiceFallback() {
        return new EchoServiceFallback();
    }
}

class EchoServiceFallback implements EchoService {
    @Override
    public String echo(@PathVariable("str") String str) {
        return "echo fallback";
    }
}
  • EchoService 接口中方法 echo 对应的资源名为 GET:http://service-provider/echo/{str}。 通过对该资源限流配置即可。
  • 实际验证失败,可能是没有开发到springcloud2023。

Sentinel集成之Gateway

若想跟 Sentinel Starter 配合使用,需要加上 spring-cloud-alibaba-sentinel-gateway 依赖,同时需要添加 spring-cloud-starter-gateway 依赖来让 spring-cloud-alibaba-sentinel-gateway 模块里的 Spring Cloud Gateway 自动化配置类生效:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

同时请将 spring.cloud.sentinel.filter.enabled 配置项置为 false(若在网关流控控制台上看到了 URL 资源,就是此配置项没有置为 false)。Sentinel 网关流控默认的粒度是 route 维度以及自定义 API 分组维度,默认不支持 URL 粒度。

文档来自Sentinel文档。这里不仔细展开开发和说明,后续在网关业务层进行配置和说明。

完整源码信息查看可以在gitee或者github上搜索r0ad。

关于作者

来自一线全栈程序员nine的探索与实践,持续迭代中。

欢迎关注或者点赞~