SpringCloud Alibaba 实战 - 前京东金融架构师 - 拉勾教育

上一讲我为各位讲解了 Nacos 配置中心的用途及配置技巧。本讲咱们基于上一讲的成果,学习如何在生产环境下通过 Nacos 实现 Sentinel 规则持久化。本讲咱们将介绍三方面内容:

  • Sentinel 与 Nacos 整合实现规则持久化;
  • 自定义资源点进行熔断保护;
  • 开发友好的异常处理程序。

Sentinel 与 Nacos 整合实现规则持久化

细心的你肯定在前面 Sentinel 的使用过程中已经发现,当微服务重启以后所有的配置规则都会丢失,其中的根源是默认微服务将 Sentinel 的规则保存在 JVM 内存中,当应用重启后 JVM 内存销毁,规则就会丢失。为了解决这个问题,我们就需要通过某种机制将配置好的规则进行持久化保存,同时这些规则变更后还能及时通知微服务进行变更。

正好,上一讲我们讲解了 Nacos 配置中心的用法,无论是配置数据的持久化特性还是配置中心主动推送的特性都是我们需要的,因此 Nacos 自然就成了 Sentinel 规则持久化的首选。

本讲我们仍然通过实例讲解 Sentinel 与 Nacos 的整合过程。

案例准备

首先,咱们快速构建演示工程 sentinel-sample。

1. 利用 Spring Initializr 向导创建 sentinel-sample 工程,pom.xml 增加以下三项依赖。

  1. <dependency>
  2. <groupId>com.alibaba.cloud</groupId>
  3. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>com.alibaba.cloud</groupId>
  7. <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
  8. </dependency>
  9. <dependency>
  10. <groupId>org.springframework.boot</groupId>
  11. <artifactId>spring-boot-starter-actuator</artifactId>
  12. </dependency>

2. 配置 Nacos 与 Sentinel 客户端。

  1. spring:
  2. application:
  3. name: sentinel-sample
  4. cloud:
  5. sentinel:
  6. transport:
  7. dashboard: 192.168.31.10:9100
  8. eager: true
  9. nacos:
  10. server-addr: 192.168.31.10:8848
  11. username: nacos
  12. password: nacos
  13. jackson:
  14. default-property-inclusion: non_null
  15. server:
  16. port: 80
  17. management:
  18. endpoints:
  19. web:
  20. exposure:
  21. include: '*'
  22. logging:
  23. level:
  24. root: debug

3. 在 sentinel-sample 服务中,增加 SentinelSampleController 类,用于演示运行效果。

  1. @RestController
  2. public class SentinelSampleController {
  3. @Resource
  4. private SampleService sampleService;
  5. * 流控测试接口
  6. * @return
  7. */
  8. @GetMapping("/test_flow_rule")
  9. public ResponseObject testFlowRule(){
  10. return new ResponseObject("0","success!");
  11. }
  12. * 熔断测试接口
  13. * @return
  14. */
  15. @GetMapping("/test_degrade_rule")
  16. public ResponseObject testDegradeRule(){
  17. try {
  18. sampleService.createOrder();
  19. }catch (IllegalStateException e){
  20. return new ResponseObject(e.getClass().getSimpleName(),e.getMessage());
  21. }
  22. return new ResponseObject("0","order created!");
  23. }
  24. }

此外,ResponseObject 对象封装了响应的数据。

  1. * 封装响应数据的对象
  2. */
  3. public class ResponseObject {
  4. private String code;
  5. private String message;
  6. private Object data;
  7. public ResponseObject(String code, String message) {
  8. this.code = code;
  9. this.message = message;
  10. }
  11. }

