介绍
- 学习下SpringCloudAlibaba的使用,笔记主要是阿里云官网的Spring Cloud Alibaba 七天训练营中的内容和例子.
- 本地测试的demo地址在github上
Spring Cloud Alibaba Sentinel 服务限流/熔断实战
实验环境准备
需要提前下载并启动sentinel的dashboard,下载路径及wiki,本次下载的是sentinel-dashboard-1.8.1
启动服务端
启动本地nacos(略)
启动sentinel的dashboard
java -Dserver.port=8849 -Dcsp.sentinel.dashboard.server=localhost:8849 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.1.jar
tips: sentinel 控制台的默认账号和密码都是 “sentinel”
搭建微服务系统
这里在上一个dubbo的例子中修改,主要结构如下:
- dubbo-sample-api: 服务接口定义,供 consumer/provider 引用
- dubbo-provider-sample : Dubbo 服务端,对外提供一些服务
- dubbo-consumer-sample: 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 服务。
配置sentinel
添加依赖
在 dubbo-provider-sample和dubbo-consumer-sample中的build.gradle文件中加入sentinel的依赖包,配置如下:
dependencies {
implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-sentinel'
implementation 'com.alibaba.csp:sentinel-apache-dubbo-adapter'
}
修改代码和配置
修改dubbo-provider-sample和dubbo-consumer-sample中application.properties文件,添加sentinel的配置,如下:
# Sentinel 控制台地址
spring.cloud.sentinel.transport.dashboard=127.0.0.1:8849
# 取消Sentinel控制台懒加载
# 默认情况下 Sentinel 会在客户端首次调用的时候进行初始化,开始向控制台发送心跳包
# 配置 sentinel.eager=true 时,取消Sentinel控制台懒加载功能
spring.cloud.sentinel.eager=true
在dubbo-consumer-sample中,增加sentinel的配置类,以此来处理限流异常
package com.example.dubboconsumersample.demos.sentinel;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.PrintWriter;
@Configuration
public class SentinelWebConfig {
@Bean
public BlockExceptionHandler sentinelBlockExceptionHandler() {
return (request, response, e) -> {
// 429 Too Many Requests
response.setStatus(429);
PrintWriter out = response.getWriter();
out.print("Oops, blocked by Sentinel: " + e.getClass().getSimpleName());
out.flush();
out.close();
};
}
}
在dubbo-consumer-sample中,新建DemoService类,增加sentinel的限流配置,增加后代码如下
package com.example.dubboconsumersample.demos.sentinel;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.stereotype.Service;
@Service
public class DemoService {
@SentinelResource(value = "DemoService#bonjour", defaultFallback = "bonjourFallback")
public String bonjour(String name) {
return "Bonjour, " + name;
}
public String bonjourFallback(Throwable t) {
if (BlockException.isBlockException(t)) {
return "Blocked by Sentinel: " + t.getClass().getSimpleName();
}
return "Oops, failed: " + t.getClass().getCanonicalName();
}
}
在dubbo-sample-api添加dubbo的接口类,如下
package com.example.dubbo.api;
/**
* 该接口为 Dubbo 的服务端、消费端公用的接口定义。
* 当前案例中,通过复制代码的方式实现接口发布,这不是最优雅的使用方法。更好的建议是通过maven坐标的方式独立维护api。
*/
public interface FooService {
String sayHello(String name);
long getCurrentTime(boolean slow);
}
在dubbo-provider-sample中新建FooServiceImpl类,来实现接口功能
package com.example.dubboprovidersample.service;
import com.example.dubbo.api.FooService;
import org.apache.dubbo.config.annotation.DubboService;
/**
* demo dubbo sentinel
*/
@DubboService
public class FooServiceImpl implements FooService {
@Override
public String sayHello(String name) {
return "Hello, " + name;
}
@Override
public long getCurrentTime(boolean slow) {
// Simulate slow invocations randomly.
if (slow) {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
}
}
return System.currentTimeMillis();
}
}
编译并启动dubbo-provider-sample和dubbo-consumer-sample两个应用
测试访问接口:
- http://127.0.0.1:62000/demo/hello?name=theonefx
- http://127.0.0.1:62000/demo/time
- http://127.0.0.1:62000/demo/bonjour/theonefx
分别返回 - Hello, theonefx
- 1612775553724(时间戳)
- Bonjour, theonefx
流控规则
流量控制配置
(略)根据文档操作
熔断降级规则
熔断降级通常用于自动切断不稳定的服务,防止调用方被拖垮导致级联故障。熔断降级规则通常在调用端,针对弱依赖调用进行配置,在熔断时返回预定义好的 fallback 值,这样可以保证核心链路不被不稳定的旁路影响。
Sentinel 提供以下几种熔断策略:
- 慢调用比例 (SLOWREQUESTRATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 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 服务调用配置熔断降级规则。
控制台配置的统计时长默认为 1s。在上面的这条规则中,我们设定慢调用临界值为 50ms,响应时间超出 50ms 即记为慢调用。当统计时长内的请求数 >=5 且慢调用的比例超出我们配置的阈值(80%)就会触发熔断,熔断时长为 5s,经过熔断时长后会允许一个请求探测通过,若请求正常则恢复,否则继续熔断。
我们的实例中 /demo/time API 可以通过 slow 请求参数模拟慢调用,当 slow=true 时该请求耗时会超过 100ms。我们可以用 ab 等压测工具或脚本,批量请求 localhost:8090/demo/time?slow=true,可以观察到熔断的返回
2021-02-19 10:26:46.415 ERROR 13588 --- [io-62000-exec-6] 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.RuntimeException: SentinelBlockException: DegradeException] with root cause
java.lang.RuntimeException: SentinelBlockException: DegradeException
at com.alibaba.csp.sentinel.slots.block.BlockException.block(BlockException:0) ~[sentinel-core-1.8.0.jar:1.8.0]
021
如果我们一直模拟慢调用,我们可以观察到熔断后每 5s 会允许通过一个请求,但该请求仍然是慢调用,会重新打回熔断,无法恢复。我们可以在触发熔断后,等待一段时间后手动发一个不带 slow=true 的正常请求,然后再进行请求,可以观察到熔断恢复。
需要注意的是,即使服务调用方引入了熔断降级机制,我们还是需要在 HTTP 或 RPC 客户端配置请求超时时间,来做一个兜底的防护。
注解方式自定义埋点
刚才我们看到的埋点都是 Sentinel 适配模块提供的自动埋点。有的时候自动埋点可能没法满足我们的需求,我们希望在某个业务逻辑的位置进行限流,能不能做到呢?当然可以!Sentinel 提供两种方式进行自定义埋点:SphU API 和 @SentinelResource 注解,前者最为通用但是代码比较繁杂,耦合度较高;注解方式侵入性较低,但有使用场景的限制。这里我们来动手在 Web 应用的 DemoService 上添加注解,来达到针对本地服务埋点的目标。
在 DemoService 中我们实现了一个简单的打招呼的服务:
@Service
public class DemoService {
public String bonjour(String name) {
return "Bonjour, " + name;
}
}
下面我们给 bonjour 这个函数添加 @SentinelResource 注解,注解的 value 代表这个埋点的名称(resourceName),会显示在簇点链路/监控页面。
@SentinelResource(value = "DemoService#bonjour", defaultFallback = "bonjourFallback")
public String bonjour(String name) {
return "Bonjour, " + name;
}
加上该注解后,再通过网关访问 /demo/bonjour/{name} 这个 API 的时候,我们就可以在簇点链路页面看到我们自定义的 DemoService#bonjour 埋点了。
添加注解埋点只是第一步。一般在生产环境中,我们希望在这些自定义埋点发生限流的时候,有一些 fallback 逻辑,而不是直接对外抛出异常。这里我们可以写一个 fallback 函数:
public String bonjourFallback(Throwable t) {
if (BlockException.isBlockException(t)) {
return "Blocked by Sentinel: " + t.getClass().getSimpleName();
}
return "Oops, failed: " + t.getClass().getCanonicalName();
}
完整代码如下:
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.stereotype.Service;
@Service
public class DemoService {
@SentinelResource(value = "DemoService#bonjour", defaultFallback = "bonjourFallback")
public String bonjour(String name) {
return "Bonjour, " + name;
}
public String bonjourFallback(Throwable t) {
if (BlockException.isBlockException(t)) {
return "Blocked by Sentinel: " + t.getClass().getSimpleName();
}
return "Oops, failed: " + t.getClass().getCanonicalName();
}
}
这样当我们自定义的 DemoService#bonjour 资源被限流或熔断的时候,请求会走到 fallback 的逻辑中,返回 fallback 结果,而不会直接抛出异常。我们可以配一个 QPS=1 的限流规则,然后快速请求后观察返回值:
通过页面刷新访问http://127.0.0.1:62000/demo/bonjour/theonefx 返回:
Blocked by Sentinel: FlowException
注意:使用 @SentinelResource 注解要求对应的类必须由 Spring 托管(即为 Spring bean),并且不能是内部调用(没法走到代理),不能是 private 方法。Sentinel 注解生效依赖 Spring AOP 动态代理机制。
配置自定义的流控处理逻辑
Sentinel 的各种适配方式均支持自定义的流控处理逻辑。以 Spring Web 适配为例,我们只需要提供自定义的 BlockExceptionHandler 实现并注册为 bean 即可为 Web 埋点提供自定义处理逻辑。其中 BlockExceptionHandler 的定义如下:
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.PrintWriter;
@Configuration
public class SentinelWebConfig {
@Bean
public BlockExceptionHandler sentinelBlockExceptionHandler() {
return (request, response, e) -> {
// 429 Too Many Requests
response.setStatus(429);
PrintWriter out = response.getWriter();
out.print("Oops, blocked by Sentinel WebConfig: " + e.getClass().getSimpleName());
out.flush();
out.close();
};
}
}
在sentinel的链路簇点资源/demo/bonjour/{name}处增加流控规则,测试访问http://127.0.0.1:62000/demo/bonjour/theonefx ,返回结果如下:
Oops, blocked by Sentinel WebConfig: FlowException
该 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 文档来进行接入和配置。
总结
通过本教程,我们了解了流控降级作为高可用防护手段的重要性,了解了 Sentinel 的核心特性和原理,并通过动手实践学习了如何快速接入 SCA Sentinel 来为微服务进行流控降级。Sentinel 还有着非常多的高级特性等着大家去发掘,如热点防护、集群流控等,大家可以参考 Sentinel 官方文档来了解更多的特性和场景。
那么是不是服务的量级很小就不用进行限流防护了呢?是不是微服务的架构比较简单就不用引入熔断保护机制了呢?其实,这与请求的量级、架构的复杂程度无关。很多时候,可能正是一个非常边缘的服务出现故障而导致整体业务受影响,造成巨大损失。我们需要具有面向失败设计的意识,在平时就做好容量规划和强弱依赖的梳理,合理地配置流控降级规则,做好事前防护,而不是在线上出现问题以后再进行补救。