常见容错方案

在微服务等分布式架构中,服务容错是老生常谈的问题了,我们都知道在微服务架构中会存在多个微服务,而绝大部分微服务之间都会存在调用关系,若由于某个底层服务不可用从而产生连锁反应,导致一系列的上层服务崩溃、故障,这种现象被称为雪崩效应或级联故障。如下图所示:

所以在微服务等分布式架构中,能够防御服务雪崩效应的容错方案是必不可少的,常见的容错方案如下:

1、超时:

设置请求超时时间,让请求线程在等待超过一定的时间后就判定为请求失败从而释放请求线程,在某些场景下线程释放得够快的话,就不会因为不断创建线程而导致资源耗尽引起的服务崩溃

2、限流:

例如,上图中的服务A只能承受1k左右的QPS,那么就设置一个最大请求数量阈值,当QPS达到1k时就拒绝在这之后的请求。就像是我只能吃一碗饭,就算给我三碗我也只吃一碗

3、舱壁模式:

舱壁模式实际上就是借鉴于现实生活中的船舱结构而设计,一艘船想要不那么容易沉也需要具备有一定的”容错“能力,而早期的船由于设计上的欠缺,只要一个地方进水了,那么水就会逐渐漫进整个船舱,这种结构的船几乎没有“容错”能力,所以就比较容易沉。于是此时就有人想到将原本一体的船舱分隔成一个个独立的船舱,船舱之间都使用钢板焊死隔开,这些钢板就是所谓的舱壁了。采用这种设计后,就算当其中一个两个船舱进水了,也不会影响到其他船舱,这艘船依旧能够正常行驶。

在软件层面上借鉴这种思想,我们可以让每个服务都运行在自己独立的线程池中,线程池之间是互不干扰的,服务A的线程池资源耗尽也不会影响到服务B。此时线程池就像船舱的舱壁一样将不同的服务资源隔离开来,这样某个服务挂掉也不会影响其他服务的运行

4、断路器模式:

断路器模式的思想实际上和家里的断路器一样,在软件层面大致就是对某个服务的API进行监控,若在一定时间内调用的失败率或失败次数达到指定的阈值就认为该API是不可用的从而触发“跳闸”,即此时断路器就处于打开状态。过了一段时间后断路器会处于一个半开状态,若在半开状态时尝试调用该API成功后就会关闭断路器,否则依旧认为不可用让断路器继续处于打开状态

断路器三态转换如下图:

断路器模式原文:CircuitBreaker

而Spring Cloud已经提供了相关的服务容错组件,组件里已经整合了这些常用的方案,不需要我们手动去实现。在此之前Spring Cloud提供的唯一服务容错组件是Hystrix,不过现在多了一个选择,那就是Spring Cloud Alibaba的Sentinel组件。关于Hystrix可以参考如下文章,本文主要介绍Sentinel:

Spring Cloud Hystrix - 服务容错
Sentinel简介及整合

Sentinel 是什么,官方描述如下:

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式服务架构的轻量级流量控制、熔断降级框架,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助您保护服务的稳定性。

官方GitHub仓库地址如下:

https://github.com/alibaba/Sentinel

现在我们来为项目整合Sentinel,第一步添加如下依赖:

<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId></dependency><!-- actuator,用于暴露监控端点 --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId></dependency>

Tips:该项目使用的Spring Cloud版本为Greenwich.SR1,Spring Cloud Alibaba版本为0.9.0.RELEASE

第二步配置actuator:

# 暴露所有端点management: endpoints: web: exposure: include: '*'

完成以上两步后,启动项目,使用浏览器访问http://localhost:8080/actuator/sentinel,返回如下结果代表整合成功:


搭建Sentinel Dashboard控制台

在上一小节中,我们已经为项目成功整合了Sentinel,但这也只不过是完成了第一步。因为此时没有一个可视化的界面能让我们看到Sentinel具体的监控信息,所以还需要搭建官方提供的可视化Sentinel控制台,然后在控制台中整合项目的监控信息。

Sentinel控制台的下载地址如下:

https://github.com/alibaba/Sentinel/releases

