Skip to content
On this page

Sentinel

sentinel.jpg

SpringCloud Alibaba也有自己的微服务容错组件,但是它相比Hystrix更加的强大。

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

Sentinel 具有以下特征:

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

安装与部署

和Nacos一样,它是独立安装和部署的,下载地址:下载地址

注意下载下来之后是一个jar文件(其实就是个SpringBoot项目),默认是8080端口,接着就可以直接启动

启动之后,就可以访问到Sentinel的监控页面了,用户名和密码都是sentinel,地址:http://localhost:8858/#/dashboard

接着我们需要让我们的服务连接到Sentinel控制台,导入依赖:

xml

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
1
2
3
4
5

然后在配置文件中添加Sentinel相关信息

yaml
spring:
  application:
    name: userservice
  cloud:
    sentinel:
      transport:
        # 添加监控页面地址即可
        dashboard: localhost:8085
1
2
3
4
5
6
7
8

现在启动我们的服务,然后访问一次服务,这样Sentinel中就会存在信息了(懒加载机制,不会一上来就加载)

流量控制

机器不可能无限制的接受和处理客户端的请求,如果不加以限制,当发生高并发情况时,系统资源将很快被耗尽。为了避免这种情况,我们就可以添加流量控制(也可以说是限流)当一段时间内的流量到达一定的阈值的时候,新的请求将不再进行处理,这样不仅可以合理地应对高并发请求,同时也能在一定程度上保护服务器不受到外界的恶意攻击。

那么要实现限流,正常情况下,我们该采取什么样的策略呢?

  • 方案一:快速拒绝,既然不再接受新的请求,那么我们可以直接返回一个拒绝信息,告诉用户访问频率过高。
  • 方案二:预热,依然基于方案一,但是由于某些情况下高并发请求是在某一时刻突然到来,我们可以缓慢地将阈值提高到指定阈值,形成一个缓冲保护。
  • 方案三:排队等待,不接受新的请求,但是也不直接拒绝,而是进队列先等一下,如果规定时间内能够执行,那么就执行,要是超时就算了。

我们在Sentinel中进行实际测试一下,打开管理页面的簇点链路模块:

微信截图_20221017142422.png

这里演示对我们的借阅接口进行限流,点击流控,会看到让我们添加流控规则:

  • 阈值类型:QPS就是每秒钟的请求数量,并发线程数是按服务当前使用的线程数据进行统计的。
  • 流控模式:当达到阈值时,流控的对象,这里暂时只用直接。
  • 流控效果:就是我们上面所说的三种方案。

这里我们选择QPS、阈值设定为1,流控模式选择直接、流控效果选择快速失败,可以看到,当我们快速地进行请求时,会直接返回失败信息: Blocked by Sentinel (flow limiting)

限流和异常处理

限流状态下的返回结果该怎么修改呢,我们看到被限流之后返回的是Sentinel默认的数据,现在我们希望自定义改如何操作?

这里我们先创建好被限流状态下需要返回的内容,定义一个请求映射:

java

@RestController
public class SentinelBlockController {

    @GetMapping("/blocked")
    public Map<String, Object> block() {
        Map<String, Object> block = Maps.newHashMap();
        block.put("code", 403);
        block.put("success", false);
        block.put("massage", "您的请求频率过快,请稍后再试!");
        return block;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

接着我们在配置文件中将此页面设定为限流页面:

yaml
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8085
      # 将刚刚编写的请求映射设定为限流页面
      block-page: /blocked
1
2
3
4
5
6
7

这样,当被限流时,就会被重定向到指定页面:

json
{
  "code": 403,
  "success": false,
  "massage": "您的请求频率过快,请稍后再试!"
}
1
2
3
4
5

对于方法级别的限流呢?经过前面的学习我们知道,当某个方法被限流时,会直接在后台抛出异常,那么这种情况我们该怎么处理呢,比如我们之前在Hystrix中可以直接添加一个替代方案,这样当出现异常时会直接执行我们的替代方法并返回,Sentinel也可以。

比如我们还是在getUserBorrowDetailByUid方法上进行配置:

java
public class BorrowServiceImpl implements BorrowService {
    @SentinelResource(blockHandler = "borrowBlocked")
    public UserBorrowDetail getUserBorrowDetailByUid(int uid) {
        List<Borrow> borrow = mapper.getBorrowsByUid(uid);
        //获取User信息
        User user = userClient.getUserById(uid);
        //获取每一本书的详细信息
        List<Book> bookList = borrow
                .stream()
                .map(b -> bookClient.getBookById(b.getBid()))
                .collect(Collectors.toList());
        return new UserBorrowDetail(user, bookList);
    }

    /**
     * 替代方案,注意参数和返回值需要保持一致,并且参数最后还需要额外添加一个BlockException
     */
    public UserBorrowDetail borrowBlocked(int uid, BlockException e) {
        log.warn("block {} - {}", uid, e.getMessage(), e);
        return new UserBorrowDetail(null, Collections.emptyList());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

注意blockHandler只能处理限流情况下抛出的异常,包括下面即将要介绍的热点参数限流也是同理,如果是方法本身抛出的其他类型异常,不在管控范围内,但是可以通过其他参数进行处理:

java
public class TestController {
    @RequestMapping("/test")
    @SentinelResource(value = "test",
            fallback = "except",    //fallback指定出现异常时的替代方案
            exceptionsToIgnore = IOException.class)
        //忽略那些异常,也就是说这些异常出现时不使用替代方案
    String test() {
        throw new RuntimeException("HelloWorld!");
    }

    //替代方法必须和原方法返回值和参数一致,最后可以添加一个Throwable作为参数接受异常
    String except(Throwable t) {
        return t.getMessage();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

特别注意这种方式会在没有配置blockHandler的情况下,将Sentinel机制内(也就是限流的异常)的异常也一并处理了,如果配置了blockHandler,那么在出现限流时,依然只会执行blockHandler指定的替代方案(因为限流是在方法执行之前进行的)

Released under the MIT License.