环境准备

sentinel 服务端环境preparesentinel.sh

  1. #!/bin/bash
  2. mkdir logs
  3. wget -O /home/shell/sentinel-dashboard-1.8.0.jar https://handson.oss-cn-shanghai.aliyuncs.com/sentinel-dashboard-1.8.0.jar
  4. java -Dserver.port=64000 -jar sentinel-dashboard-1.8.0.jar > /home/shell/logs/sentinel.log &

nacos作为微服务应用的注册中心,通过初始化脚本preparenacos.sh,以启动nacos

  1. #!/bin/bash
  2. wget -O /home/shell/nacos-server-1.3.2.tar.gz https://handson.oss-cn-shanghai.aliyuncs.com/nacos-server-1.3.2.tar.gz
  3. tar -xzvf /home/shell/nacos-server-1.3.2.tar.gz -C /home/shell/
  4. sh /home/shell/nacos/bin/startup.sh -m standalone

搭建微服务系统

使用SpringCloudAlibaba开发的微服务系统,各部分组成如下:

  • service-api: 服务接口定义,供 consumer/provider 引用
  • service-provider: Dubbo 服务端,对外提供一些服务
  • web: Spring Boot Web 应用,其中的一些 API 会作为 consumer 来调用 dubbo-provider 获取结果。里面一共定义了三个 API path:
    • /demo/hello: 接受一个 name 参数,会 RPC 调用后端的 FooService:sayHello(name) 方法。
    • /demo/time:调用后端的 FooService:getCurrentTime 方法获取当前时间;里面可以通过 slow 请求参数模拟慢调用。
    • /demo/bonjour/{name}: 直接调用本地 DemoService 服务。

接下来,我们分别编译各个应用

首先是 api:

  1. cd ~/service-api && mvn clean install

接着是 service-provider:

  1. cd ~/service-provider && mvn clean install

再然后是 web:

  1. cd ~/web && mvn clean install

待编译完成以后,我们就可以启动应用了
api是接口定义,无需启动,先从服务端开始

  1. cd ~/service-provider && java -jar target/demo-service.jar

然后是启动 web 端:

  1. cd ~/web && java -jar target/demo-web.jar

待两个应用全部完成启动以后,就通过下面的链接,访问刚才部署的服务:

同时我们的环境也包含启动好的 Sentinel 控制台,可以直接访问并供各个服务接入。
tips: sentinel 控制台的默认账号和密码都是 “sentinel”

配置sentinel

下面我们来一步一步操作接入 SCA Sentinel 并通过控制台/Nacos 动态数据源配置流控降级规则来验证效果。

spring-cloud-alibaba-dependencies 配置

首先第一步我们在项目的父 pom 里面导入最新版本的 spring-cloud-alibaba-dependencies,这样我们在实际引入 SCA 相关依赖的时候就不需要指定版本号了:

  1. <dependencyManagement>
  2. <dependencies>
  3. <dependency>
  4. <groupId>com.alibaba.cloud</groupId>
  5. <artifactId>spring-cloud-alibaba-dependencies</artifactId>
  6. <version>2.2.5.RELEASE</version>
  7. <type>pom</type>
  8. <scope>import</scope>
  9. </dependency>
  10. </dependencies>
  11. </dependencyManagement>

不过由于需要nacos+dubbo的微服务功能,这部分代码已经被集成了

服务接入 SCA Sentinel

首先我们分别为两个服务模块引入 Spring Cloud Alibaba Sentinel 依赖:

  1. <dependency>
  2. <groupId>com.alibaba.cloud</groupId>
  3. <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>com.alibaba.csp</groupId>
  7. <artifactId>sentinel-apache-dubbo-adapter</artifactId>
  8. </dependency>

修改代码和配置

接着,我们需要配置两个服务,使得两个服务都可以连接到 sentinel 控制台
将下面的配置文件拷贝到对应应用的 application.properties 里:

  1. # Sentinel 控制台地址
  2. spring.cloud.sentinel.transport.dashboard=127.0.0.1:64000
  3. # 取消Sentinel控制台懒加载
  4. # 默认情况下 Sentinel 会在客户端首次调用的时候进行初始化,开始向控制台发送心跳包
  5. # 配置 sentinel.eager=true 时,取消Sentinel控制台懒加载功能
  6. spring.cloud.sentinel.eager=true

增加sentinel的配置类,以此来处理限流异常

  1. package com.example.demo.demos.sentinel;
  2. import java.io.PrintWriter;
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.context.annotation.Configuration;
  5. import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
  6. @Configuration
  7. public class SentinelWebConfig {
  8. @Bean
  9. public BlockExceptionHandler sentinelBlockExceptionHandler() {
  10. return (request, response, e) -> {
  11. // 429 Too Many Requests
  12. response.setStatus(429);
  13. PrintWriter out = response.getWriter();
  14. out.print("Oops, blocked by Sentinel: " + e.getClass().getSimpleName());
  15. out.flush();
  16. out.close();
  17. };
  18. }
  19. }

