微服务组件 Sentinel(一)
前言
在服务提供者不可用的时候,会出现大量重试的情况:用户重试、代码逻辑重试,这些重试最终导致:进-步加大请求流量。 所以归根结底导致雪崩效应的最根本原因是:大量请求线程同步等待造成
的资源耗尽。当服务调用者使用同步调用时,会产生大量的等待线程占用系统资源。一-旦线程 资源被耗尽服务调用者提供的服务也将处于不可用状态,于是服务雪崩效应产生了。
解决方案
稳定性(Reliability)、恢复性(Resilience)
常见的容错机制:
- 超时机制
在不做任何处理的情况下,服务提供者不可用会导致消费者请求线程强制等待,而造成系统资源耗尽。加入超时机制,一旦超时,就释放资源。由于释放资源速度较快,一定程度上可以抑制资源耗尽
的问题。
- 限流机制
例如一个服务的 QPS 为500,当有 800 个 QPS访问时,300 个 QPS 直接拒绝请求
- 隔离
原理:用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,则会进行降级处理,用户的请求不会被阻塞,至少可以看到一个执行结果(例如返回友好的提示信息),而不是无休止的等待或者看到系统崩溃。
- 信号隔离
制并发访问,防止阻塞扩散,与线程隔离最大不同在于执行依赖代码的线程依然是请求线程,该线程需要通过信号申请,如果客户端是可信的且可以快速返回,可以使用信号隔离替换线程隔离,降低开销。信号量的大小可以动态调整,线程池大小不可以。
- 服务熔断
`远程服务不稳定或网络抖动时暂时关闭,就叫服务熔断。`
现实世界的断路器大家肯定都很了解,断路器实时监控电路的情况,如果发现电路电流异常,就会跳闸,从而防止电路被烧毁。
软件世界的断路器可以这样理解:实时监测应用,如果发现在一定时间内失败次数/失败率达到一 定阈值,就“跳闸”,断路器打开,此时,请求直接返回,而不去调用原本调用的逻辑。跳闸一段时间后(例如10秒),断路器会进入半开状态,这是一个瞬间态,此时允许一次请求调用该调的逻辑,如果成功,则断路器关闭,应用正常调用;如果调用依然不成功,断路器继续回到打开状态,过段时间再进入半开状态尝试通过“跳闸”, 应用可以保护自己,而且避免浪费资源;而通过半开的设计,可实现应用的“自我修复”。
所以,同样的道理,`当依赖的服务有大量超时时,在让新的请求去访问根本没有意义,只会无畏的消耗现有资源`。比如我们设置了超时时间为1s,如果短时间内有大量请求在1s内都得不到响应,就意味着这个服务出现了异常,此时就没有必要再让其他的请求去访问这个依赖了,这个时候就应该使用断路器避免资源浪费。
- 服务降级
有服务熔断,必然要有服务降级。
所谓降级,就是当某个服务熔断之后,服务将不再被调用,此时客户端可以自己准备一个本地的fallback(回退)回调,返回一个缺省值。例如: (备用接口/缓存/mock数据)。这样做,虽然服务水平下降,但好歹可用,比直接挂掉要强,当然这也要看适合的业务场景。
一、什么是 Sentinel
Sentinel 是阿里巴巴开源的,面向分布式架构的高可用防护组件,是流量防卫兵。提供了多维度的流控降级能力,以不同的运行指标为基准,例如可以针对 QPS,并发线程数,异常,响应时间,系统负载等不同运行指标进行自动降级,排队,快速失败。同时也提供了秒级实时监控与动态规划管理。
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式服务架构的流量控制组件,主要以流量为切入点从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。
Sentinel 具有以下特征
- 丰富的应用场景: Sentinel 承接了阿里巴巴近10年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、实时熔断下游不可用应用等。
- 完备的实时监控: Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至500台以下规模的集群的汇总运行情况。
- 广泛的开源生态: Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与Spring Cloud、Dubbo、 gRPC的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入Sentinel。
- 完善的SPI扩展点: Sentinel 提供简单易用、完善的SPI扩展点。您可以通过实现扩展点,快速的定制逻辑。例如定制规则管理、适配数据源等。
Sentinel | Hystrix | |
---|---|---|
隔离策略 | 信号量隔离 | 线程池隔离/信号量隔离 |
熔断降级策略 | 基于响应时间或失败比例 | 基于失败比率 |
实时指标实现 | 滑动窗口 | 滑动窗口(基于RxJava) |
规则配置 | 支持多种数据源 | 支持多种数据源 |
拓展性 | 多个拓展性 | 插件的形式 |
基于注解的支持 | 支持 | 支持 |
限流 | 基于 QPS,支持基于调用关系的限流 | 有限的支持 |
流量整形 | 支持慢启动、匀速器模式 | 不支持 |
系统负载保护 | 支持 | 不支持 |
控制台 | 开箱即用,可配置规则,查看秒级监控,机器发现等 | 不完善 |
常见框架的适配 | Servlet、SpringCloud、Dubbo、GRpc | Servlet、SpringCloud Netflix |
二、Sentinel 流控体验
2.1、Sentinel 快速开始
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--sentinel 核心库-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.4</version>
</dependency>
controller 层代码,定义资源,一般资源名称和接口名称保持一致,并且为资源设置流控规则
@RestController
@Slf4j
public class HelloController {
public static final String RESOURCE_NAME = "hello";
public static final String USER_RESOURCE_NAME = "user";
public static final String DEGRADE_RESOURCE_NAME = "degrade";
// 进行流控
@RequestMapping("/hello")
public String hello() {
Entry entry = null;
try {
// sentinel 针对资源进行限制的
entry = SphU.entry(RESOURCE_NAME);
// 被保护的业务逻辑
String str = "hello world";
log.info("======"+str+"======");
return str;
} catch (BlockException e) {
// 资源访问阻止,被限流或降级
// 进行相应的处理操作
log.info("block!");
return "被流控了!";
} catch (Exception e) {
// 若需要配置降级规则,需要通过这种方式记录业务异常
Tracer.traceEntry(e,entry);
} finally {
if (entry != null) {
entry.exit();
}
}
return null;
}
/**
* 定义规则
*
* spring 的初始化方法
*/
@PostConstruct // init method
public static void initFlowRules() {
// 流控规则
List<FlowRule> rules = new ArrayList<>();
// 流控
FlowRule rule = new FlowRule();
// 设置受保护的资源,为哪个资源进行流控
rule.setResource(RESOURCE_NAME);
// 设置流控规则,qps 每秒访问数
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
// 设置受保护的资源阈值
// Set limit QPS to 1
rule.setCount(1);
rules.add(rule);
// 加载配置好的规则
FlowRuleManager.loadRules(rules);
}
}
请求http://localhost:8060/hello
,当请求过于频繁时,被流控
2.2、@SentinelResource
上面的代码虽然实现了流控,但是我们可以发现,Sentinel 与代码的耦合性太强了,使用@SentinelResource
注解
1、引入依赖
<!--如果要使用 @SentinelResource-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
<version>1.8.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
2、设置 bean
// 注解支持的配置 Bean
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
3、资源限流设置
@RestController
@Slf4j
public class HelloController {
public static final String RESOURCE_NAME = "hello";
public static final String USER_RESOURCE_NAME = "user";
public static final String DEGRADE_RESOURCE_NAME = "degrade";
/**
* 定义规则
* <p>
* spring 的初始化方法
*/
@PostConstruct // init method
public static void initFlowRules() {
// 流控规则
List<FlowRule> rules = new ArrayList<>();
// 通过 @SentinelResource 来定义资源并配置剪辑和流控的处理方法
FlowRule rule2 = new FlowRule();
// 设置受保护的资源
rule2.setResource(USER_RESOURCE_NAME);
// 设置流控规则 QPS
rule2.setGrade(RuleConstant.FLOW_GRADE_QPS);
// 设置受保护的资源的阈值
rule2.setCount(1);
rules.add(rule2);
// 加载配置好的规则
FlowRuleManager.loadRules(rules);
}
/**
* @SentinelResource 改善接口中资源定义和被流控降级后的处理方法
* 怎么使用
* 1、添加依赖
* 2、配置Bean SentinelResourceAspect
* value 定义资源
* blockHandler 设置流控降级后的处理方法(默认该方法必须设置在一个同一个类中)
* 如果不想在同一个类中,可以设置 blockHandlerClass,并且方法必须是静态方法
* fallback 当接口出现异常,就可以交给 fallback 指定的方法进行处理
*/
@RequestMapping("/user")
@SentinelResource(value = USER_RESOURCE_NAME, blockHandlerClass = User.class, blockHandler = "blockHandlerForGetUser")
public User getUser(String id) {
return new User("hudu");
}
}
4、pojo 类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String username;
/**
* 注意
* 1、一定要 public
* 2、返回值一定要和原方法保持一致,包含原方法的参数
* 3、可以在参数最后添加 BlockException 可以区分是什么规则的处理方法
*/
public static User blockHandlerForGetUser(String id, BlockException ex) {
ex.printStackTrace();
return new User("流控!");
}
}
当访问频繁时,被流控了
fallback 测试
/**
* @SentinelResource 改善接口中资源定义和被流控降级后的处理方法
* 怎么使用
* 1、添加依赖
* 2、配置Bean SentinelResourceAspect
* value 定义资源
* blockHandler 设置流控降级后的处理方法(默认该方法必须设置在一个同一个类中)
* 如果不想在同一个类中,可以设置 blockHandlerClass,并且方法必须是静态方法
* fallback 当接口出现异常,就可以交给 fallback 指定的方法进行处理
*
* blockHandler 和 fallback 同时指定了,则 blockHandler 优先级更高
*
* 可以通过 exceptionsToIgnore 排除不处理的异常
*/
@RequestMapping("/user")
@SentinelResource(value = USER_RESOURCE_NAME,
blockHandlerClass = User.class, blockHandler = "blockHandlerForGetUser",
fallbackClass = User.class, fallback = "fallbackForGetUser",
exceptionsToIgnore = {ArithmeticException.class})
public User getUser(String id) {
int a = 1 / 0;
return new User("hudu");
}
pojo 类代码
public static User fallbackForGetUser(String id,Throwable e) {
e.printStackTrace();
return new User("异常处理");
}
再次请求接口,发现已经被异常处理的方法处理了
当我们刷新频率过高时,会发现接口被流控了,当 blockHandler 和 fallback 同时指定了,则 blockHandler 优先级更高。
三、Sentinel 降级体验
设置降级规则
@PostConstruct
public void initDegradeRule() {
// 降级规则
List<DegradeRule> degradeRules = new ArrayList<>();
DegradeRule degradeRule = new DegradeRule();
degradeRule.setResource(DEGRADE_RESOURCE_NAME);
// 设置降级规则策略,异常数
degradeRule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT);
// 设置阈值,异常数 2
degradeRule.setCount(2);
// 触发熔断的最小请求数量
degradeRule.setMinRequestAmount(2);
// 统计时长,单位 ms,默认 1000 一秒
degradeRule.setStatIntervalMs(60*1000);
// 一分钟内:执行了2次,出现了2次异常,就会触发熔断
// 熔断持续的时长(时间窗口)单位 s,
// 一旦触发了熔断,再次请求对应的接口就会直接调用 降级方法
// 熔断时长多了之后,进入半开状态,恢复接口请求调用,如果第一次请求就异常,再次熔断,不会根据设置的条件进行判定
degradeRule.setTimeWindow(10);
degradeRules.add(degradeRule);
DegradeRuleManager.loadRules(degradeRules);
}
@RequestMapping("/degrade")
@SentinelResource(value = DEGRADE_RESOURCE_NAME,entryType = EntryType.IN,blockHandlerClass = User.class,blockHandler = "blockHandlerForFb")
public User degrade(String id) {
// 异常数
throw new RuntimeException("异常");
}
public static User blockHandlerForFb(String id, BlockException ex) {
return new User("降级");
}
请求http://localhost:8060/degrade
接口第四次,发现被降级
在等十秒,可以请求,但是异常之后直接熔断降级。
流控规则一般会设置在服务提供方,熔断降级规则一般设置在服务消费端
本作品采用《CC 协议》,转载必须注明作者和本文链接