avatar

SpringCloud Alibaba 之 Sentinel(三)

五、Spring Cloud 集成Sentinel实践

1、Sentinel接入Spring Cloud

  • 创建一个基于Spring Boot的项目

  • 添加Sentinel依赖包

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    <version>2.2.0.RELEASE</version>
    </dependency>
  • 创建一个REST接口,并通过@SentinelResource 配置限流保护资源

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @RestController
    public class HelloController {


    @SentinelResource(value = "hello",blockHandler = "blockHandlerHello")
    @GetMapping("/say")
    public String hello(){
    return "hello cs-go";
    }

    public String blockHandlerHello(BlockException e){
    return "被限流了";
    }

    }
    • Sentinel starter 在默认情况下会为所有的HTTP服务提供限流埋点,所有如果只想对HTTP服务进行限流,那么只需要添加依赖即可,不需要修改任何代码。
    • 如果想要对特定的方法进行限流或者降级,则需要通过@SentinelResouce注解来实现限流资源的定义
    • 可以通过 SphU.entry()方法来配置资源
  • 手动配置流控规则,可以借用Sentinel的InitFunc SPI 扩展接口来实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class FlowRuleInitFunc implements InitFunc {
    @Override
    public void init() throws Exception {
    List<FlowRule> flowRuleList = new ArrayList<>();
    FlowRule rule = new FlowRule();
    rule.setCount(1);
    rule.setResource("hello");
    rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
    rule.setLimitApp("default");
    flowRuleList.add(rule);
    FlowRuleManager.loadRules(flowRuleList);
    }
    }

    SPI 是扩展点机制,如果需要被Sentinel加载,要么还要在resouce目录下创建 META-INF\services\com.alibaba.csp.sentinel.init.InitFunc按照上述配置好后,在初次访问任何资源的时候,Sentinel就会自动加载hello资源的流控规则

  • 启动服务后,访问localhost:8080/say方法, 当访问频率超过设定阈值时,就会触发限流。

2、基于Sentinel Dashboard 来实现流控配置

  • 启动 Sentinel Dashboard

  • 在application.properties添加配置:

    1
    2
    3
    spring.application.name=spring-cloud-sentinel-sample
    # 指的是Sentinel Dashboard的服务器地址,可以实现流控数据的监控和流控规则的分发
    spring.cloud.sentinel.transport.dashboard=localhost:8080
  • 提供一个REST接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @RestController
    public class DashboardController {

    // 此处不需要添加任何的资源埋点,在默认情况下Sentinel Starter会对所有Http请求进行限流。
    @GetMapping("/dash")
    public String dash(){
    return "hello dash";
    }

    }
  • 启动服务后,此时访问 /dash接口,由于没有配置流控规则,所有不存在流控行为。

    集成Sentinel就配置完成了,接下来,进入Sentinel Dashboard来实现流控规则的配置

  • 访问 Sentinel Dashboard的地址

  • 点击”簇点链接“,点击“流控”,设置单机阈值为1

  • 添加完成后,访问对应接口,当QPS超过1时,就可以看到限流的效果

    1
    Blocked by Sentinel(flow limiting)

3、自定义URL限流异常

在默认情况下,URL触发限流后会直接返回。

1
Blocked by Sentinel(flow limiting)

在实际应用中,大都采用 JSON 格式的数据,所以如果希望修改触发限流之后的返回结果形式,则可以通过自定义限流异常来处理,实现BlockExceptionHandler接口并重写handle方法:

1
2
3
4
5
6
7
8
9
@Service
public class CustomUrlBlockHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
httpServletResponse.setHeader("Content-Type","application/json;charset=UTF-8");
String message = "{\"code\":999,\"msg\":\"访问人数过多\"}";
httpServletResponse.getWriter().write(message);
}
}

还有一个场景时,当触发限流之后,我们希望直接跳转到一个降级页面,可以通过下面这个配置来实现

1
spring.cloud.sentinel.block-page={url}

4、URL 资源清洗

Sentinel中的HTTP服务限流默认由Sentinel-Web-Servlet 包中的CommonFilter 来实现,从代码中可以看到,这个Filter会把每个URL 都作为不同的资源来处理。

但如果是携带{id}参数的 REST 风格 API (例如:@GetMapping(“clean/{id}”)),对于每一个不同的{id},URL 也都是不一样的,所以默认情况下 Sentinel 会把所有的URL都当作资源来进行流控。