Sentinel Dashboard有多个release版本,应该选择哪个呢?如果你是用在生产环境则选择与项目中sentinel-core版本对应的即可,如下:

若只是学习或测试使用那就可以随便选择了,只要能用就行,所以我这里选择最新版本1.6.3,注意这里选择jar包进行下载:

下载完成后,存放到一个你觉得ok的目录下,然后打开cmd,通过命令运行该jar包。如下:

E:\Spring Cloud Alibaba\Sentinel>java -jar sentinel-dashboard-1.6.3.jar

启动成功,监听的端口是8080:

使用浏览器访问http://localhost:8080进入到登录页面,默认的账户密码都是sentinel:

登录成功,此时控制台上是空白的,因为还没有监控任何的项目:

所以接着到项目中整合一下Sentinel Dashboard的请求地址,在配置文件中添加如下配置:

spring: cloud: sentinel: transport: # 配置sentinel控制台的地址 dashboard: 127.0.0.1:8080

配置完成启动项目后需要先访问一下该项目的接口,因为Sentinel Dashboard是懒加载的,只有监控的项目被访问后才会收集监控信息。这样才能看到下图的实时监控信息,我这里的服务名是content-center:


Sentinel 相关配置项小结

客户端(微服务)连接控制台相关配置项:

spring: cloud: sentinel: transport: #指定控制台的地址 dashboard: localhost:8080 #指定和控制台通信的IP #如不配置,会自动选择一个IP注册 client-ip: 127.0.0.1 #指定和控制台通信的端口,默认值8719 #如不设置,会自动从8719开始扫描,依次+1,直到找到未被占用的端口 port: 8719 #心跳发送周期,默认值null #但在S impleHttpHeartbeatSender会用默认值10秒 heartbeat- interval-ms : 10000

控制台相关配置项:

配置项默认值最小值描述sentinel.dashboard.app.hideAppNoMachineMillis060000是否隐藏无健康节点的应用,距离最近一次主机心跳时间的毫秒数,默认关闭sentinel.dashboard.removeAppNoMachineMillis0120000是否自动删除无健康节点的应用,距离最近一次其下节点的心跳时间毫秒数,默认关闭sentinel.dashboard.unhealthyMachineMillis6000030000主机失联判定,不可关闭sentinel.dashboard.autoRemoveMachineMillis0300000距离最近心跳时间超过指定时间是否自动删除失联节点,默认关闭server.port8080-指定端口csp.sentinel.dashboard.serverlocalhost:8080-指定地址project.name--指定程序的名称sentinel.dashboard.auth.username [1.6版本支持]sentinel-Sentinel Dashboard登录账号sentinel.dashboard.auth.password [1.6版本支持]sentinel-Sentinel Dashboard登录密码server.servlet.session.timeout [1.6版本支持]30分钟-登录Session过期时间。配置为7200表示7200秒;配置为60m表示60分钟

控制台配置项需在启动命令中指定,例如指定账户密码,如下:

java -jar -Dsentinel.dashboard.auth.username=admin -Dsentinel.dashboard.auth.password=123456 sentinel-dashboard-1.6.3.jar


流控规则

我们可以在Sentinel控制台中给某个接口添加流控规则,点击簇点链路,可以看到该服务曾经被访问过的路径:

然后点击接口右边的流控按钮就可以添加流控规则:

添加成功:

此时访问该服务的接口,QPS超过设定的阈值1,就会返回如下信息:


关于流控规则中的流控模式:

直接:当前资源的QPS达到设定的阈值,就触发限流关联:当关联的资源的QPS达到设定的阈值,就触发限流。例如,/shares/1关联了/query,那么/query达到阈值,就会对/shares/1限流链路:只记录指定链路上的流量,这种模式是针对接口级别的来源进行限流

链路模式稍微有些抽象,这里举个简单的例子说明一下。下图中有两个调用链路,图中的/test-b/test-a实际就是两个接口,它们都调用了同一个common资源,所以/test-b/test-a就称为common的入口资源:

此时我为common添加一个限流规则如下:

可以看到流控模式选择链路后,需要填写一个入口资源,我这里填的是/test-a,那么这意味着什么呢?意味着当/test-a的QPS达到该规则的阈值后,就会对/test-a限流,同时/test-b不会受到任何影响。说明这种流控模式可以针对接口级别的来源进行限流,而“针对来源”则是对微服务级别的来源进行限流。