修改,增加sentinel的限流配置,增加后代码如下:

  1. package com.example.demo.demos.sentinel;
  2. import org.springframework.stereotype.Service;
  3. import com.alibaba.csp.sentinel.annotation.SentinelResource;
  4. import com.alibaba.csp.sentinel.slots.block.BlockException;
  5. @Service
  6. public class DemoService {
  7. @SentinelResource(value = "DemoService#bonjour", defaultFallback = "bonjourFallback")
  8. public String bonjour(String name) {
  9. return "Bonjour, " + name;
  10. }
  11. public String bonjourFallback(Throwable t) {
  12. if (BlockException.isBlockException(t)) {
  13. return "Blocked by Sentinel: " + t.getClass().getSimpleName();
  14. }
  15. return "Oops, failed: " + t.getClass().getCanonicalName();
  16. }
  17. }

重新编译provider和web两个应用

  1. cd ~/service-provider && mvn clean install
  2. cd ~/web && mvn clean install

待编译完成以后,再次重新启动两个应用,重新启动两个应用:

  1. cd ~/service-provider && java -jar target/demo-service.jar
  2. cd ~/web && java -jar target/demo-web.jar

流控规则

下面我们来配一条最简单的流控规则。打开
,在 Dubbo provider 端,我们进入簇点链路页面,针对 com.alibaba.csp.sentinel.demo.dubbo.FooService:getCurrentTime(boolean) 这个服务调用配置限流规则(需要有过访问量才能看到)。我们配一条 QPS 为 1 的流控规则,这代表针对该服务方法的调用每秒钟不能超过 1 次,超出会直接拒绝。
image.png
点击“新增”按钮,成功添加规则。我们可以在浏览器反复请求 localhost:8090/demo/time(频率不要太慢),可以看到会出现限流异常信息(Dubbo provider 默认的限流处理逻辑是抛出异常,该异常信息由 Dubbo 直接返回,并由 Spring 展示为默认 error 页面):
image.png
同时我们也可以在“实时监控”页面看到实时的访问量和拒绝量:
image.png
我们同样也可以在 Web API 处配置限流规则,观察效果。Spring Web 默认的限流处理逻辑是返回默认的提示信息(Blocked by Sentinel),状态码为 429。在后面的章节我们会介绍如何自定义流控处理逻辑。
了解了限流的基本用法,大家可能想问:生产环境我需要针对每个接口都去配置流控规则吗?阈值不会配怎么办?其实,限流降级的配置是需要结合容量规划、依赖梳理来做的。我们可以借助 JMeter 或 阿里云 PTS 等压测工具对我们的服务进行全链路压测,了解每个服务的最大承受能力,来确定核心接口的最大容量并作为 QPS 阈值。

熔断降级规则

熔断降级通常用于自动切断不稳定的服务,防止调用方被拖垮导致级联故障。熔断降级规则通常在调用端,针对弱依赖调用进行配置,在熔断时返回预定义好的 fallback 值,这样可以保证核心链路不被不稳定的旁路影响。
Sentinel 提供以下几种熔断策略:

  • 慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs,默认为 1s)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
  • 异常比例 (ERROR_RATIO):当单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
  • 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

下面我们来在 Web 应用中针对 Dubbo consumer 来配置慢调用熔断规则,并模拟慢调用来观察效果。我们在 web-api-demo 中针对 com.alibaba.csp.sentinel.demo.dubbo.FooService 服务调用配置熔断降级规则。
image.png
控制台配置的统计时长默认为 1s。在上面的这条规则中,我们设定慢调用临界值为 50ms,响应时间超出 50ms 即记为慢调用。当统计时长内的请求数 >=5 且慢调用的比例超出我们配置的阈值(80%)就会触发熔断,熔断时长为 5s,经过熔断时长后会允许一个请求探测通过,若请求正常则恢复,否则继续熔断。
我们的实例中 /demo/time API 可以通过 slow 请求参数模拟慢调用,当 slow=true 时该请求耗时会超过 100ms。我们可以用 ab 等压测工具或脚本,批量请求 localhost:8090/demo/time?slow=true,可以观察到熔断的返回
image.png
如果我们一直模拟慢调用,我们可以观察到熔断后每 5s 会允许通过一个请求,但该请求仍然是慢调用,会重新打回熔断,无法恢复。我们可以在触发熔断后,等待一段时间后手动发一个不带 slow=true 的正常请求,然后再进行请求,可以观察到熔断恢复。
需要注意的是,即使服务调用方引入了熔断降级机制,我们还是需要在 HTTP 或 RPC 客户端配置请求超时时间,来做一个兜底的防护。

注解方式自定义埋点

