服务雪崩

什么是服务雪崩?

服务雪崩是一种因”服务提供者的不可用”导致”服务调用者不可用”,并将不可用逐渐放大的现象

雪崩的原因及三个阶段

阶段一: 服务不可用

​ – 硬件故障

​ – 程序BUG

​ – 缓存击穿 -> 例如一个程序是通过id号查询其详细信息,我们利用redis缓存保存一些热点信息,当一个查询的id不在缓存内时,会转向数据库查询这些数据,每次访问这些数据都会直接去查询数据库里面的信息

​ – 用户的大量请求

阶段二: 调用端的重试加大流量(例: openfeign的底层使用了重试器,当用户发送请求时,发生了网络抖动,导致请求无法在规定的时间的时间内拿到数据,这时重试器会再次发送这个请求,再次对数据进行拉去)

​ – 用户重试

​ – 代码逻辑重试

阶段三: 服务调用者不可用 ->同步等待造成的资源耗尽

解决方案:

流控限流

缓存预加载

服务降级(保留重要的服务关闭不重要的服务;降低一致性)

服务熔断

舱壁模式 规定每个业务的最大线程数

服务熔断-断路器模式

最开始处于closed状态,一旦检测到错误到达一定阈值,便转为open状态;

这时候会有个 reset timeout,到了这个时间了,会转移到half open状态;

尝试放行一部分请求到后端,一旦检测成功便回归到closed状态,即恢复服务;

Sentinel

概念

Sentinel 是一款开源的,且功能强大的流量控制和熔断降级框架,用于保护分布式系统的稳定性和可靠性。它主要用于应对高并发访问、突发流量和异常情况下的服务保护和流量控制。

Sentinel以流量为切入点,从流量控制熔断降级,系统负载保护等来保障服务的稳定性.

组件与功能

多种限流算法:包括令牌桶、漏桶等,可以根据业务场景选择合适的算法。

多种限流维度:包括QPS、并发线程数、异常比例等,可以根据不同的维度来进行限流。

多种应用场景:支持Dubbo、Spring Cloud、gRPC等多种RPC框架的服务发现和调用。

动态规则源:支持多种数据源,如Nacos、Zookeeper、Apollo等,可以动态推送和更新规则。

实时监控:提供实时的监控和统计功能,可以查看服务的运行状态和指标

Sentinel本地项目

引入依赖-核心库

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.6</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.8.6</version>
</dependency>

定义资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
//配置流控规则
initFlowRules();
//模拟访问
while (true) {
// 1.5.0 版本开始可以直接利用 try-with-resources 特性
try (Entry entry = SphU.entry("HelloWorld")) { //HelloWorld 资源呢的名字
// 被保护的逻辑
System.out.println("要进行的操作");
} catch (BlockException ex) {
// 处理被流控的逻辑
System.out.println("blocked!");
}
}
}

规则配置

1
2
3
4
5
6
7
8
9
10
private static void initFlowRules(){
List<FlowRule> rules = new ArrayList<>(); //规则列表
FlowRule rule = new FlowRule();
rule.setResource("HelloWorld");//此规则对HelloWorld起作用,匹配资源名
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);//将规则设置为按照每秒请求数来进行限流 QPS: query per second
// Set limit QPS to 20.
rule.setCount(200000); //每秒200000个请求
rules.add(rule);
FlowRuleManager.loadRules(rules); //添加规则到规则管理器
}

要进行的操作 表示服务正常运行

blocked!表示每秒的请求数超过规定的20000次被拒绝

Sentinel控制台

Sentinel安装与配置

安装sentinel-dashboard-1.8.6.jar

下载地址: https://github.com/alibaba/Sentinel/releases/tag/1.8.6

启动

新建一个.bat后缀的批处理文件,以java命令启动

1
2
3
// `./` 与该文件同级下的sentinel-dashboard-1.8.6.jar文件
java -jar ./sentinel-dashboard-1.8.6.jar --server.port=9999
pause

访问Sentinel控制台

http://localhost:9999/#/login

默认用户名密码都为sentinel

引入依赖

客户端信息需要用HttpClient接入到Sentinel客户端

1
2
3
4
5
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.8.6</version>
</dependency>

配置启动参数

指定控制台的地址和端口

服务连接sentinel控制台

启动SentinelApp,在控制台查看详情(图表可视化)

动态修改阈值

Sentinel控制台动态修改阈值,通过实时监控,可以看到阈值可以通过控制台在程序运行时动态实时修改

