一、Sentinel介绍
Sentinel 是面向分布式服务架构的流量控制组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。
-
流量控制
任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状。
-
熔断降级
及时对调用链路中的不稳定因素进行熔断也是 Sentinel 的使命之一,Sentinel 和 Hystrix 的原则是一致的,当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联故障。
-
系统保护
Sentinel 为服务集群提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。
-
实时监控
Sentinel 提供实时的监控系统,方便您快速了解目前系统的状态。
二、Dashboard的搭建与启动
从官网下载最新版本的jar包,然后按照如下命令启动Dashboard。
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.x.x.jar
启动成功后,我们就可以在localhost:8080端口访问Dashboard的网站了,初始账密为sentinel。
三、应用入门案例
3.1 应用搭建
首先新建一个项目,引入必要的模块:web和sentinel
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2021.1</version>
</dependency>
然后新建一个Controller
@Slf4j
@RestController
public class HelloController {
@GetMapping("/getHello")
public String getHello(){
return "hello";
}
}
最后,我们需要增加如下的配置内容,来对sentinel进行配置。
server.port=8081
spring.application.name=sentinel-demo
# 指定sentinel控制台的地址
spring.cloud.sentinel.transport.dashboard=localhost:8080
# 指定和控制台通信的端口,默认8719
spring.cloud.sentinel.transport.port=8719
# 指定心跳周期,默认null
spring.cloud.sentinel.transport.heartbeat-interval-ms=10000
到此,我们可以启动项目了,然后通过访问localhost:8081/getHello
多尝试几次,然后过一会就可以在Dahboard上看到我们的调用监控了。
当然,如果需要监控的不是接口,而是普通的方法,我们可以使用@SentinelResource
注解来达成目的。
@Slf4j
@Component
public class HelloJob {
@Scheduled(cron = "0/10 * * * * ?")
@SentinelResource("helloJob")
public void startHello(){
log.info("hello! hello.");
}
}
@EnableScheduling
@SpringBootApplication
public class SentinelDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SentinelDemoApplication.class, args);
}
}
重新启动项目,发现我们的helloJob也可以被Dashboard监控管理了。
3.2 抛出异常的方式定义资源
此时我们还没有用到Sentinel的任何功能,我们来试下对如上的Controller来进行QPS的限流,这里使用的方式是抛出异常的方式。
/**
* 抛出异常的方式定义资源
* @return
*/
@GetMapping("/getHello")
public String getHello() {
// 使用限流规则,保护”业务逻辑“
try(Entry entry = SphU.entry("getHello")) {
// 正常的业务逻辑
return "OK";
} catch (Exception e) {
log.info("当前请求被限流了!", e);
// 降级方案
return "系统繁忙,请稍后再试!";
}
}
/**
* 定义隔离规则
*
* @PostConstruct 当前类的构造函数执行之后执行该方法
*/
@PostConstruct
public void initFlowRule() {
List<FlowRule> ruleList = new ArrayList<>();
FlowRule rule = new FlowRule();
// 设置资源名称
rule.setResource("getHello");
// 指定限流模式为QPS
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
// 指定QPS限流阈值
rule.setCount(2);
ruleList.add(rule);
// 加载该规则
FlowRuleManager.loadRules(ruleList);
}
我们在代码中设置了/getHello的QPS阈值为2,那么此时启动项目后,快速刷新访问该URL,就可能会看到限流后的降级输出。
3.3 返回布尔值方式定义资源
我们还有其它方式来实现如上相同的功能:
/**
* 返回布尔值方式定义资源
* @return
*/
@GetMapping("/getHi")
public String getHi() {
if (SphO.entry("getHello")) {
try {
// 正常的业务逻辑
return "Hi";
} finally {
SphO.exit();
}
} else {
log.info("当前请求被限流了!");
// 降级方案
return "系统繁忙,请稍后再试!";
}
}
3.4 异步调用方式定义资源
前面的例子都是同步调用的,我们来个异步调用的例子,前端请求到了之后,异步去处理具体的业务逻辑。
首先我们需要在启动类上增加@EnableAsync
注解。
其次我们创建一个异步业务类:
@Slf4j
@Service
public class AsyncService {
@Async
public void asyncInvoke(){
log.info("开始asyncInvoke。。。");
try {
// 沉睡10秒,模拟远程调用
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("结束asyncInvoke。。。");
}
}
然后,我们在上面例子中的controller类中增加一个:
@Autowired
private AsyncService asyncService;
/**
* 异步调用方式定义资源
*/
@GetMapping("/getAsync")
public String getAsync(){
AsyncEntry asyncEntry = null;
try {
asyncEntry = SphU.asyncEntry("getHello");
// 正常的业务逻辑
asyncService.asyncInvoke();
} catch (BlockException e) {
log.info("当前请求被限流了:",e);
// 降级方案
return "系统繁忙,请稍后再试!";
} finally {
if(asyncEntry != null){
asyncEntry.exit();
}
}
return "OK";
}
3.5 使用注解的方式定义资源
/**
* 使用注解额方式定义资源
*/
@GetMapping("/getAnnotation")
@SentinelResource(value = "getHello", blockHandler = "exceptionHandler")
public String getAnnotation(){
// 正常的业务逻辑
return "annotation";
}
public String exceptionHandler(BlockException blockException){
// 降级方案
log.info("当前请求被限流了:",blockException);
return "系统繁忙,请稍后再试!";
}
在实际使用Sentinel的过程中,我们很少使用如上硬编码的方式来设置规则,因为如果需要改规则会很不方便,所以一般推荐使用Sentinel Dashboard来设置和编辑规则,这部分在如下例子中讲解。
四、流量控制
4.1 并发线程数控制
用于保护业务线程池不被慢调用耗尽。和Hystrix相比,Hystrix是使用了线程池隔离的技术,虽然隔离的比较彻底,不会存在两个不同业务调用相互竞争的情况,但是线程资源会比较浪费,线程的切换也会带来很大的性能损耗。Sentinel则是不会创建和管理线程池,仅仅是统计当前资源上下文的总线程数,只要超过阈值,就拒绝新的调用,效果类似于Hystrix中的信号量模式。
下面我们来实验下并发线程数控制的效果:
还是上面入门案例,我们改造一下HelloController,主要是使得请求线程沉睡1秒,使得每一个请求线程在1秒内只会处理一个请求。
@GetMapping("/getHello")
public String getHello() throws InterruptedException {
Thread.sleep(1000);
return "hello";
}
然后我们使用Jmeter做并发压力测试,设置线程数量为30,循环2次,开始后Seninel控制台会显示所有请求全部成功,此时系统能承受的最大并发数量上限为tomcat容器允许的最大线程数。
然后,在Sentinel的控制台上找到”流控规则“,新增一个规则,并在如下选项中依次填写:
此时,我们重复如上Jmeter的并发测试,会发现1秒内的并发数量被降低到20以下了,说明Sentinel限流成功。
4.2 QPS控制
是用于限制某个资源的请求并发数量,和线程无关,我们使用如上HelloController的例子,去掉线程的沉睡代码。在不限QPS的时候,Jmeter并发30个线程,循环两次,发现都可以访问成功。
然后我们增加流控规则如下:
此时启动Jmeter,会发现QPS被限制为一秒内最多20个,超过20个的都会拒绝,从而使请求失败。
五、熔断降级
5.1 RT响应时间
DEGRADE_GRADE_RT,当一秒内对某个资源发起的多个请求,它们的平均响应时间超过了阈值T,那么在接下来的N秒内,所有对这个资源的请求访问都会被熔断进行降级处理。其中,阈值T是我们设置的毫秒数,N秒是我们设置的熔断时间窗口。
5.2 异常比例
DEGRADE_GRADE_EXCEPTION_RATIO,当每秒对某个资源的请求量超过阈值C的时候,异常请求/正常请求的比例超过阈值P时,在接下来的T秒时间窗口内,所有对这个资源的请求访问都会被熔断进行降级处理。其中并发量C、比例阈值P、时间窗口T都是我们可以设置的。
5.3 异常数
DEGRADE_GRADE_EXCEPTION_COUNT,最近一分钟内对某个资源的请求异常数量达到我们设置的阈值C的时候,在该分钟的剩余时间里,所有对这个资源的请求访问都会被熔断进行降级处理。在下一分钟才能恢复访问。
这部分熔断降级的案例既可以通过如上案例中硬编码在代码里,也可以在Sentinel Dashboard的降级规则里面设置,比较简单,不再演示。
六、整合Feign
首先需要引入Fein的依赖:
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>3.0.3</version>
</dependency>
然后,我们创建一个Feign调用的service,并指定降级处理方法。
@FeignClient(name="feign-helo", url = "http://localhost:8081", fallback = FeignFallBackService.class)
public interface HelloFeignService {
@RequestMapping(value = "/getHello0", method = RequestMethod.GET)
String getHello();
}
@Component
public class FeignFallBackService implements HelloFeignService {
@Override
public String getHello() {
// 降级方案
return "Feign提示系统繁忙,请稍后再试!";
}
}
然后,我们在如上Controller里面新增一个:
@Autowired
private HelloFeignService helloFeignService;
/**
* 整合Feign
*/
@GetMapping("/getFeignHello")
public String getFeignHello(){
return helloFeignService.getHello();
}
现在我们需要在启动类上加上@EnableFeignClients
注解,使得Feign生效。
最后一步,增加如下的配置内容:
# feign开启sentinel支持,否则降级方法得不到执行,直接抛出异常
feign.sentinel.enabled=true
此时我们使用JMeter做测试,调用/getFeignHello
接口,就会发现通过Feign调用的/getHello
是有限流的,当/getHello
服务不可用时,会执行Feign的降级方法。
相关内容和Hystrix整合OpenFeign比较类似,可以参考:
Hystrix使用入门
七、整合Gateway
关于Gateway的配置可以参考:Gateway使用入门,本文直接在这基础上进行改造。
首先,我们新建一个配置类,用以当网关转发请求失败的时候降级处理的逻辑。
@Component
public class GatewayConfiguration {
/**
* 设置限流或者降级之后的处理方法,对所有路径都生效
*/
@PostConstruct
public void doInit(){
GatewayCallbackManager.setBlockHandler(new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
return ServerResponse.status(200).syncBody("gateway提示:系统繁忙,请稍后再试!");
}
});
}
}
然后在配置文件中先增加和Sentinel Dashboard的链接配置:
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080
port: 8720
然后增加一个路由规则,用以将请求网关的请求转发到上面例子中的getHello
中。
spring:
cloud:
gateway:
routes:
- id: gateway-hello
uri: http://localhost:8081
predicates:
- Path=/getHello
此时,我们同时启动网关服务和上面的getHello
服务。先测试下gateway能否正常工作,请求localhost:8089/getHello
,如果正常返回getHello
的返回内容,则说明gateway项目没有问题。
此时,我们的降级方案配置GatewayConfiguration 还不能起作用,因为还没有限流的设置,所以我们可以在SentinelDashboard上增加相应的限流规则。
- Route ID,根据配置文件中配置的id进行限流;
- API分组,根据请求API路径进行匹配限流;
一旦触发限流,那么就会使用GatewayConfiguration 的降级处理方案来响应请求。
需要注意的是,在本案例中,gateway和hello项目的限流规则将会同时起作用,如果要准确测试gateway的限流效果,建议可以先关闭hello项目的限流规则。
- 场景1:gateway限流6,hello限流2,我们并发20个请求,那么gateway将会拦截14个,进入gateway降级方案处理,只通过6个请求转发给hello,然后hello再拦截4个,进入hello降级方案处理,最终只通过2个。
- 场景2:gateway限流6,hello不限流,我们并发20个请求,那么gateway将会拦截14个,进入gateway降级方案处理,只通过6个请求转发给hello,全部处理成功。
- 场景3:gateway限流6,hello停机,我们并发20个请求,那么gateway拦截的会走降级处理方案,通过的请求因为找不到hello服务,所以抛出异常了。
八、系统自适应保护
Sentinel还支持根据应用机器的负载情况来自适应地对我们的资源进行限流和降级。使得系统的入口流量和系统的负载达到一个平衡,不会因为流量过大拖垮机器。
Sentinel自适应保护的主要模式有如下几种:
- Load自适应,仅对Linux/Unix-like的机器生效,即将系统的Load1作为启发指标,当系统超过设定的启发值后,Sentinel估算当前机器的并发线程数超过了预估的最大容量后,对资源进行熔断降级。
- CPU Usage,根据当前机器的CPU使用率是否超过了设定的阈值来触发熔断降级。
- 平均RT,当前机器的所有入口流量平均RT超过设定的阈值时触发熔断降级。
- 并发线程数,当前机器的所有入口流量造成的并发线程数超过设定的阈值;
- 入口QPS,当前机器的所有入口流量造成的QPS数超过设定的阈值;
需要注意的是,以上设定的自适应保护规则,会对所有入口流量的资源生效。
/**
* 系统自适应保护规则测试
*/
@GetMapping("/getAdapt1")
// 标识为入口流量资源
@SentinelResource(entryType = EntryType.IN)
public String getAdapt1(){
return "adapt1";
}
@GetMapping("/getAdapt2")
// 未标识为入口资源,但也属于入口流量
public String getAdapt2(){
return "adapt2";
}
以上两个都属于系统的入口流量资源,当超过设定的阈值后,都会被Sentinel自动降级处理,比如返回结果如下:
Blocked by Sentinel (flow limiting)
九、授权控制
主要作用就是对资源的访问进行授权控制,通过黑白名单的形式来限制某个请求是否有权限访问我们的某个资源。
- 白名单,在白名单中的请求来源(IP)允许访问资源;
- 黑名单,在黑名单中的请求来源(IP)不允许访问资源;
原理就是通过HttpServletRequest.getRemoteAddr()
来获取请求方的IP地址,从而判断其是否在黑、白名单中来达到限制的目的。
十、动态规则扩展
在上面的例子中,讲到规则的配置只有两种方式,要么在代码里面写好了,要么在Sentinel Dashboard上进行配置。在实际的场景中,我们很少采用第一种方式,因为如果涉及到规则的变更那就需要改动代码了,十分不便,那么对于第二种方式,配置的规则是保存在内存中的,一旦服务重启规则就没有了,所以不能持久化。
动态规则要想实现扩展,一般都是结合配置中心来实现持久化的,同时也方便动态地更改规则。Sentinel支持Consul、Zookeeper、Redis、Nacos、Apollo、etcd等数据源的持久化支持,本文则演示使用Nacos的例子。
开始之前,可以先参考Nacos使用入门了解下Nacos的基本入门使用,我们需要先将Nacos服务端启动起来。
然后,我们的sentinel项目中需要引入nacos的datasource支持:
<!-- https://mvnrepository.com/artifact/com.alibaba.csp/sentinel-datasource-nacos -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.8.2</version>
<scope>test</scope>
</dependency>
然后在nacos服务端,新建并写上我们的限流规则:
[
{
"resource": "/getNacos",
"limitApp": "default",
"grade": 1,
"count": 2,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
对应dataId为自定义的,比如sentinel-nacos;group就是用DEFAULT_GROUP。
然后,我们就需要将如上Nacos中的限流规则加载到应用中,如下两种方式是等同的,可以视情况选择一种。
- init
我们在上面getHello的例子中使用过在代码中配置限流规则的案例,现在把其改为:
/**
* 使用Nacos数据源来管理规则
*/
@PostConstruct
public void initDataSource() {
String remoteAddress = "192.168.31.17:8848";
String groupId = "DEFAULT_GROUP";
String dataId = "sentinel-nacos";
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId
, source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
}));
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
}
如上代码即是在初始化的时候就将Nacos上的限流规则加载进来了,此时我们重启项目,对/getNacos
资源的限流就生效了,此时查看Sentinel Dashboard的限流规则就能看到这条规则,并且重启后不会丢失。
注意:@PostConstruct只能有一个,如果有多个,那么按照顺序只有第一个生效,所以此处需要替换/getHello
中的例子,而不是增加。
- 配置
新建一个类:
public class DataSourceInitFunc implements InitFunc {
@Override
public void init() throws Exception {
String remoteAddress = "192.168.31.17:8848";
String groupId = "DEFAULT_GROUP";
String dataId = "sentinel-nacos";
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId,
source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}));
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
}
}
然后在resources目录下,新建目录META-INF/services
,然后再创建一个文件,名称为com.alibaba.csp.sentinel.init.InitFunc
,里面写上刚才新建类的名称,比如:
com.example.sentineldemo.config.DataSourceInitFunc
此时就OK了。
注意,当启用Nacos作为配置规则的数据源之后,代码里面init时配置的规则就不再生效了。
十一、参考资料:
官方文档讲解的很详细很到位,各种问题和案例基本都能找到,推荐阅读官方文档。