关于流控规则中的监控效果:

快速失败:直接失败,抛出异常,不做任何额外的处理,是最简单的效果相关源码:com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultControllerWarm Up(预热):会根据codeFactor(默认3)的值,从阈值除以codeFactor,经过预热时长,才到达设置的QPS阈值。适用于将突然增大的流量转换为缓步增长的场景相关的官方文档:限流 - 冷启动相关源码:com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController排队等待:匀速排队,让请求以均匀的速度通过,若请求等待时间超过设置的超时时间则抛弃该请求,阈值类型必须设置成QPS,否则无效。适用于突发流量的场景相关的官方文档:限流 - 匀速器相关源码:com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController

关于流控的官方文档:

流量控制
降级规则

服务降级实际就是断路器模式的应用,相对于流控规则,降级规则要简单一些。降级规则可以在“簇点链路”或“降级规则”中添加:

例如,这里给/shares/1添加降级规则,降级策略先以RT为例:

该降级规则的含义如下图:

此时访问/shares/1接口,秒级平均响应时间超出阈值1,并且在时间窗口内通过的请求大于等于5,就会返回如下信息:

关于RT这种降级策略需要注意的点:

RT默认最大为4900ms,所以即便设置的值大于4900ms也依旧会按照4900ms计算可以通过参数修改:-Dcsp.sentinel.statistic.max.rt=xxx

若将降级策略改为异常比例,则含义如下:

若将降级策略改为异常数,则含义如下:

关于异常数这种降级策略需要注意的点:

若将时间窗口的值设置小于60秒则可能会出问题,因为异常数的统计是分钟级别的,时间窗口小于60秒就有可能不断进入降级状态

降级规则的相关源码:

com.alibaba.csp.sentinel.slots.block.degradeDegradeRule#passCheck(对降级的判断都在这个方法里完成)

在文章的开头我们介绍过断路器有三个状态,所以这里需要提及一下的是目前Sentinel的降级断路器是不支持半开状态的,只有打开和关闭两个状态,据官方人员描述说是会预计在未来添加半开的支持。

关于降级的官方文档:

熔断降级
热点规则

热点规则全称是热点参数限流规则,从名称可以得知,需要有参数的接口才能够使用热点规则。例如,有一个接口的代码如下:

@GetMapping("/test-hot")@SentinelResource("hot") // 该注解用于声明是Sentinel需要监控的资源public String testHot(@RequestParam(required = false) String a, @RequestParam(required = false) String b) { return a + " " + b;}

在控制台中为hot添加热点规则,如下:

Tips:参数索引从0开始,对应到代码中的话,则参数a的索引为0,参数b的索引为1,所以该规则是作用于参数a

添加完该规则后,此时访问这个接口,两个参数都传值,当QPS达到阈值时,就会抛出如下异常信息:

如果不传参数a,仅传参数b的话,则不会受到该规则的限流,如下:

说明该规则表达的含义是:在时间窗口内,一旦该规则指定的索引参数QPS达到了阈值,则会触发限流

除此之外,还有高级选项,在这里可以添加参数例外项,如下示例:

添加完成后,此时将参数a的值设置为5,然后频繁发送请求,会发现即便QPS超过1也不会触发限流:

这是因为参数a的值设置为5时,限流阈值是1000,设置为其他值时,限流阈值才是1。这就是所谓的参数例外项了,即参数的为某个特定的值时,只受参数例外项里的限流阈值影响。

热点规则适用的场景:

适用于存在热点参数并希望提升API可用性的场景,即某个特定请求参数QPS偏高于其他请求参数时,仅对该参数的请求限流,使用其他请求参数则可以正常响应,这样可以提高一定的可用性

使用热点规则需要注意的点:

参数必须是基本类型或者String类型,否则将不会生效

热点规则相关源码:

com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowChecker#passCheck(对热点参数规则的判断逻辑都在这个方法里)
系统规则