微服务整合Sentinel客户端

原理图

1.引入依赖

重要子依赖

simple-http:它是一个简版的http服务器,用于与dashboard通讯,Sentinel中的simple http是一种提供HTTP端点的通信模块,用于接收来自Dashboard的控制指令

aspectj的依赖: 用于完成对目标资源的统计(流量QPS)工作(aop)

1
2
3
4
5
<!-- sentinel 客户端依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

2.yml中配置sentinel服务

在nacos配置中心的resfoods的配置中修改,如果使用了负载均衡策略有该项目多个配置文件,可在不同的配置文件中配置不同的sentinel端口即可

1
2
3
4
5
6
7
spring:
cloud:
sentinel: #sentinel配置
transport:
port: 8719 #跟控制台交流的端口,随意指定一个未使用的端口即可
dashboard: localhost:9999 #dashboard地址与端口
eager: true #表示 Sentinel 会在应用启动时立即进行初始化。这意味着 Sentinel 会立即加载规则、统计信息等相关的数据,并开始监控和限流

3.测试

启动项目建立与sentinel夫区埃的连接,使用Jmeter测试是否可以对客户端进行监控

熔断降级

原理图

慢调用

慢调用RT(最大的响应时间)

请求的响应时间大于RT则统计为慢调用

当单位统计时长(统计窗口时长)内请求数目大于设置的最小请求数目,并且慢调用的比例要大于阈值,接下来熔断时长内的额请求会被自动熔断(同时满足两个条件)

示例:

1
2
3
4
5
6
7
8
9
@GetMapping("payAction")
public Map<String,Object> payAction( Integer flag ) throws InterruptedException { //TODO: 1. 测试慢请求
Thread.sleep(1000);//模拟慢请求

Map<String,Object> map=new HashMap<>( );
//取出当前用户的订单金额,调用第三方接口,完成支付.
map.put("code",1);
return map;
}

sentinel控制台配置熔断规则

在Apache Jmeter里配置请求

100个请求分5秒发送,每秒20个请求

发送请求,查看控制台实时监控图

如图14:49:16 - 14:49:18时刻,14:49:16时刻单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,发生熔断,在熔断这两秒的时长内,也就是14:49:18时刻发送了一个请求测试是否有响应,经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态)

异常比例与异常数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@GetMapping("payAction")
public Map<String,Object> payAction( Integer flag ) throws InterruptedException {
//2.异常数 这里异常比例为40%
Random r = new Random();
int a = r.nextInt(5);
// (0,1)/(0,1,2,3,4) 40%
if (a==0||a==1){
throw new RuntimeException("发生异常");
}
Map<String,Object> map=new HashMap<>( );
//取出当前用户的订单金额,调用第三方接口,完成支付.
map.put("code",1);
return map;
}

在Apche Jmeter中查看详情

通过测试可以得知当前异常比大约为40%

配置熔断规则

这里异常数配置为6是因为,发送的请求的异常比例为40%,又因为每秒的请求数有20,当异常数为8的时候,异常比例为40%,设置为6说明我们模拟的环境是发生熔断的条件是低于40%异常比(阈值低于0.4),而程序中的异常比规定为40%,异常的比例大于阈值发生熔断

Apche Jmeter重新发送请求,查看控制台监控图

流控

阈值类型:
QPS: 单位实践类,请求接口数限制

并发线程数: 单位时间类, 请求并发数限制

单机阈值: 每秒限制的次数

流控模式: 直接 快速 失败

流控效果: 快速失败 Warm Up 排队等待

快速失败: 单位时间内请求数超过阈值直接拒绝

Warm Up预热时间: 冷启动,在预热时间内请求数慢慢增多直至达到达到阈值,但不超过阈值

排队等待: 在超时时间内,所有请求根据阈值限制,均匀排成多个队列进行排队等待

关联流量控制

防止两个或多个资源之间有资源争抢而影响资源吞吐量

比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢

热点限流

控制热门资源的访问流量,以避免资源过载

Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控

1.定义资源

在需要限流服务的方法上加入@SentinelSource(“流控资源名”)

2.启动服务

启动本地服务与sentinel控制台建立连接

3.测试

在Apache Jmeter上配置请求,每秒100个请求,循环一次,每个请求访问第一页数据

在sentinel控制台编辑热点规则

这里对请求进行了限流,限定了只接受两个请求

在实时监控图中可看到只有两个请求通过,其余的请求被拒绝