刚才我们看到的埋点都是 Sentinel 适配模块提供的自动埋点。有的时候自动埋点可能没法满足我们的需求,我们希望在某个业务逻辑的位置进行限流,能不能做到呢?当然可以!Sentinel 提供两种方式进行自定义埋点:SphU API 和 @SentinelResource 注解,前者最为通用但是代码比较繁杂,耦合度较高;注解方式侵入性较低,但有使用场景的限制。这里我们来动手在 Web 应用的 DemoService 上添加注解,来达到针对本地服务埋点的目标。
在 DemoService 中我们实现了一个简单的打招呼的服务:

  1. @Service
  2. public class DemoService {
  3. public String bonjour(String name) {
  4. return "Bonjour, " + name;
  5. }
  6. }

下面我们给 bonjour 这个函数添加 @SentinelResource 注解,注解的 value 代表这个埋点的名称(resourceName),会显示在簇点链路/监控页面。

  1. @SentinelResource(value = "DemoService#bonjour")
  2. public String bonjour(String name)

加上该注解后,再通过网关访问 /demo/bonjour/{name} 这个 API 的时候,我们就可以在簇点链路页面看到我们自定义的 DemoService#bonjour 埋点了。
image.png
添加注解埋点只是第一步。一般在生产环境中,我们希望在这些自定义埋点发生限流的时候,有一些 fallback 逻辑,而不是直接对外抛出异常。这里我们可以写一个 fallback 函数:

  1. public String bonjourFallback(Throwable t) {
  2. if (BlockException.isBlockException(t)) {
  3. return "Blocked by Sentinel: " + t.getClass().getSimpleName();
  4. }
  5. return "Oops, failed: " + t.getClass().getCanonicalName();
  6. }

我们的 fallback 函数接受一个 Throwable 参数,可以从中获取异常信息。Sentinel 注解的 fallback 会捕获业务异常和流控异常(即 BlockException 及其子类),我们可以在 fallback 逻辑里面进行相应的处理(如日志记录),并返回 fallback 的值。
注意:Sentinel 注解对 fallback 和 blockHandler 函数的方法签名有要求,具体请参考此处文档
写好 fallback 函数的实现后,我们在 @SentinelResource 注解里面指定一下:

  1. @SentinelResource(value = "DemoService#bonjour", defaultFallback = "bonjourFallback")
  2. public String bonjour(String name)

这样当我们自定义的 DemoService#bonjour 资源被限流或熔断的时候,请求会走到 fallback 的逻辑中,返回 fallback 结果,而不会直接抛出异常。我们可以配一个 QPS=1 的限流规则,然后快速请求后观察返回值:

  1. ? ~ curl http://localhost:8090/demo/bonjour/Sentinel
  2. Bonjour, Sentinel
  3. ? ~ curl http://localhost:8090/demo/bonjour/Sentinel
  4. Blocked by Sentinel: FlowException

注意:使用 @SentinelResource 注解要求对应的类必须由 Spring 托管(即为 Spring bean),并且不能是内部调用(没法走到代理),不能是 private 方法。Sentinel 注解生效依赖 Spring AOP 动态代理机制。

配置自定义的流控处理逻辑

Sentinel 的各种适配方式均支持自定义的流控处理逻辑。以 Spring Web 适配为例,我们只需要提供自定义的 BlockExceptionHandler 实现并注册为 bean 即可为 Web 埋点提供自定义处理逻辑。其中 BlockExceptionHandler 的定义如下:

  1. public interface BlockExceptionHandler {
  2. // 在此处处理限流异常,可以跳转到指定页面或返回指定的内容
  3. void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception;
  4. }

我们的 Web 应用中提供了 Web 埋点自定义流控处理逻辑的示例:

  1. @Configuration
  2. public class SentinelWebConfig {
  3. @Bean
  4. public BlockExceptionHandler sentinelBlockExceptionHandler() {
  5. return (request, response, e) -> {
  6. // 429 Too Many Requests
  7. response.setStatus(429);
  8. PrintWriter out = response.getWriter();
  9. out.print("Oops, blocked by Sentinel: " + e.getClass().getSimpleName());
  10. out.flush();
  11. out.close();
  12. };
  13. }
  14. }

该 handler 会获取流控类型并打印返回信息,返回状态码为 429。我们可以根据实际的业务需求,配置跳转或自定义的返回信息。
对于注解方式,我们上一节已经提到,可以指定 fallback 函数来处理流控异常和业务异常,这里不再展开讲解;对于 Dubbo 适配,我们可以通过 DubboAdapterGlobalConfig 注册 provider/consumer fallback 来提供自定义的流控处理逻辑;对于 Spring Cloud Gateway 适配,我们可以注册自定义的 BlockRequestHandler 实现类来为网关流控注册自定义的处理逻辑。

对 Spring Cloud 其他组件的支持

Spring Cloud Alibaba Sentinel 还提供对 Spring Cloud 其它常用组件的支持,包括 RestTemplate、Feign 等。篇幅所限,我们不展开实践。大家可以参考 Spring Cloud Alibaba 文档 来进行接入和配置。

参考

https://github.com/seata/seata-samples