系统规则全称为系统保护规则,从名称可以得知该规则是用于保护系统、防止系统负载过高而崩溃的,所以触发系统规则后会对整个系统限流。添加系统规则如下图所示:

设置系统规则比较简单,选择一个合适的阈值类型并填写阈值即可:

关于阈值类型:

LOAD(负载):当系统load1(1分钟的load)超过阈值,且并发线程数超过系统容量时触发,建议设置为CPU核心数 2.5(注意:仅对 Linux/Unit-like 机器生效)。例如CPU核心数为4,`4 2.5 = 10`系统容量 = maxQPS * minRT;(由Sentinel计算 )maxQPS:秒级统计出来的最大QPSminRT:秒级统计出来的最小响应时间相关源码:com.alibaba.csp.sentinel.slots.system.SystemRuleManager#checkBbrRT:所有入口流量的平均RT达到阈值时触发线程数:所有入口流量的并发线程数达到阈值时触发入口QPS:所有入口流量的QPS达到阈值时触发CPU使用率:系统CPU使用率达到阈值时触发

系统规则的判断逻辑所在的源码如下:

com.alibaba.csp.sentinel.slots.system.SystemRuleManager#checkSystem
授权规则

授权规则用于限制某个资源仅允许哪个服务访问,所以通常用于对服务消费者的访问权进行控制。我们可以在簇点链路中为某个接口添加授权规则,这里以/shares/1接口为例,如下:

新增授权规则:

该授权规则的含义为:仅允许test服务访问/shares/1接口,如果授权类型设置为黑名单则表示/shares/1接口不允许test服务访问。即白名单是授权某个服务访问,黑名单则是限制某个服务访问,从而实现访问控制的效果。
代码配置规则

上面几个关于规则的小节中已经介绍了如何在Sentinel控制台中配置各种规则,除此之外,Sentinel还支持在代码中配置这些规则,所以本小节将简单介绍一下如何在代码中进行配置。

代码如下(Tips:代码基于sentinel-core 1.5.2版本):

