副本介绍
分布式系统中,我们往往会遇到一些服务可用性问题?出现这些问题的原因是什么?我们又应该如何解决这些问题呢?
第一关:分布式系统中遇到的问题
服务的可用性问题
4个9 5个9 x个9。
x代表了3-5。 代表系统一年使用过程中,系统可以正常使用时间与总时间(1年)之比。
例如 4个9. (1-99.99%)x365x24=0.876小时=52.6分钟,表示该系统在连续运行1年时间里最多可能的业务中断时间是52.6分钟。
服务雪崩效应: 因服务提供者的不可用导致服务调用者的不可用,并将不可用逐渐放大的过程,就叫服务雪崩效应。
导致服务不可用的原因:
Reliability && Resilience
常见的容错机制:
-
超时机制
在不做任何处理的情况下,服务提供者不可用会导致消费者请求线程强制等待,而造成系统资源耗尽。加入超时机制,一旦超时,就释放资源。由于释放资源速度较快,一定程度上可以抑制资源耗尽的问题。
-
服务限流
-
隔离
用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,则会进行降级处理,用户的请求不会被阻塞,至少可以看到一个执行结果(例如返回友好的提示信息),而不是无休止的等待或者看到系统崩溃。
第二关:Sentinel 介绍
Sentinel 是什么?
Sentinel 是阿里开源的,面向分布式服务架构的高可用防护组件。提供了多维度的流控降级能力,秒级实时监控与动态规则管理。主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。
Sentinel 具有以下特征:
- 丰富的应用场景: Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、实时熔断下游不可用应用等。
- 完备的实时监控: Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态: Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
- 完善的 SPI 扩展点: Sentinel 提供简单易用、完善的 SPI 扩展点。您可以通过实现扩展点,快速的定制逻辑。例如定制规则管理、适配数据源等。
Sentinel 分为两部分:
- 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
- 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
Sentinel 和 Hystrix对比
第三关:Sentinel - 实践<原生Spring>
1. 补充pom文件
<!--sentinel核心库-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.0</version>
</dependency>
<!--如果要使用@SentinelResource-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
<version>1.8.0</version>
</dependency>
<!--整合控制台-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.8.0</version>
</dependency>
2. 启动类中配置bean - SentinelResourceAspect
如果是通过 SpringCloud Alibaba 接入的 Sentinel,则无需进行额外配置,就可以使用 @SentinelResource 注解
若使用的是Spring AOP ( 无论是 SpringBoot 还是 传统的 Spring 应用 ),则需要配置下面的bean
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
3. 自定义降级控流、熔断的方法
/**
* 定义规则
*
* spring 的初始化方法
*/
@PostConstruct //该注解的接口会在spring启动的时候,自动执行
private 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 20.
rule.setCount(1);
rules.add(rule);
// 通过@SentinelResource来定义资源并配置降级和流控的处理方法
FlowRule rule2 = new FlowRule();
//设置受保护的资源
rule2.setResource(USER_RESOURCE_NAME);
// 设置流控规则 QPS
rule2.setGrade(RuleConstant.FLOW_GRADE_QPS);
// 设置受保护的资源阈值
// Set limit QPS to 20.
rule2.setCount(1);
rules.add(rule2);
// 加载配置好的规则
FlowRuleManager.loadRules(rules);
}
@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);
// 触发熔断最小请求数:2
degradeRule.setMinRequestAmount(2);
// 统计时长: 单位:ms 1分钟
degradeRule.setStatIntervalMs(60*1000); // 时间太短不好测
// 一分钟内: 执行了2次以上 出现了2次异常 就会触发熔断
// 熔断持续时长 : 单位 秒
// 一旦触发了熔断, 再次请求对应的接口就会直接调用 降级方法。
// 10秒过了后——半开状态: 恢复接口请求调用, 如果第一次请求就异常, 再次熔断,不会根据设置的条件进行判定
degradeRule.setTimeWindow(10);
degradeRules.add(degradeRule);
DegradeRuleManager.loadRules(degradeRules);
/*
慢调用比率--DEGRADE_GRADE_RT
degradeRule.setGrade(RuleConstant.DEGRADE_GRADE_RT);
degradeRule.setCount(100);
degradeRule.setTimeWindow(10);
//请求总数小于minRequestAmount时不做熔断处理
degradeRule.setMinRequestAmount(2);
// 在这个时间段内2次请求
degradeRule.setStatIntervalMs(60*1000*60); // 时间太短不好测
// 慢请求率:慢请求数/总请求数> SlowRatioThreshold ,
// 这里要设置小于1 因为慢请求数/总请求数 永远不会大于1
degradeRule.setSlowRatioThreshold(0.9);*/
}
4. 给对应接口添加 @SentinelResource 注解
private static final String USER_RESOURCE_NAME = "user";
/**
* @SentinelResource 改善接口中资源定义和被流控降级后的处理方法
* 怎么使用: 1.添加依赖<artifactId>sentinel-annotation-aspectj</artifactId>
* 2.配置bean——SentinelResourceAspect
* value 定义资源
* blockHandler 设置 流控降级后的处理方法(默认该方法必须声明在同一个类)
* 如果不想在同一个类中 添加blockHandlerClass指定对应的类 但是方法必须是static
* fallback 当接口出现了异常,就可以交给fallback指定的方法进行处理
* 如果不想在同一个类中 添加fallbackClass指定对应的类 但是方法必须是static
* blockHandler 如果和fallback同时指定了,则blockHandler优先级更高
*
* exceptionsToIgnore 排除哪些异常不处理
* @param id
* @return
*/
@RequestMapping("/user")
@SentinelResource(value = USER_RESOURCE_NAME, fallback = "fallbackHandleForGetUser",
/*exceptionsToIgnore = {ArithmeticException.class},*/
/*blockHandlerClass = User.class,*/ blockHandler = "blockHandlerForGetUser")
public User getUser(String id) {
int a=1/0;
return new User("成功");
}
public User fallbackHandleForGetUser(String id,Throwable e) {
e.printStackTrace();
return new User("异常处理");
}
/**
* 注意:
* 1. 一定要public
* 2. 返回值一定要和源方法保证一致, 包含源方法的参数。
* 3. 可以在参数最后添加BlockException 可以区分是什么规则的处理方法
* @param id
* @param ex
* @return
*/
public User blockHandlerForGetUser(String id, BlockException ex) {
ex.printStackTrace();
return new User("流控!!");
}
第四关:Sentinel - 整合SpringCloud Alibaba
-
补充pom文件
<!--sentinel启动器--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
-
补充配置文件
这样我们请求了该服务的接口后,sentinel控制台就会有记录了
server: port: 8861 spring: application: name: order-sentinel cloud: sentinel: transport: dashboard: 192.168.2.101:8858 web-context-unify: false # 默认将调用链路收敛, 导致链路流控效果无效
请求接口被流控就会被sentinel默认的方法展示。想要自定义流控方法的返回,跟Sentinel整合原生Spring 一样, 通过加注解@SentinelResource(value = "xx", blockHandler = "流控方法名"),自定义流控方法来实现。
除此之外,当我们多个方法的流控、降级、熔断方法返回值都一样,就可以编写自定义拦截器进行统一处理
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.alibaba.csp.sentinel.slots.system.SystemBlockException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tulingxueyuan.order.domain.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {
Logger log = LoggerFactory.getLogger(this.getClass());
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse response, BlockException e) throws Exception {
// getRule() 资源 规则的详细信息
log.info("BlockExceptionHandler BlockException================"+e.getRule());
Result r = null;
if (e instanceof FlowException) {
r = Result.error(100,"接口限流了");
} else if (e instanceof DegradeException) {
r = Result.error(101,"服务降级了");
} else if (e instanceof ParamFlowException) {
r = Result.error(102,"热点参数限流了");
} else if (e instanceof SystemBlockException) {
r = Result.error(103,"触发系统保护规则了");
} else if (e instanceof AuthorityException) {
r = Result.error(104,"授权规则不通过");
}
//返回json数据
response.setStatus(500);
response.setCharacterEncoding("utf-8");
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getWriter(), r);
}
}
第五关:Sentinel-Dashboard 安装
Sentinel 控制台: https://github.com/alibaba/Sentinel/wiki/%E6%8E%A7%E5%88%B6%E5%8F%B0
docker安装控制台
搜索、下载容器
[root@192 ~]# docker search sentinel
[root@192 ~]# docker pull bladex/sentinel-dashboard
启动容器
docker run --name sentinel-dashboard -d -p 8858:8858 bladex/sentinel-dashboard
客户端接入控制台
引入pom文件
客户端需要引入 Transport 模块来与 Sentinel 控制台进行通信。您可以通过 `pom.xml` 引入 JAR 包
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>x.y.z</version>
</dependency>
配置启动参数 ( 原生spring )
启动时加入 JVM 参数 `-Dcsp.sentinel.dashboard.server=192.168.2.101:8858` 指定控制台地址和端口。若启动多个应用,则需要通过 `-Dcsp.sentinel.api.port=xxxx` 指定客户端监控 API 的端口(默认是 8719)。
从 1.6.3 版本开始,控制台支持网关流控规则管理。您需要在接入端添加 `-Dcsp.sentinel.app.type=1` 启动参数以将您的服务标记为 API Gateway,在接入控制台时您的服务会自动注册为网关类型,然后您即可在控制台配置网关规则和 API 分组。
除了修改 JVM 参数,也可以通过配置文件取得同样的效果。更详细的信息可以参考 启动配置项。
第六关:流控规则 < 流控通常在服务端 >
流控模式
直接流控 关联流控 链路流控
1、直接流控
给谁设置流控,达到要求,谁就被限流。
2、关联流控
给接口A设置流控,关联资源是接口B。 这时候,当请求B达到一定要求,A就会被限流。
3、链路流控
接口A和接口B都调用了接口C。我们对C设置链路流控,入口资源是接口A。这时候,当接口A调用达到一定要求,接口A请求C就被流控了。但是接口B请求C不管怎么请求,都是可以调通的。
// 接口C
@SentinelResource(value="getUser",blockHandler = "blockHandlerGetUser")
public String getUser() {
return "查询用户";
}
// 关联流控 访问 触发/getUser
@RequestMapping("/test1")
public String test1(){
return orderService.getUser();
}
// 关联流控 访问/add 触发/get
@RequestMapping("/test2")
public String test2() throws InterruptedException {
return orderService.getUser();
}
测试会发现链路规则不生效
注意,高版本此功能直接使用不生效,如何解决?
从1.6.3 版本开始,Sentinel Web filter默认收敛所有URL的入口context,因此链路限流不生效。
1.7.0 版本开始(对应SCA的2.1.1.RELEASE),官方在CommonFilter 引入了WEB_CONTEXT_UNIFY 参数,用于控制是否收敛context。将其配置为 false 即可根据不同的URL 进行链路限流。SCA 2.1.1.RELEASE之后的版本,可以通过配置spring.cloud.sentinel.web-context-unify=false即可关闭收敛
server:
port: 8861
spring:
application:
name: order-sentinel
cloud:
sentinel:
transport:
dashboard: 192.168.2.101:8858
web-context-unify: false # 默认将调用链路收敛, 导致链路流控效果无效
流控效果
快速失败 Warm Up 排队等待
1、快速失败
超出要求,直接失败,走流控方法 ( block )
官方解释: ( RuleConstant.CONTROL_BEHAVIOR_DEFAULT )方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。
2、Warm Up
适用于 洪峰流量 ,预热模式,一点一点给释放
官方解释: Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。
冷加载因子: codeFactor 默认是3,即请求 QPS 从 threshold / 3 开始,经预热时长逐渐升至设定的 QPS 阈值。
3、排队等待
适用于 脉冲流量 ,雪峰填谷的意思
官方解释: 匀速排队(
RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER
)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。注意: 匀速排队模式暂时不支QPS > 1000 的请求
第七关:熔断降级与隔离 < 通常在消费端 >
降级规则
除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。我们需要对不稳定的 **弱依赖服务调用** 进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。
熔断策略
慢调用比例
设置最大响应时间,超过了就是慢调用。 超过慢调用比例就会被熔断降级。熔断之后第一次调用如果还是慢调用,则直接再次熔断,不必判断慢调用比例。
官方解释: 慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALFOPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
异常比例
在设定的**最小请求**的基础上,出现异常的概率**大于异常比例**,之后的请求则会熔断降级。熔断之后第一次调用如果还是异常调用,则直接再次熔断,不必判断异常比例。
官方解释: 异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALFOPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% 100%。
![异常比例熔断](https://upload-images.jianshu.io/upload_images/20818477-053fcc19e245f8d1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
异常数
在设定的最小请求的基础上,出现异常次数大于异常数,之后的请求则会熔断降级。熔断之后第一次调用如果还是异常调用,则直接再次熔断,不必判断异常数。
官方解释: 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断会进入探测恢复状态(HALFOPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。注意:异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(BlockException)不生效。
![异常数熔断](https://upload-images.jianshu.io/upload_images/20818477-5644c71d6eea12cf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
第八关:openFeign跟Sentinel的整合
1. pom文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos-服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--1. 添加openfeign依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--sentinel依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
2.yaml配置文件
开启openFeign对sentinel的整合. Feign.sentinel.enabled = true
server:
port: 8041
# 应用名称 (nacos会将该名称当做服务名称)
spring:
application:
name: order-service
cloud:
nacos:
server-addr: 192.168.2.101:8847
discovery:
username: nacos
password: nacos
namespace: public
feign:
sentinel:
# openfeign整合sentinel
enabled: true
3.feign接口补充实现类,进行降级方法的补充
// feign 接口 指定降级类
@FeignClient(value="stock-nacos",path = "/stock",fallback = StockFeignServiceFallback.class)
public interface StockFeignService {
@RequestMapping("/reduct")
public String reduct2();
}
// feign的fallback实现类,实现降级方法
@Component
public class StockFeignServiceFallback implements StockFeignService {
@Override
public String reduct2() {
return "降级啦!!!";
}
}
第九关:热点参数流控
何为热点?
热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的数据,并对其访问进行限制。热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
注意:
热点规则需要使用@SentinelResource("resourceName")注解,否则不生效
参数必须是7种基本数据类型才会生效
单机阈值: 针对所有参数的值进行设置的一个公共的阈值
假设当前 参数 大部分的值都是热点流量, 单机阈值就是针对热点流量进行设置, 额外针对普通流量进行参数值流控
假设当前 参数 大部分的值都是普通流量, 单机阈值就是针对普通流量进行设置, 额外针对热点流量进行参数值流控
第十关:Sentinel规则持久化
问题
上述方法,在服务重启之后,流控规则就被清空了,没有数据持久化。
原始模式
如果不做任何修改,Dashboard 的推送规则方式是通过 API 将规则推送至客户端并直接更新到内存中。这种做法的好处是简单,无依赖;坏处是应用重启规则就会消失,仅用于简单测试,不能用于生产环境。
拉模式( Pull )
pull 模式的数据源(如本地文件、RDBMS 等)一般是可写入的。使用时需要在客户端注册数据源:将对应的读数据源注册至对应的 RuleManager,将写数据源注册至 transport 的WritableDataSourceRegistry 中。
推模式 ( Push )
生产环境下一般更常用的是 push 模式的数据源。对于 push 模式的数据源,如远程配置中心(ZooKeeper, Nacos, Apollo等等),推送的操作不应由 Sentinel 客户端进行,而应该经控制台统一进行管理,直接进行推送,数据源仅负责获取配置中心推送的配置并更新到本地。因此推送规则正确做法应该是 配置中心控制台 /Sentinel 控制台 → 配置中心 → Sentinel 数据源 → Sentinel,而不是经 Sentinel 数据源推送至配置中心。
持久化方法一:嵌入到代码里
像 第三关 Sentinel 集成 原生Spring 一样,把流控、熔断降级的规则写在代码里。
持久化方法二:Nacos配置中心实现推送
1、nacos配置中心中配置流控规则
[
{
"resource": "TestResource",
"controlBehavior": 0,
"count": 10.0,
"grade": 1,
"limitApp": "default",
"strategy": 0
}
]
2、补充yml配置
server:
port: 8861
spring:
application:
name: order-sentinel
cloud:
sentinel:
transport:
dashboard: 192.168.2.101:8858
web-context-unify: false # 默认将调用链路收敛, 导致链路流控效果无效
datasource:
flow-rule:
nacos:
server-addr: 192.168.2.101:8847
username: nacos
password: nacos
dataId: order-sentinel-flow-rule
groupId: SENTINEL_GROUP # 默认的不用配置
data‐type: yaml
rule-type: flow
问题:
nacos 配置这一套。如果从 sentinel-dashboard 设置流控、降级等操作,是不会同步到nacos配置文件里的,还需要手动更改nacos里的配置。
至于可以怎么解决? 如果在控制台设置了流控,nacos自动同步,还需要研究源码,查看方法。 但是确定是可以实现的。