一、分布式系统遇到的问题
1 服务雪崩效应
在分布式系统中,由于网络原因或自身的原因,服务一般无法保证 100%是可用的。如果一个服务出现了问题,调用这个服务就会出现线程阻塞的情况,此时若有大量的请求涌入,就会出现多条线程阻塞等待,进而导致调用服务瘫痪。
由于服务与服务之间的依赖性,故障会进行传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的 雪崩效应。
雪崩发生的原因有可能,最常用的原因:程序Bug,大流量请求,硬件故障,缓存击穿。等等
- 程序Bug: 比如说代码导致的死循环,这里的问题比较多,反正只要把服务拖死的都是 BUG。
- 大流量请求:在秒杀和大促开始前,如果准备不充分,瞬间大量请求会造成服务提供者的不可用。
- 硬件故障:可能为硬件损坏造成的服务器主机宕机, 网络硬件故障造成的服务提供者的不可访问。
- 缓存击穿:一般发生在缓存应用重启, 缓存失效时高并发,所有缓存被清空时,以及短时间内大量缓存失效时。大量的缓存不命中, 使请求直击后端,造成服务提供者超负荷运行,引起服务不可用。
我们无法完全杜绝雪崩源头的发生,只有做好足够的容错,保证在一个服务发生问 题,不会影响到其它服务的正常运行。也就是"雪落而不雪崩"。
二、常见容错方案
要防止雪崩的扩展,我们就要做好 服务的容错,容错说白了就是 保护自己 不被猪队友拖垮的一些措施,下面介绍下常见的服务容错思路和组件:
1、常见的容错思路
常见的容错思路有超时、限流、熔断、降级这几种,下面分别介绍一下。
-
超时:在上游服务调用下游服务的时候,上游服务设置一个最大响应时间,如果超过这个时间,下游未作出反应,上游服务就断开请求,释放掉线程。
-
限制请求核心服务提供者的流量,使大流量拦截在核心服务之外,这样可以更好的保证核心服务提供者不出问题,对于一些出问题的服务可以限制流量访问。
- 计数器固定窗口算法
- 计数器滑动窗口算法
- 漏桶算法
- 令牌桶算法
-
在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。
这种牺牲局部,保全整体的措施就叫做熔断。
服务熔断一般有三种状态:
-
熔断关闭状态(Closed)
服务没有故障时,熔断器所处的状态,对调用方的调用不做任何限制
-
熔断开启状态(Open)
后续对该服务接口的调用不再经给网络请求,直接执行本地的 fallback 方法。
-
半熔断状态(Half-Open)
尝试恢复服务调用,允许有限的流量调用该服务,并监控调用的成功率。如果成功率达到预期,说明服务可以恢复。进入熔断关闭状态;如果成功率仍旧很低,则重新进入熔断关闭状态。
【现在世界中的断路器不知道大家是否了解;断路器实时监控电路的情况,如果发现电路电流异常,就会跳闸。从而防止电路被烧毁。
软件世界的断路器可以这样理解:实时监测应用,如果发现在一定时间内失败次数 / 失败率达到一定阈值,就“跳闸”,断路器打开 ----- 此时,请求直接返回,而不去调用原本调用的逻辑。跳闸 一段时间后(比如10秒),断路器会进入 半开状态,这是一个瞬间的状态。此时允许一次请求调用其他服务,如果成功,则断路器关闭,应用正常调用;如果调用依然不成功,断路器继续回到 打开状态,过一段时间 再进行反复上面的重试。应用可以保护自己,而且避免浪费资源;而通过半开的设计,可实现应用的 “自我修复“。
所以,当依赖的服务有大量超时时,再让新的请求去访问根本没有意义,只会无畏的消耗现有的资源。比如我们设置了超时时间为 (10秒),如果短时间内大量的请求在 超时时间内 没有得到响应,就意味着这个服务出现了问题,此时就没有必要让其他的请求去进行访问了,这个时候我们就应该使用断路器避免资源的浪费。】
下面是具体的流程图
-
-
降级
所谓降级就是我们调用的服务异常超时等原因不能正常返回的情况下,我们返回一个缺省的值。【由于降级经常和熔断一起使用,所以就会有熔断降级的说法。】
2、常见的容错思路
-
Hystrix
Hystrix是由Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止 级联失败,从而提升系统的可用性与容错性。
-
Resilience4J
Resilicence4J一款非常轻量、简单,并且文档非常清晰、丰富的熔断工具,这也是Hystrix官方推 荐的替代产品。不仅如此,Resilicence4j还原生支持Spring Boot 1.x/2.x,而且监控也支持和prometheus等多款主流产品进行整合。
-
Sentinel
Sentinel 是阿里巴巴开源的一款断路器实现,本身在阿里内部已经被大规模采用,非常稳定。
下面是三个组件在各方面的对比:
Sentinel | Hystrix | resilience4j | |
---|---|---|---|
隔离策略 | 信号量隔离(并发线程数限流) | 线程池隔离/信号量隔离 | 信号量隔离 |
熔断降级策略 | 基于慢调用比例、异常比例、异常数 | 基于异常比例 | 基于异常比例、响应时间 |
实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口(基于 RxJava) | Ring Bit Buffer |
动态规则配置 | 支持近十种动态数据源 | 支持多种数据源 | 有限支持 |
扩展性 | 多个扩展点 | 插件的形式 | 接口的形式 |
基于注解的支持 | 支持 | 支持 | 支持 |
单机限流 | 基于 QPS,支持基于调用关系的限流 | 有限的支持 | Rate Limiter |
集群流控 | 支持 | 不支持 | 不支持 |
流量整形 | 支持预热模式与匀速排队控制效果 | 不支持 | 简单的 Rate Limiter 模式 |
系统自适应保护 | 支持 | 不支持 | 不支持 |
热点识别/防护 | 支持 | 不支持 | 不支持 |
多语言支持 | Java/Go/C++ | Java | Java |
Service Mesh 支持 | 支持 Envoy/Istio | 不支持 | 加粗样式不支持 |
控制台 | 提供开箱即用的控制台,可配置规则、实时监控、机器发现等 | 简单的监控查看 | 不提供控制台,可对接其它监控系统 |
三、sentinel基本操作
1.什么是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等应用容器。
2、基本概念
-
资源
资源就是Sentinel要保护的东西
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。
-
规则
规则就是用来定义如何进行保护资源的
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整
3、Sentinel 功能和设计理念
Sentinel的主要功能就是容错,主要体现为下面这三个:
3.1 流量控制(上游)
流量控制在网络传输中是一个常用的概念,它用于调整网络包的数据。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。
Sentinel作为一个调配器,可以根据需要把随机的请求调整成合适的形状。
3.2 熔断降级(下游)
当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则对这个资源的调用进行限制,让请求快速失败(或者其他处理方式),避免影响到其它的资源而导致级联故障。
四、整合sentinel
在官方文档中,定义的Sentinel进行资源保护的几个步骤:
- 定义资源
- 定义规则
- 检验规则是否生效
抛出异常的方式定义资源:
Entry entry = null;
// 务必保证 finally 会被执行
try {
// 资源名可使用任意有业务语义的字符串,注意数目不能太多(超过 1K),超出几千请作为参数传入而不要直接作为资源名
// EntryType 代表流量类型(inbound/outbound),其中系统规则只对 IN 类型的埋点生效
entry = SphU.entry("自定义资源名");
// 被保护的业务逻辑
// do something...
} catch (BlockException ex) {
// 资源访问阻止,被限流或被降级
// 进行相应的处理操作
} catch (Exception ex) {
// 若需要配置降级规则,需要通过这种方式记录业务异常
Tracer.traceEntry(ex, entry);
} finally {
// 务必保证 exit,务必保证每个 entry 与 exit 配对
if (entry != null) {
entry.exit();
}
}
4.1 基于api方式的实现
1.添加项目依赖
<!-- 引入 sentinel 直接引入 我们不用指定版本 根据父级项目的 cloudAlibab的依赖走 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
</dependency>
2.添加 CommonFlowRules 类 在里面添加限流规则
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Component
public class CommonFlowRules {
/**
* 配置 api形式的 资源
*/
public static final String API_RESOURCE_NAME = "API-SOURCE";
static {
log.info("加载配置信息---------------------------");
//定义规则列表
List<FlowRule> rules = new ArrayList<>();
//定义 api资源的 限流
FlowRule apiRule = new FlowRule();
//定义受保护的资源
apiRule.setResource(API_RESOURCE_NAME);
//设置流控规则 QPS
apiRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
//设置受保护的资源阈值
apiRule.setCount(1);
rules.add(apiRule);
//将规则加载到 规则列表当中
FlowRuleManager.loadRules(rules);
}
}
3.添加 SentinelApiController 编写代码
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.Tracer;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.example.cloudsentinel.common.CommonFlowRules;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
public class SentinelApiController {
@GetMapping("getInfo")
public String getInfo() {
Entry entry = null;
// 务必保证 finally 会被执行
try {
// 资源名可使用任意有业务语义的字符串,注意数目不能太多(超过 1K),超出几千请作为参数传入而不要直接作为资源名
// EntryType 代表流量类型(inbound/outbound),其中系统规则只对 IN 类型的埋点生效
entry = SphU.entry(CommonFlowRules.API_RESOURCE_NAME);
// 被保护的业务逻辑
String str = "业务逻辑正常处理";
log.info("=====" + str + "=====");
return str;
} catch (BlockException ex) {
// 资源访问阻止,被限流或被降级
// 进行相应的处理操作
log.info("Block...!");
return "业务被限流了!";
} catch (Exception ex) {
// 若需要配置降级规则,需要通过这种方式记录业务异常
Tracer.traceEntry(ex, entry);
} finally {
// 务必保证 exit,务必保证每个 entry 与 exit 配对
if (entry != null) {
entry.exit();
}
}
return null;
}
}
缺点:
- 业务侵入性很强,需要在controller中写入非业务代码.
- 配置不灵活 若需要添加新的受保护资源 需要手动添加 init方法来添加流控规则
4.2 基于注解的方式来定义
@SentinelResource
注解实现
在定义了资源点之后,我们可以通过Dashboard来设置限流和降级策略来对资源点进行保护。同时还能 通过@SentinelResource来指定出现异常时的处理策略。
@SentinelResource用于定义资源,并提供可选的异常处理和fallback 配置项。其主要参数如下:
属性 | 作用 |
---|---|
value | 资源名称 |
entryType | entry类型,标记流量的方向,取值IN/OUT,默认是OUT |
blockHandler | 处理BlockException的函数名称,函数要求:<br>1.必须是public<br/>2.返回类型 参数与原方法一致<br/>3.默认需和原方法在同一个类中。若希望使用其他类的函数,可配置blockHandlerClass,并指定blockHandlerClass里面的方法。 |
blockHandlerClass | 存放blockHandler的类,对应的处理函数必须static修饰。 |
fallback | 用于在抛出异常的时候提供fallback处理逻辑。fallback函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。函数要求:<br/>1.返回类型与原方法一致<br/>2.参数类型需要和原方法相匹配<br/>3.默认需和原方法在同一个类中。若希望使用其他类的函数,可配置fallbackClass ,并指定fallbackClass里面的方法。 |
fallbackClass | 存放fallback的类。对应的处理函数必须static修饰。 |
defaultFallback | 用于通用的 fallback 逻辑。默认fallback函数可以针对所有类型的异常进行处理。若同时配置了 fallback 和 defaultFallback,以fallback为准。函数要求:<br/>1.返回类型与原方法一致<br/>2.方法参数列表为空,或者有一个Throwable类型的参数。<br/>3.默认需要和原方法在同一个类中。若希望使用其他类的函数,可配置fallbackClass ,并指定 fallbackClass 里面的方法。 |
exceptionsToIgnore | 指定排除掉哪些异常。排除的异常不会计入异常统计,也不会进入fallback逻辑,而是原样抛出。 |
exceptionsToTrace | 需要trace的异常 |
1.添加项目依赖:
<!-- 注解的形式引入 sentinel 我们不用指定版本 根据父级项目的 cloudAlibab的依赖走 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
</dependency>
2.添加SentinelAspectConfiguration 声明注解
import com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 配置 切面(注解) 的支持
*/
@Configuration
public class SentinelAspectConfiguration {
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
}
3.在 CommonFlowRules 添加 注解的限流配置
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Component
public class CommonFlowRules {
/**
* 配置注解的 资源
*/
public static final String ANNOTATION_RESOURCE_NAME = "testAspect";
/**
* 配置 api形式的 资源
*/
public static final String API_RESOURCE_NAME = "API-SOURCE";
static {
log.info("加载配置信息---------------------------");
//定义规则列表
List<FlowRule> rules = new ArrayList<>();
//定义 基于注解的配置
FlowRule rule = new FlowRule();
//定义受保护的资源
rule.setResource(ANNOTATION_RESOURCE_NAME);
//设置流控规则 QPS
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
//设置受保护的资源阈值
rule.setCount(1);
rules.add(rule);
//定义 api资源的 限流
FlowRule apiRule = new FlowRule();
//定义受保护的资源
apiRule.setResource(API_RESOURCE_NAME);
//设置流控规则 QPS
apiRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
//设置受保护的资源阈值
apiRule.setCount(1);
rules.add(apiRule);
//将规则加载到 规则列表当中
FlowRuleManager.loadRules(rules);
}
}
4.添加SentinelAnnotationController 编写代码
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.example.cloudsentinel.common.CommonException;
import com.example.cloudsentinel.common.CommonFlowRules;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
public class SentinelAnnotationController {
@GetMapping("/testAspect/{id}")
@SentinelResource(value = CommonFlowRules.ANNOTATION_RESOURCE_NAME,
fallback = "fallback", fallbackClass = CommonException.class,
blockHandler = "handleException", blockHandlerClass = CommonException.class
)
public String testAspect(@PathVariable("id") Integer id) {
if (Integer.compare(id, Integer.parseInt("0")) == -1) {
throw new IllegalArgumentException("参数异常");
}
log.info("处理业务信息");
return "测试注解方式限流正常";
}
}
上面这两种方式,我们是基于代码的方式来控制业务的具体限流规则的,sentinel还支持页面的方式配置每个资源的限流规则,我们整合到我们的 SpringCloudAlibaba里面看一下:
4.3 图形化的方式整合
整合SpringBoot和SpringCloud官方文档传送地址
- 如果要整合 sentinel图形化的话,代码中不可以出现自定义规则,如下面这个代码
// 当前代码会把 项目初始化时候 cloud-startet 里面的配置 给冲掉 会把旧的给 替换掉
FlowRuleManager.loadRules(rules);
不然你的服务,就永远注册不到 sentinel的控制台上。
1. 添加项目依赖
<!-- 引入支持图形化方式的依赖 我们不用指定版本 根据父级项目的 cloudAlibaba的依赖走 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
2.配置文件添加sentinel地址
spring:
application:
name: sentinel-service
cloud:
nacos:
discovery:
# 注册到 nacos的地址
server-addr: localhost:8848
config:
# 配置中心的地址
server-addr: localhost:8848
file-extension: yaml
namespace: 89b87df8-4e58-4363-a11a-ff1863b6f5fb
group: DEFAULT_GROUP
# sentinel的配置
sentinel:
transport:
port: 7810 #跟控制台交流的端口,随意指定一个未使用的端口即可
dashboard: localhost:8080 # 指定控制台服务的地址
3.引入控制台
Sentinel 提供一个轻量级的控制台, 它提供机器发现、单机资源实时监控以及规则管理等功能。
第1步 下载jar包,解压到文件夹
Sentinel控制台下载地址
第2步 启动控制台
直接使用jar命令启动项目(控制台本身是一个SpringBoot项目)
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.6.jar
把dashboard自己也当成一个资源加入到了dashboard中来进行监控,如果不想把dashboard自己加入控制台监控可以使用简单启动指令如下:
java -Dserver.port=8080 -jar sentinel-dashboard-1.8.6.jar
第4步: 访问控制台
用户可以通过如下参数进行配置:
-Dsentinel.dashboard.auth.username=sentinel 用于指定控制台的登录用户名为 sentinel;
-Dsentinel.dashboard.auth.password=123456 用于指定控制台的登录密码为 123456;如果省略这两个参数,默认用户和密码均为 sentinel;
-Dserver.servlet.session.timeout=7200 用于指定 Spring Boot 服务端 session 的过期时间,如 7200 表示 7200 秒;60m 表示 60 分钟,默认为 30 分钟;
访问http://localhost:8080/#/login ,默认用户名密码: sentinel/sentinel
Sentinel 会在客户端首次调用的时候进行初始化,开始向控制台发送心跳包,所以要确保客户端有访问量;
补充:了解控制台的使用原理
Sentinel的控制台其实就是一个SpringBoot编写的程序。我们需要将我们的微服务程序注册到控制台上, 即在微服务中指定控制台的地址, 并且还要开启一个跟控制台传递数据的端口, 控制台也可以通过此端口调用微服务中的监控程序获取微服务的各种信息。