package com.zj.node.contentcenter.controller.content;import com.alibaba.csp.sentinel.slots.block.RuleConstant;import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule;import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager;import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowItem;import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule;import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager;import com.alibaba.csp.sentinel.slots.system.SystemRule;import com.alibaba.csp.sentinel.slots.system.SystemRuleManager;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RestController;import java.util.ArrayList;import java.util.Collections;import java.util.List;/** * 添加Sentinel规则 * * @author 01 * @date 2019-07-31 **/@Slf4j@RestControllerpublic class SentinelRuleController { /** * 测试添加流控规则 */ @PostMapping("/test-add-flow-rule") public String testAddFlowRile(String resourceName) { log.info("add flow rule. resourceName is {}", resourceName); addFlowQpsRule(resourceName); return "add flow rule success!"; } /** * 添加流控规则 * * @param resourceName 资源名称 */ private void addFlowQpsRule(String resourceName) { // 规则列表 List<FlowRule> rules = new ArrayList<>(); FlowRule rule = new FlowRule(resourceName); // 针对来源 rule.setLimitApp("default"); // 设置阈值类型为QPS rule.setGrade(RuleConstant.FLOW_GRADE_QPS); // 单机阈值 rule.setCount(20); // 将规则添加到规则列表 rules.add(rule); // 加载规则列表 FlowRuleManager.loadRules(rules); } /** * 添加降级规则 * * @param resourceName 资源名称 */ private void addDegradeRule(String resourceName) { List<DegradeRule> rules = new ArrayList<>(); DegradeRule rule = new DegradeRule(resourceName); // 设置降级策略为 RT rule.setGrade(RuleConstant.DEGRADE_GRADE_RT); // set threshold RT, 10 ms(设置RT时间阈值) rule.setCount(10); // 时间窗口 rule.setTimeWindow(10); rules.add(rule); DegradeRuleManager.loadRules(rules); } /** * 添加热点规则 * * @param resourceName 资源名称 */ private void addHotRule(String resourceName) { ParamFlowRule rule = new ParamFlowRule(resourceName); // 参数索引 rule.setParamIdx(0); // 单机阈值 rule.setCount(5); // 添加参数例外项 ParamFlowItem item = new ParamFlowItem(); // 参数类型 item.setClassType(int.class.getName()); // 参数值 item.setObject("5"); // 限流阈值 item.setCount(10); rule.setParamFlowItemList(Collections.singletonList(item)); ParamFlowRuleManager.loadRules(Collections.singletonList(rule)); } /** * 添加系统规则 */ private void addSystemRule() { List<SystemRule> rules = new ArrayList<>(); SystemRule rule = new SystemRule(); // 设置系统最高负载阈值 rule.setHighestSystemLoad(10); rules.add(rule); SystemRuleManager.loadRules(rules); } /** * 添加授权规则 * * @param resourceName 资源名称 * @param limitApp 流控应用(指调用方,多个调用方名称使用英文逗号分隔) */ private void addAuthorityRule(String resourceName, String limitApp) { AuthorityRule rule = new AuthorityRule(); // 资源名称 rule.setResource(resourceName); // 流控应用 rule.setLimitApp(limitApp); // 设置授权类型为白名单 rule.setStrategy(RuleConstant.AUTHORITY_WHITE); AuthorityRuleManager.loadRules(Collections.singletonList(rule)); }}

我们来测试添加流控规则,使用postman访问测试接口,如下:

添加成功后,到Sentinel控制台中,查看是否存在该规则:

从上图中可以看到该流控规则已经成功添加到Sentinel中了,证明测试成功。至于其他的规则也可以使用类似的方式添加,并且也都给出了代码,这里就不一一去演示了。


Sentinel规则参数总结

下面总结一下Alibaba Sentinel各种规则的参数,并且提供了官方文档的链接,若未来本文不再适用,可以自行点击链接前往官方文档查看

1、流控规则:

Field说明默认值resource资源名,资源名是限流规则的作用对象无count限流阈值无grade限流阈值类型,QPS 或线程数模式QPS 模式limitApp流控针对的调用来源default,代表不区分调用来源strategy判断的根据是资源自身,还是根据其它关联资源 (refResource),还是根据链路入口根据资源本身controlBehavior流控效果(直接拒绝 / 排队等待 / 慢启动模式)直接拒绝

官方文档:

流量控制如何使用#流量控制规则-flowrule

2、降级规则:

Field说明默认值resource资源名,即限流规则的作用对象无count阈值无grade降级模式,根据 RT 降级还是根据异常比例或异常数降级RTtimeWindow降级的时间,单位为 s无

官方文档:

如何使用#熔断降级规则-degraderule熔断降级

3、热点规则:

Field说明默认值resource资源名,即热点规则的作用对象无count限流阈值,必填无grade限流模式QPS 模式durationInSec统计窗口时间长度(单位为秒),1.6.0 版本开始支持1scontrolBehavior流控效果(支持快速失败和匀速排队模式),1.6.0 版本开始支持快速失败maxQueueingTimeMs最大排队等待时长(仅在匀速排队模式生效),1.6.0 版本开始支持0msparamIdx热点参数的索引,必填,对应 SphU.entry(xxx, args) 中的参数索引位置无paramFlowItemList参数例外项,可以针对指定的参数值单独设置限流阈值,不受前面 count 阈值的限制。仅支持基本类型和字符串类型无clusterMode是否是集群参数流控规则falseclusterConfig集群流控相关配置无

官方文档:

热点参数限流#热点参数规则

4、系统规则:

Field说明默认值highestSystemLoad最大的 load1,参考值-1 (不生效)avgRt所有入口流量的平均响应时间-1 (不生效)maxThread入口流量的最大并发数-1 (不生效)qps所有入口资源的 QPS-1 (不生效)

官方文档:

如何使用#系统保护规则-systemrule系统自适应限流

5、授权规则:

Field说明默认值resource资源名,即授权规则的作用对象无limitApp流控应用(指调用方,即服务消费者),对应的黑名单/白名单,不同 origin 用英文逗号(,)分隔,如 appA,appB无strategy限制模式,AUTHORITY_WHITE 为白名单模式,AUTHORITY_BLACK 为黑名单模式,默认为白名单模式AUTHORITY_WHITE

官方文档:

如何使用#访问控制规则-authorityrule黑白名单控制