这样会导致两个问题

  • 限流统计的不准确,实际需求是控制clean方法总的QPS,结果统计的是每个URL的QPS。
  • 导致Sentinel中的资源数量过多,默认资源数量的阈值是6000,对于多出来的资源规则将不会生效。

针对这个问题可以通过 UrlCleaner 接口来实现资源清洗,也就是对于/clean/{id} 这个Url我们可以统一归集到/clean/* 资源下,具体配置代码如下,实现UrlCleaner 接口,并重写clean方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class CustomerUrlCleaner implements UrlCleaner {
@Override
public String clean(String originUrl) {
if (StringUtil.isEmpty(originUrl)){
return originUrl;
}
if (originUrl.startsWith("/clean/")){
return "/clean/*";
}
return originUrl;
}
}

六、Sentinel 集成 Nacos 实现动态流控规则

Sentinel的理念是只需要开发者关注资源的定义,他会默认对资源进行流控,当然,我们还需要对定义的资源设置流控规则:

  • 通过FlowRuleManager.loadRules (List rules) 手动加载流控规则。
  • 在Sentinel Dashboard 上针对资源动态创建流控规则。

基于Sentinel Dashboard所配置的流控规则,都是保存在内存中的,一旦重启,这些规则就会被清除。

Sentinel还支持很多数据源的扩展,下面演示Spring Cloud Sentinel 集成 Nacos 实现动态流控规则:

  • 添加数据源的依赖包

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
    <version>1.7.0</version>
    </dependency>
  • 创建REST接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @RestController
    public class DynamicController {

    @GetMapping("/dynamic")
    public String dynamic(){
    return "HELLO Dynamic Rule";
    }

    }
  • 添加数据源配置

    1
    2
    3
    4
    5
    spring.cloud.sentinel.datasource[0].nacos.server-addr=localhost:8848
    spring.cloud.sentinel.datasource[0].nacos.data-id=${spring.application.name}-sentinel
    spring.cloud.sentinel.datasource[0].nacos.group-id=DEFAULT_GROUP
    spring.cloud.sentinel.datasource[0].nacos.data-type=json
    spring.cloud.sentinel.datasource[0].nacos.rule-type=flow
  • 登录Nacos 控制台,创建流控配置规则

  • 登录 Sentinel Dashboard ,找到执行项目名称菜单下的“流控规则”,就可以看到在Nacos上所配置的流控规则已经被加载了

  • 当我们在Nacos上修改流控规则后,可以同步在Sentinel Dashboard 上看到流控规则的变化

问题:在Nacos控制台上修改流控规则,虽然可以同步到Sentinel Dashboard ,但是Nacos此时应该作为一个流控规则的持久化平台,所以正常的操作过程应该是开发者在Sentinel Dashboard 上修改流控规则后同步到Nacos上,遗憾的是,目前Sentinel Dashboard没有这个功能。直接在Nacos上修改流控规则,这种修改方式的危险系数很高,毕竟Sentinel Dashboard的 UI 才是专门负责流控规则维护的。

所以我们需要实现Sentinel Dashboard来动态维护流控规则并同步到Nacos上,目前官方没有提供支持,但是大家可以自己来实现。


七、 Sentinel Dashboard 集成Nacos实现规则同步

Sentinel Dashboard 的”流控规则“下的所有操作,都会调用 Sentinel Dashboard源码中的 FlowControllerV1这个类,这个类中包含流控规则本地化的CRUD操作。

另外,包下还存在一个FlowControllerV2类,这个类同样提供流控规则的CRUD,和V1版本不同的是,它可以实现指定数据源的规则拉取和发布,下面是部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
@RequestMapping(value = "/v2/flow")
public class FlowControllerV2 {

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

@Autowired
private InMemoryRuleRepositoryAdapter<FlowRuleEntity> repository;

@Autowired
@Qualifier("flowRuleDefaultProvider")
// 动态规则的拉取,从指定数据源中获取流控规则后在Sentinel Dashboard 中展示。
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleDefaultPublisher")
// 动态规则的发布,将Sentinel Dashboard中修改的规则同步到指定数据源中。
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;

这里我们扩展这两个类,然后集成Nacos来实现Sentinel Dashboard 规则的同步。

1、 Sentinel Dashboard 源码修改

  • 下载 Sentinel Dashboard 源码并打开工程

  • 在pom.xml文件中把sentinel-datasource-nacos依赖的注释掉

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
    <!-- <scope>test</scope>-->
    </dependency>
  • 修改 src\main\webapp\resources\app\scripts\directives\sidebar\sidebar.html 文件下的如下代码,将dashboard.flowV1 改为 dashboard.flow

    1
    2
    3
    4
    5
    6
    <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>&nbsp;&nbsp;流控规则
    </a>
    </li>

    修改之后,会调用FlowControllerV2中的接口

  • 在com.alibaba.csp.sentinel.dashboard.rule 包中创建一个nacos包,并创建一个类用来加载外部化配置。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    package com.alibaba.csp.sentinel.dashboard.rule.nacos;

    @ConfigurationProperties(prefix = "sentinel.nacos")
    public class NacosPropertiesConfiguration {

    private String serverAddr;
    private String dataId;
    private String groupId = "DEFAULT_GROUP";
    private String namespace;

    public String getServerAddr() {
    return serverAddr;
    }

    public void setServerAddr(String serverAddr) {
    this.serverAddr = serverAddr;
    }

    public String getDataId() {
    return dataId;
    }

    public void setDataId(String dataId) {
    this.dataId = dataId;
    }

    public String getGroupId() {
    return groupId;
    }

    public void setGroupId(String groupId) {
    this.groupId = groupId;
    }

    public String getNamespace() {
    return namespace;
    }

    public void setNamespace(String namespace) {
    this.namespace = namespace;
    }
    }
  • 创建一个Nacos配置类NacosConfiguration

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @EnableConfigurationProperties(NacosPropertiesConfiguration.class)
    @Configuration
    public class NacosConfiguration {

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

    @Bean
    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());
    properties.put(PropertyKeyConst.NAMESPACE,nacosPropertiesConfiguration.getNamespace());
    return ConfigFactory.createConfigService(properties);
    }

    }
    • 注入Converter 转换器,将FlowRuleEntity 转化为 FlowRule,以及反向转化

    • 注入Nacos配置服务ConfigService

  • 创建一个常量类 NacosConstants ,分别表示默认的 GROUP_ID 和 DATA_ID 的后缀

    1
    2
    3
    4
    5
    6
    7
    public class NacosConstants {

    public static final String DATA_ID_POSTFIX = "-sentinel-flow";

    public static final String GROUP_ID = "DEFAULT_GROUP";

    }
  • 实现动态从Nacos配置中心获取流控规则

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    @Service
    public class FlowRuleNacosProvider implements DynamicRuleProvider<List<FlowRuleEntity>> {

    private static Logger logger = LoggerFactory.getLogger(FlowRuleNacosProvider.class);

    @Autowired
    private NacosPropertiesConfiguration nacosConfigProperties;

    @Autowired
    private ConfigService configService;
    @Autowired
    private Converter<String, List<FlowRuleEntity>> converter;

    @Override
    public List<FlowRuleEntity> getRules(String appName) throws Exception {

    String dataID = new StringBuilder(appName).append(NacosConstants.DATA_ID_POSTFIX).toString();

    // 通过该方法从Nacos Config Server中读取指定配置信息,并通过converter转化为FlowRule 规则
    String rules = configService.getConfig(dataID,
    nacosConfigProperties.getGroupId(), 3000);
    logger.info("pull FlowRule from Nacos Config:{}",rules);
    if (StringUtil.isEmpty(rules)) {
    return new ArrayList<>();
    }
    return converter.convert(rules);
    }
    }
  • 创建一个流控规则发布类,在Sentinel Dashboard上修改完配置之后,需要调用该发布方法将数据持久化到Nacos中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @Service
    public class FlowRuleNacosPublisher implements DynamicRulePublisher<List<FlowRuleEntity>> {

    @Autowired
    private NacosPropertiesConfiguration nacosPropertiesConfiguration;

    @Autowired
    private ConfigService configService;

    @Autowired
    private Converter<List<FlowRuleEntity>, String> converter;

    @Override
    public void publish(String appName, List<FlowRuleEntity> rules) throws Exception {
    AssertUtil.notEmpty(appName, "appName cannot be empty");
    if (rules == null){
    return;
    }
    String dataID = new StringBuilder(appName).append(NacosConstants.DATA_ID_POSTFIX).toString();
    configService.publishConfig(dataID,nacosPropertiesConfiguration.getGroupId(),converter.convert(rules));
    }
    }
  • 修改FlowControllerV2类,将上面配置的两个类注入进来,表示规则的拉取和规则的发布统一用我们之前自定义的两个实例

    1
    2
    3
    4
    5
    6
    7
    8
        @Autowired
    // @Qualifier("flowRuleDefaultProvider")
    @Qualifier("flowRuleNacosProvider")
    private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
    @Autowired
    // @Qualifier("flowRuleDefaultPublisher")
    @Qualifier("flowRuleNacosPublisher")
    private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
  • 在application.properties 文件中添加Nacos服务端的配置信息

    1
    2
    3
    sentinel.nacos.serverAddr=localhost:8848
    sentinel.nacos.namespace=
    sentinel.nacos.group-id=DEFAULT_GROUP
  • 使用以下命令将代码打包成一个 fat jar ,根据前面介绍的操作方法启动服务。

    1
    mvn clean package

2、Sentinel Dashboard 规则数据同步

应用程序改动极少,只需要把配置文件中data-id的命名要以-sentinel-flow结尾就行,因为在Sentinel Dashboard中我们写了一个固定的后缀

1
spring.cloud.sentinel.datasource[0].nacos.data-id=${spring.application.name}-sentinel-flow

后面就可以通过在Sentinel Dashboard 修改流控规则,会同步到nacos控制台了。

八、Dubbo 集成 Sentinel 实现限流

Sentinel 提供了与Dubbo 整合的模块 sentinel-apache-dubbo-adapter,可以针对服务提供方和服务消费方进行流控,在使用的时候,只需要添加依赖:

1
2
3
4
5
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-apache-dubbo-adapter</artifactId>
<version>1.7.1</version>
</dependency>

添加好依赖后,Dubbo服务中的接口和方法(包括服务端和消费端)就会成为Sentinel中的资源,只需针对指定资源配置流控规则就可以实现Sentinel流控功能。

sentinel-apache-dubbo-adapter实现限流的核心原理是:基于Dubbo的SPI机制实现Filter扩展,Dubbo的Filter机制是专门为服务提供方和服务消费方调用过程进行拦截设计的,每次执行远程方法,该拦截都会被执行。

同时,sentinel-apache-dubbo-adapter还可以自定义开启或关闭某个过滤器的功能,下面这段代码表示关闭消费端的过滤器。

1
2
3
4
5
6
@Bean
public ComsumerConfig comsumerConfig(){
ConsumerConfig consumerConfig = new ConsumerConfig();
consumerConfig.setFilter("-sentinel.dubbo.filter");
return consumerConfig;
}

1、Dubbo服务接入Sentinel Dashborad

spring-cloud-starter-alibaba-sentinel 目前无法支持Dubbo服务的限流,所以针对Dubbo服务的限流只能使用 sentinel-apache-dubbo-adapter。这个适配组件需要手动接入 Sentinel Dashborad。

  • 引入依赖,这个依赖可以上报应用相关信息到控制台。

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-transport-simple-http</artifactId>
    <version>1.7.1</version>
    </dependency>
  • 添加启动参数

    1
    2
    3
    4
    -Djava.net.preferIPv4Stack=true 表示只支持IPv4。
    -Dcsp.sentinel.api.port=8720 客户端的port,用于上报应用的信息
    -Dscp.sentinel.dashborad.server=192.168.215.128:7777 Sentinel Dashborad地址
    -Dproject.name=spring-cloud.sentinel-dubbo.provider 应用名称,会在Sentinel Dashboard 右侧展示
  • 登录 Sentinel Dashboard后,进入“簇点链路” ,就可以看到Dbbo资源信息。

  • 注意:限流可以通过服务接口或服务方法设置

    • 服务接口:resourceName 为接口的全限定名。
    • 服务方法:resourceName 为接口全限定名:方法名。

2、Dubbo 服务限流规则配置

文章作者: Frosro
文章链接: https://frosro.github.io/2022/02/26/SpringCloudAlibaba%E4%B9%8BSentinel-3/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 BETTER LATE THAN NEVER
打赏
  • 微信
    微信
  • 支付宝
    支付宝

评论