4. 额外增加 SampleService 用于模拟业务逻辑,等下我们将用它讲解自定义资源点与熔断设置。

  1. * 演示用的业务逻辑类
  2. */
  3. @Service
  4. public class SampleService {
  5. public void createOrder(){
  6. try {
  7. Thread.sleep(101);
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. System.out.println("订单已创建");
  12. }
  13. }

启动 sentinel-sample,访问http://localhost/test_flow_rule,浏览器出现 code=0 的 JSON 响应,说明 sentinel-sample 服务启动成功。

  1. {
  2. code: "0",
  3. message: "success!"
  4. }

流控规则持久化

工程创建完成,下面咱们将 Sentinel 接入 Nacos 配置中心。

第一步,pom.xml 新增 sentinel-datasource-nacos 依赖。

  1. <dependency>
  2. <groupId>com.alibaba.csp</groupId>
  3. <artifactId>sentinel-datasource-nacos</artifactId>
  4. </dependency>

sentinel-datasource-nacos 是 Sentinel 为 Nacos 扩展的数据源模块,允许将规则数据存储在 Nacos 配置中心,在微服务启动时利用该模块 Sentinel 会自动在 Nacos 下载对应的规则数据。

第二步,在 application.yml 文件中增加 Nacos 下载规则,在原有的 sentinel 配置下新增 datasource 选项。这里咱们暂时只对流控规则进行设置,重要配置项我在代码中进行了注释,请同学们仔细阅读。

  1. spring:
  2. application:
  3. name: sentinel-sample
  4. cloud:
  5. sentinel:
  6. transport:
  7. dashboard: 192.168.31.10:9100
  8. eager: true
  9. datasource:
  10. flow:
  11. nacos:
  12. server-addr: ${spring.cloud.nacos.server-addr}
  13. dataId: ${spring.application.name}-flow-rules
  14. groupId: SAMPLE_GROUP
  15. rule-type: flow
  16. username: nacos
  17. password: nacos
  18. nacos:
  19. server-addr: 192.168.31.10:8848
  20. username: nacos
  21. password: nacos
  22. ...

通过这一份配置,微服务在启动时就会自动从 Nacos 配置中心 SAMPLE_GROUP 分组下载 data-id 为 sentinel-sample-flow-rules 的配置信息并将其作为流控规则生效。
第三步,在 Nacos 配置中心页面,新增 data-id 为 sentinel-sample-flow-rules 的配置项。

13 | 生产实践:Sentinel 进阶应用场景 - 图1

流控规则设置

这里 data-id 与 groups 与微服务应用的配置保持对应,最核心的配置内容采用 JSON 格式进行书写,我们来分析下这段流控规则。

  1. [
  2. {
  3. "resource":"/test_flow_rule", #资源名,说明对那个URI进行流控
  4. "limitApp":"default", #命名空间,默认default
  5. "grade":1, #类型 0-线程 1-QPS
  6. "count":2, #超过2个QPS限流将被限流
  7. "strategy":0, #限流策略: 0-直接 1-关联 2-链路
  8. "controlBehavior":0, #控制行为: 0-快速失败 1-WarmUp 2-排队等待
  9. "clusterMode":false #是否集群模式
  10. }
  11. ]

仔细观察不难发现,这些配置项与 Dashboard UI 中的选项是对应的,其实使用 DashboardUI 最终目的是为了生成这段 JSON 数据而已,只不过通过 UI 更容易使用罢了。

13 | 生产实践:Sentinel 进阶应用场景 - 图2

Sentinel Dashboard 流控设置界面

关于这些设置项的各种细节,有兴趣的同学可以访问 Sentinel 的 GitHub 文档进行学习。

https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6

最后,我们启动应用来验证流控效果。

访问 http://localhost/test_flow_rule,在日志中将会看到这条 Debug 信息,说明在服务启动时已向 Nacos 配置中心获取到流控规则。

  1. DEBUG 12728 --- [main] s.n.www.protocol.http.HttpURLConnection : sun.net.www.MessageHeader@5432948015 pairs: {GET /nacos/v1/cs/configs?dataId=sentinel-sample-flow-rules&accessToken=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTYxMDg3NTA1M30.Hq561OkXuAqPI20IBsnPIn0ia86R9sZgdWwa_K8zwvw&group=SAMPLE_GROUP HTTP/1.1: null}...

咱们在浏览器反复刷新,当 test_flow_rule 接口每秒超过 2 次访问,就会出现 “Blocked by Sentinel (flow limiting)” 的错误信息,说明流控规则已生效。

之后我们再来验证能否自动推送新规则,将 Nacos 配置中心中流控规则 count 选项改为 1。

  1. [
  2. {
  3. "resource":"/test_flow_rule",
  4. "limitApp":"default",
  5. "grade":1,
  6. "count":1, #2改为1
  7. "strategy":0,
  8. "controlBehavior":0,
  9. "clusterMode":false
  10. }
  11. ]

修改后的流控规则

当新规则发布后,sentinel-sample 控制台会立即收到下面的日志,说明新的流控规则即时生效。

  1. DEBUG 12728 --- [.168.31.10_8848] s.n.www.protocol.http.HttpURLConnection : sun.net.www.MessageHeader@41257f3915 pairs: {GET /nacos/v1/cs/configs?dataId=sentinel-sample-flow-rules&accessToken=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTYxMDg3NTA1M30.Hq561OkXuAqPI20IBsnPIn0ia86R9sZgdWwa_K8zwvw&group=SAMPLE_GROUP HTTP/1.1: null}

在无须重启的情况下,test_flow_rule 接口 QPS 超过 1 就会直接报错。

与此同时,我们还可以通过 Spring Boot Actuator 提供的监控指标确认流控规则已生效。

访问 http://localhost/actuator/sentinel,在 flowRules 这个数组中,可以看到 test_flow_rule 的限流规则,每一次流控规则产生改变时 Nacos 都会主动推送到微服务并立即生效。

13 | 生产实践:Sentinel 进阶应用场景 - 图3

新的流控规则

自定义资源点进行熔断保护

讲到这,我们已经实现了 test_flow_rule 接口的流控规则,但你发现了没有,在前面一系列 Sentinel 的讲解中我们都是针对 RESTful 的接口进行限流熔断设置,但是在项目中有的时候是要针对某一个 Service 业务逻辑方法进行限流熔断等规则设置,这要怎么办呢?

在 sentinel-core 客户端中为开发者提供了 @SentinelResource 注解,该注解允许在程序代码中自定义 Sentinel 资源点来实现对特定方法的保护,下面我们以熔断降级规则为例来进行讲解。熔断降级是指在某个服务接口在执行过程中频繁出现故障的情况下,在一段时间内将服务停用的保护措施。

在 sentinel-core 中基于 Spring AOP(面向切面技术)可在目标 Service 方法执行前按熔断规则进行检查,只允许有效的数据被送入目标方法进行处理。

还是以 sentinel-sample 工程为例,我希望对 SampleSerivce.createOrder 方法进行熔断保护,该如何设置呢?

第一步,声明切面类。

在应用入口 SentinelSampleApplication 声明 SentinelResourceAspect,SentinelResourceAspect 就是 Sentinel 提供的切面类,用于进行熔断的前置检查。

  1. @SpringBootApplication
  2. public class SentinelSampleApplication {
  3. @Bean
  4. public SentinelResourceAspect sentinelResourceAspect() {
  5. return new SentinelResourceAspect();
  6. }
  7. public static void main(String[] args) {
  8. SpringApplication.run(SentinelSampleApplication.class, args);
  9. }
  10. }

第二步,声明资源点。

在 SampleService.createOrder 方法上增加 @SentinelResource 注解用于声明 Sentinel 资源点,只有声明了资源点,Sentinel 才能实施限流熔断等保护措施。

  1. * 演示用的业务逻辑类
  2. */
  3. @Service
  4. public class SampleService {
  5. @SentinelResource("createOrder")
  6. * 模拟创建订单业务
  7. * 抛出IllegalStateException异常用于模拟业务逻辑执行失败的情况
  8. */
  9. public void createOrder() throws IllegalStateException{
  10. try {
  11. Thread.sleep(101);
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. System.out.println("订单已创建");
  16. }
  17. }

修改完毕,启动服务访问 http://localhost/test_degrade_rule,当见到 code=0 的 JSON 响应便代表应用运行正常。

  1. {
  2. code: "0",
  3. message: "order created!"
  4. }

然后打开访问 Sentinel Dashboard,在簇点链路中要确认 createOrder 资源点已存在。

13 | 生产实践:Sentinel 进阶应用场景 - 图4

createOrder 资源点已生效

第三步,获取熔断规则。

打开 sentinel-sample 工程的 application.yml 文件,将服务接入 Nacos 配置中心的参数以获取熔断规则。

  1. datasource:
  2. flow:
  3. ...
  4. degrade:
  5. nacos:
  6. server-addr: ${spring.cloud.nacos.server-addr}
  7. dataId: ${spring.application.name}-degrade-rules
  8. groupId: SAMPLE_GROUP
  9. rule-type: degrade
  10. username: nacos
  11. password: nacos

熔断规则的获取过程和前面流控规则类似,只不过 data-id 改为 sentinel-sample-degrade-rules,以及 rule-type 更改为 degrade。

启动过程中,出现下面日志代表配置成功。

  1. [main] s.n.www.protocol.http.HttpURLConnection : sun.net.www.MessageHeader@d96945215 pairs: {GET /nacos/v1/cs/configs?dataId=sentinel-sample-degrade-rules&accessToken=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTYxMDg5MDMwNH0.ooHkFb4zX14klmHMuLXTDkHSoCrwI8LtN7ex__9tMHg&group=SAMPLE_GROUP HTTP/1.1: null}...

第四步,在 Nacos 配置熔断规则。
设置 data-id 为 sentinel-sample-degrade-rules,Groups 为 SAMPLE_GROUP 与微服务的设置保持一致。

13 | 生产实践:Sentinel 进阶应用场景 - 图5

配置内容如下,我对每一项也做了说明。

  1. [{
  2. "resource": "createOrder", #自定义资源名
  3. "limitApp": "default", #命名空间
  4. "grade": 0, #0-慢调用比例 1-异常比例 2-异常数
  5. "count": 100, #最大RT 100毫秒执行时间
  6. "timeWindow": 5, #时间窗口5秒
  7. "minRequestAmount": 1, #最小请求数
  8. "slowRatioThreshold": 0.1 #比例阈值
  9. }]

上面这段 JSON 完整的含义是:在过去 1 秒内,如果 createOrder 资源被访问 1 次后便开启熔断检查,如果其中有 10% 的访问处理时间超过 100 毫秒,则触发熔断 5 秒钟,这期间访问该方法所有请求都将直接抛出 DegradeException,5 秒后该资源点回到 “半开” 状态等待新的访问,如果下一次访问处理成功,资源点恢复正常状态,如果下一次处理失败,则继续熔断 5 秒钟。
13 | 生产实践:Sentinel 进阶应用场景 - 图6

熔断机制示意图

设置成功,访问 Spring Boot Actuatorhttp://localhost/actuator/sentinel,可以看到此时 gradeRules 数组下 createOrder 资源点的熔断规则已被 Nacos 推送并立即生效。

13 | 生产实践:Sentinel 进阶应用场景 - 图7

自定义资源点熔断规则

下面咱们来验证下,因为规则允许最大时长为 100 毫秒,而在 createOrder 方法中模拟业务处理需要 101 毫秒,显然会触发熔断。

  1. @SentinelResource("createOrder")
  2. public void createOrder(){
  3. try {
  4. Thread.sleep(101);
  5. } catch (InterruptedException e) {
  6. e.printStackTrace();
  7. }
  8. System.out.println("订单已创建");
  9. }

连续访问 http://localhost/test_degrade_rule,当第二次访问时便会出现 500 错误。

13 | 生产实践:Sentinel 进阶应用场景 - 图8

已触发熔断的错误提示

在控制台日志也看到了 ERROR 日志,说明熔断已生效。

  1. 2021-01-17 17:19:44.795 ERROR 13244 --- [p-nio-80-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.reflect.UndeclaredThrowableException] with root cause
  2. com.alibaba.csp.sentinel.slots.block.degrade.DegradeException: null

看到 DegradeException 就说明之前配置的熔断规则已经生效。

讲到这,我们已经将 Sentinel 规则在 Nacos 配置中心进行了持久化,并通过 Nacos 的推送机制做到新规则的实时更新,但在刚才的过程中,我们在触发流控或熔断后默认的错误提示是非常不友好的,在真正的项目中还需要对异常进行二次处理才能满足要求。

开发友好的异常处理程序

对于 Sentinel 的异常处理程序要区分为两种情况:

  • 针对 RESTful 接口的异常处理;
  • 针对自定义资源点的异常处理。

针对 RESTful 接口的异常处理

默认情况下如果访问触发了流控规则,则会直接响应异常信息 “BlockedbySentinel (flow limiting)”。

13 | 生产实践:Sentinel 进阶应用场景 - 图9

触发流控的默认错误信息

我们都知道,RESTful 接口默认应返回 JSON 格式数据,如果直接返回纯文本,调用者将无法正确解析,所以咱们要对其进行封装提供更友好的 JSON 格式数据。

针对 RESTful 接口的统一异常处理需要实现 BlockExceptionHandler,我们先给出完整代码。

  1. @Component
  2. public class UrlBlockHandler implements BlockExceptionHandler {
  3. * RESTFul异常信息处理器
  4. * @param httpServletRequest
  5. * @param httpServletResponse
  6. * @param e
  7. * @throws Exception
  8. */
  9. @Override
  10. public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
  11. String msg = null;
  12. if(e instanceof FlowException){
  13. msg = "接口已被限流";
  14. }else if(e instanceof DegradeException){
  15. msg = "接口已被熔断,请稍后再试";
  16. }else if(e instanceof ParamFlowException){
  17. msg = "热点参数限流";
  18. }else if(e instanceof SystemBlockException){
  19. msg = "系统规则(负载/....不满足要求)";
  20. }else if(e instanceof AuthorityException){
  21. msg = "授权规则不通过";
  22. }
  23. httpServletResponse.setStatus(500);
  24. httpServletResponse.setCharacterEncoding("UTF-8");
  25. httpServletResponse.setContentType("application/json;charset=utf-8");
  26. ObjectMapper mapper = new ObjectMapper();
  27. mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
  28. mapper.writeValue(httpServletResponse.getWriter(),
  29. new ResponseObject(e.getClass().getSimpleName(), msg)
  30. );
  31. }
  32. }

BlockExceptionHandler.handle() 方法第三个参数类型是 BlockException,它有五种子类代表不同类型的规则异常。

1. FlowException:流控规则异常。
2. DegradeException:熔断规则异常。
3. ParamFlowException:热点参数规则异常。

例如:针对 id=5 的冷门商品编号时不开启限流,针对 id=10 的热门商品编号则需要进行限流,当 10 号商品被限流时抛出热点参数异常。

4.SystemBlockException:系统规则异常。

例如:服务器 CPU 负载超过 80%,抛出系统规则异常。

5. AuthorityException:授权规则异常。

例如:某个 IP 被列入黑名单,该 IP 在访问时就会抛出授权规则异常。

我们利用 instanceof 关键字确定具体的规则异常后,便通过 response 响应对象将封装好的 ResponseObject 对象返回给应用前端,此时响应中 code 值不再为 0,而是对应的异常类型。

例如,当 RESTful 触发流控规则后,前端响应如下:

  1. {
  2. code: "FlowException",
  3. message: "接口已被限流"
  4. }

当触发熔断规则后,前端响应如下。

  1. {
  2. code: "DegradeException",
  3. message: "接口已被熔断,请稍后再试"
  4. }

通过这种统一而通用的异常处理机制,对 RESTful 屏蔽了 sentinel-core 默认的错误文本,让项目采用统一的 JSON 规范进行输出。

说完了 RESTful 的异常处理,咱们再来说一说自定义资源点如何控制异常信息。

自定义资源点的异常处理

自定义资源点的异常处理与 RESTful 接口处理略有不同,我们需要在 @SentinelResource 注解上额外附加 blockHandler 属性进行异常处理,这里先给出完整代码。

  1. * 演示用的业务逻辑类
  2. */
  3. @Service
  4. public class SampleService {
  5. @SentinelResource(value = "createOrder",blockHandler = "createOrderBlockHandler")
  6. * 模拟创建订单业务
  7. * 抛出 IllegalStateException 异常用于模拟业务逻辑执行失败的情况
  8. */
  9. public void createOrder() throws IllegalStateException{
  10. try {
  11. Thread.sleep(101);
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. System.out.println("订单已创建");
  16. }
  17. public void createOrderBlockHandler(BlockException e) throws IllegalStateException{
  18. String msg = null;
  19. if(e instanceof FlowException){
  20. msg = "资源已被限流";
  21. }else if(e instanceof DegradeException){
  22. msg = "资源已被熔断,请稍后再试";
  23. }else if(e instanceof ParamFlowException){
  24. msg = "热点参数限流";
  25. }else if(e instanceof SystemBlockException){
  26. msg = "系统规则(负载/....不满足要求)";
  27. }else if(e instanceof AuthorityException){
  28. msg = "授权规则不通过";
  29. }
  30. throw new IllegalStateException(msg);
  31. }
  32. }

在这份代码中可以清楚地看到以下两点变化。

第一,我们为 @SentinelResource 附加了 blockHandler 属性,这个属性指向 createOrderBlockHandler 方法名,它的作用是当 sentinel-core 触发规则异常后,自动执行 createOrderBlockHandler 方法进行异常处理。

第二,createOrderBlockHandler 方法的书写有两个要求:

  • 方法返回值、访问修饰符、抛出异常要与原始的 createOrder 方法完全相同。
  • createOrderBlockHandler 方法名允许自定义,但最后一个参数必须是 BlockException 对象,这是所有规则异常的父类,通过判断 BlockException 我们就知道触发了哪种规则异常。

至于 createOrderBlockHandler 方法的代码和 RESTful 异常处理基本一致,先判断规则异常的种类再对外抛出 IllegalStateException 异常。SampleController 会对 IllegalStateException 异常进行捕获,对外输出为 JSON 响应。

当程序运行后,咱们看一下结果。

访问 http://localhost/test_degrade_rule 当触发流控规则后,响应如下:

  1. {
  2. code: "IllegalStateException",
  3. message: "资源已被限流"
  4. }

当触发熔断规则后,响应如下:

  1. {
  2. code: "IllegalStateException",
  3. message: "资源已被熔断,请稍后再试"
  4. }

讲到这里,我们针对在生产实践中 Sentinel 必然会涉及的应用场景进行了讲解,下面咱们进行总结。

小结与预告

本讲咱们学习了三方面内容,首先通过流控案例说明如何利用 Nacos 配置中心将 Sentinel 规则持久化存储,并利用 Nacos 的推送功能实现新规则的实时更新;其次咱们通过熔断规则学习了 @SentinelResource 注解的用法,同时将自定义资源点的熔断规则放入 Nacos 进行持久化;最后针对流控熔断中默认非常不友好的异常输出,咱们利用 Sentinel 自带的机制逐一进行的优化,生成了符合项目要求的友好的 JSON 格式数据。

在这里我为你留一道自习题:Sentinel 除了流控与熔断外,还有三种不常用的规则:

  • 热点参数流控;
  • 系统规则;
  • 授权规则(黑白名单)。

请你自行前往 Sentinel 官网文档https://github.com/alibaba/Sentinel/wiki 进行学习。

到这里关于 Sentinel 的话题咱们告一段落。下一讲开始进入新的篇章,学习微服务架构下的链路追踪技术。