链路限流

只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就可以限流)[api级别的针对来源]

示例

1.关闭默认整合链路功能

sentinel底层统计链路信息的时候,将多个入口的请求做了合并

              machine-root
               /       \
              /         \
        Entrance1     Entrance2
           /             \
          /               \
 DefaultNode(nodeA)   DefaultNode(nodeA)

请求不管从哪个入口进入都会进行计数,所以需要关闭默认整合链路功能,每条链路单独计数

1
2
3
4
spring:
cloud:
sentinel:
web-context-unify: false #关闭context整合

2.示例

模拟订单的两个统计功能,无论哪个统计都需要访问商品信息

​ goodsInfo

​ / \

​ / \

​ serviceA serviceB

业务类

1
2
3
4
5
6
7
@Service
public class GoodsBiz {
@SentinelResource("goodsInfo") //将此方法定义为sentinel管理的资源
public void goodsInfo(){
System.out.println("商品信息");
}
}

controller类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Autowired
private GoodsBiz goodsBiz;

@RequestMapping(value = "serviceA",method = {RequestMethod.GET,RequestMethod.POST})
public Map<String,Object> serviceA(){
Map<String,Object> map = new HashMap<>();
goodsBiz.goodsInfo();
map.put("code",1);
return map;
}

@RequestMapping(value = "serviceB",method = {RequestMethod.GET,RequestMethod.POST})
public Map<String,Object> serviceB(){
Map<String,Object> map = new HashMap<>();
goodsBiz.goodsInfo();
map.put("code",1);
return map;
}

Apche Jmeter

一个线程组添加两个Http请求 serviceA serviceB

参数:

  • 线程数 100
  • Ramp-Up 1
  • 循环次数 1

流控规则配置

这段配置的意思是只要goodInfo的调用次数超过2就限制service的流量

结果

授权规则

黑白名单控制

规则配置

来源访问控制规则(AuthorityRule)非常简单,主要有以下配置项:

  • resource:资源名,即限流规则的作用对象。
  • limitApp:对应的黑名单/白名单,不同 origin 用 , 分隔,如 appA,appB
  • strategy:限制模式,AUTHORITY_WHITE 为白名单模式,AUTHORITY_BLACK 为黑名单模式,默认为白名单模式。

配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。

官方文档: https://github.com/alibaba/Sentinel/wiki/黑白名单控制

系统自适应限流

系统规则

系统规则支持以下的模式:

  • Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5
  • CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
  • 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
  • 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
  • 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。

推论

我们把系统处理请求的过程想象为一个水管,到来的请求是往这个水管灌水,当系统处理顺畅的时候,请求不需要排队,直接从水管中穿过,这个请求的RT是最短的;反之,当请求堆积的时候,那么处理请求的时间则会变为:排队时间 + 最短处理时间。

  • 推论一: 如果我们能够保证水管里的水量,能够让水顺畅的流动,则不会增加排队的请求;也就是说,这个时候的系统负载不会进一步恶化。

接下来的问题是,水管的水位是可以达到了一个平衡点,但是这个平衡点只能保证水管的水位不再继续增高,但是还面临一个问题,就是在达到平衡点之前,这个水管里已经堆积了多少水。如果之前水管的水已经在一个量级了,那么这个时候系统允许通过的水量可能只能缓慢通过,RT会大,之前堆积在水管里的水会滞留;反之,如果之前的水管水位偏低,那么又会浪费了系统的处理能力。

  • 推论二: 当保持入口的流量是水管出来的流量的最大的值的时候,可以最大利用水管的处理能力。

使用

1.添加系统规则

2.单服务流量分配

Apche Jmeter参数:

  • 线程数 100
  • Ramp-Up 1
  • 循环次数 1

3.多服务流量分配

按每秒服务请求数比例进行分配

异常处理

流控异常处理

1
@SentinelResource(value = "findById",blockHandler = "blockHandlerForFindById")

业务异常处理

当资源出现业务异常时,统一回调一个处理方法,重组异常信息

1
2
3
//fallback 业务异常处理,handleException: 流控回调处理的方法名
@SentinelResource(value = "findById",fallback = "handleException")

1
2
3
4
5
6
7
public Map<String,Object> handleException(Integer fid, Throwable exception){

Map<String,Object> map = new HashMap<>();
map.put("code",0);
map.put("msg","出现异常,异常信息为:" + exception.getMessage());
return map;
}

异常的统一处理