分布式架构下的服务雪崩问题

分布式架构请求链路图

在分布式架构下,往往一个服务会依赖其他服务,如 商品详情服务,依赖于 商品服务、价格服务、商品评论服务。

服务雪崩

当一个用户请求进入后,需要查询商品详情,就会去访问 商品服务、价格服务、评论服务。但是如果此时评论服务宕机或其他原因导致服务不可用时,所有的线程都会被阻塞,从而造成 上游服务资源耗尽导致服务雪崩。

服务雪崩:因为服务提供者服务不可用,导致调用者的线程堵塞并逐渐放大,最终导致整个依赖链路都出现问题。

导致雪崩的原因

  • 高并发请求
    • 一般在秒杀或者促销的情况下出现,瞬间高并发大量的请求进来,服务提供者没有做缓存预热和服务集群来应对,导致服务不可用,从而导致链上的服务雪崩
  • 硬件故障
    • 服务提供者硬件故障,导致上游调用者服务不可用,最终整个依赖链路服务雪崩
  • 缓存击穿
    • 缓存层应用故障或缓存大量同一时间失效导致大量缓存没有命中,请求打到了存储层 数据库上,导致数据库宕机
  • 业务层BUG
    • 代码BUG导致服务不可用,自己写的BUG自己认,自己挖的坑自己跳

服务雪崩解决方案

超时机制

如果没有超时机制,服务提供者不可用的话会导致消费者的线程一直等待,直到 资源耗尽。

如:消费者通过线程池的线程请求某服务,但是某服务一直没有响应,消费者就一直等待。以此类推,线程池的线程就会被耗尽 。

如果引入了超时机制,服务提供者在规定的时间内没有正确的响应,就会释放该线程。

服务限流

服务限流

核心思想:为一些服务分配固定的线程,这样即便某个服务挂掉了,也只会消耗固定的线程,也不会导致线程池里的线程都被消耗掉。

如:使用服务限流后,详情服务调用评论服务,此时评论服务不可用导致20个线程等待,就不会再启用线程调用该服务,同时也不会影响其他的服务现成调用。

服务熔断

远程服务不稳定或网络抖动时暂时关闭,就叫服务熔断。

服务熔断

  1. 当大量的请求进行服务调用时,服务调用失败了,达到阈值就会触发 断路器打开。
  2. 断路器打开一段时间后,就会进入 半开状态,半开状态只是一个瞬间的状态
  3. 进入半开状态后 只允许一次请求调用,服务这次请求成功了,就会关闭 断路器,如果请求还是失败,就会打开断路器。过段时间后,再次进入半开状态。

半开状态可以实现 应用的自我恢复,并且通过断路器可以避免资源浪费。

服务降级

有服务熔断,必然要有服务降级。当服务不可用时,我们可以降级 让请求访问另外一个服务或者一个fallback 回调方法,返回一个友好的提示,如 “服务繁忙请稍后再试”。

虽然降级好像看起来没什么作用,但是却能给用户一个友好的提示,比 服务直接宕机返回 未知错误 强。

具体怎么降级要看实际的场景,如果服务不可用,有的场景需要降级到 另外的服务进行补偿措施,有的场景直接返回友好的错误提示。

Sentinel 微服务流量治理组件

什么是Sentinel

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。

源码地址:https://github.com/alibaba/Sentinel

官方文档:https://github.com/alibaba/Sentinel/wiki https://sentinelguard.io/zh-cn/docs/introduction.html

Sentinel优势:

  • 丰富的应用场景: Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、实时熔断下游不可用应用等。
  • 完备的实时监控: Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
  • 广泛的开源生态: Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
  • 完善的 SPI 扩展点: Sentinel 提供简单易用、完善的 SPI 扩展点。您可以通过实现扩展点,快速的定制逻辑。例如定制规则管理、适配数据源等。

阿里云除了开源的Sentinel外,还提供了 企业级的 Sentinel 服务,应用高可用服务 AHAS,不过是商业收费的。

Sentinel和Hystrix对比

Hystrix 的关注点在于以 隔离 和 熔断 为主的容错机制,超时或被熔断的调用将会快速失败,并可以提供 fallback 机制。

而 Sentinel 的侧重点在于:

  • 多样化的流量控制
  • 熔断降级
  • 系统负载保护
  • 实时监控和控制台

0

相较Hystrix而言,Sentinel明显支持的功能更多,更加全面,所以我们当前的微服务架构更多的是使用Sentinel来进行流量治理。

Sentinel 工作原理

基本概念

资源

资源是Sentinel的概念,可以是一个应用,也可以是一段代码。我们可以通过Sentinel API把这个资源保护起来,而我们可以通过方法签名,URL,甚至服务名称作为资源名来标示资源。

规则

根据资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。

Sentinel 功能和设计理念

流量控制

每台服务节点的处理资源都是有限的,如果流量太大的话,节点承受不住就会变得卡顿甚至内存溢出从而导致宕机,甚至上游服务雪崩。

为此,我们就需要对流量进行控制,比如 某节点最大1秒只能承受1000次请求,那我们就对流量进行控制,让他1秒内接收最大800个请求,留下20%的资源 确保服务有足够的预留空间。

流量控制

流量控制有以下几个角度:

  • 资源的调用关系,例如资源的调用链路,资源和资源之间的关系;
  • 运行指标,例如 QPS、线程池、系统负载等;
  • 控制的效果,例如直接限流、冷启动、排队等。

Sentinel 的设计理念是让您自由选择控制的角度,并进行灵活组合,从而达到想要的效果。

熔断降级

Sentinel 和 Hystrix 的原则是一致的: 当调用链路中某个资源出现不稳定,例如,表现为 timeout,异常次数达到了阈值,就会对这个资源的调用进行限制,并让请求快速失败,避免影响到其它的资源,最终产生雪崩的效果。

熔断降级

在限制的手段上,Sentinel 和 Hystrix 采取了完全不一样的方法。

Hystrix 通过线程池的方式,来对资源进行了隔离。这样做的好处是资源和资源之间做到了最彻底的隔离。缺点是除了增加了线程切换的成本,还需要预先给各个资源做线程池大小的分配。

Sentinel通过两种方式来进行熔断降级

  • 通过并发线程数进行限制

Sentinel通过限制并发线程数,来实现 流量控制。

如:1秒内只允许5个线程,此时5个线程的时间窗口(运行周期,请求处理时间)是2秒,如果有新的请求进来会直接被拒绝,因为在1秒内5个线程都占满了。

  • 通过响应时间(QPS)对资源进行降级

除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间窗口之后才重新恢复。

假设一个请求响应时间是0.2秒,那么1秒的QPS就是5,如果我们设置1秒只允许5个请求的话,新的请求进来就就会被拒绝。

系统过载保护

单台节点的并发IO是有限的,如果大量的请求进入了该节点,该节点资源处理不过来,很可能会宕机。我们可以通过过载保护来解决这个问题,超过阈值的请求都会被拒绝返回。

Sentinel工作主流程

官方文档:https://github.com/alibaba/Sentinel/wiki/Sentinel%E5%B7%A5%E4%BD%9C%E4%B8%BB%E6%B5%81%E7%A8%8B

在 Sentinel 里面,所有的资源(服务)都对应一个资源名称(resourceName),每次资源调用都会创建一个 Entry 对象。

Entry 可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 SphU API 显式创建。

Slot功能槽

Entry 创建的时候,同时也会创建一系列功能插槽(slot chain),这些插槽有不同的职责:

  • NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
  • ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
  • StatisticSlot 则用于记录、统计不同纬度的 runtime 指标监控信息;
  • FlowSlot 则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;
  • AuthoritySlot 则根据配置的黑白名单和调用来源信息,来做黑白名单控制;
  • DegradeSlot 则通过统计信息以及预设的规则,来做熔断降级;
  • SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量;

Sentinel构建链路

当外部请求进来后,sentinel会对每个资源(服务)构建一个链路,后面所有服务都会走这个链路,而在这个链路上会有很多的slot,每个slot都有自己的规则

自定义Slot

自定义Slot功能槽

Sentinel 将 ProcessorSlot 作为 SPI 接口进行扩展,使得 Slot Chain 具备了扩展的能力。我们可以自行加入自定义的 slot 并编排 slot 间的顺序,从而可以给 Sentinel 添加自定义的功能。

SpringBoot集成Sentinel

在代码中,集成Sentinel的办法有很多种,常用的有两种:

  • 代码实现策略限流
  • 通过集成SpringCloudSentinel并且在WEB管理端配置策略

本次实验的所有sentinel版本均为:1.8.4。

建议:springBoot的sentinel 依赖和sentinel dashboard的版本要统一,避免一些意料之外的错误。

代码实现策略限流

1.1 引入依赖

1
2
3
4
5
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.4</version>
</dependency>

1.2 编写策略和业务逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.javaxing.springsentineldemo.controller;

import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.BlockException;
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 org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;

@RestController
public class TestSentinelController {

private static final String RESOURCE_NAME = "sentinelTest";

@GetMapping("/sentinel")
public String sentinel(){
try (Entry entry = SphU.entry(RESOURCE_NAME)) {
// 被保护的逻辑
return "正常的逻辑...";
} catch (BlockException ex) {
// 处理被流控的逻辑
return "被流控了";
}
}


/**
* 定义流控规则
*/
@PostConstruct
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);
// 加载配置好的规则
FlowRuleManager.loadRules(rules);
}
}

1秒内访问1次

image-20230406193150866

1秒内访问2次,就会触发流控策略

image-20230406193158175

1.3 缺点

  • 业务侵入性很强,需要在java代码里面进行 用sentinel代码去包裹
  • 配置不灵活 若需要添加新的受保护资源 需要手动添加在代码里面添加

Spring Cloud Alibaba整合Sentinel

2.1 引入依赖

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

2.2 Controller控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
public class TestSpringBootSentinelController {
@GetMapping("/springBootSentinelException")
public String springBootSentinelException(){
// 模拟代码异常
int a = 1/0;
return "正常的业务逻辑执行...";
}

@GetMapping("/springBootSentinelSuccess")
public String springBootSentinelSuccess(){
// 模拟代码异常
return "正常的业务逻辑执行...";
}
}

2.3 application.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
spring:
application:
name: sentinel

cloud:
nacos:
config:
server-addr: 10.211.55.12:8848
discovery:
server-addr: 10.211.55.12:8848
sentinel:
transport:
# 添加sentinel的控制台地址
dashboard: 10.211.55.12:9991
# 指定应用与Sentinel控制台交互的端口,如果不指定,应用本地会起一个该端口占用的HttpServer
# port: 8719

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

1、我们需要在application.yaml中指定 sentinel控制台地址,服务会与该IP进行通信

2、SpringBoot默认不会暴露actuator端点,所以我们需要在application.yaml中设置

2.4 安装并运行 Sentinel dashboard

  • 我们可以通过Sentinel dashboard配置流控规则和策略

github:https://github.com/alibaba/Sentinel/wiki/%E6%8E%A7%E5%88%B6%E5%8F%B0#2-%E5%90%AF%E5%8A%A8%E6%8E%A7%E5%88%B6%E5%8F%B0

本次实验的下载地址:https://files.javaxing.com/sentinel/sentinel-dashboard-1.8.4.jar

1
2
# 运行控制台jar包
nohup java -jar sentinel-dashboard-1.8.4.jar --server.port=9991 >s.log

用户可以通过如下参数进行配置:

  • -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 分钟;

登录sentinel

访问http://10.211.55.12:9998/#/login,默认用户名密码: sentinel/sentinel

sentinel控制台

控制台

image-20230407123912013

sentinel刚开始是没数据的,sentinel会在客户端首次调用接口的时候进行初始化,所以我们需要访问一下controller

2.5 访问controller

image-20230407124008595

访问controller接口后,再次查看sentinel dashboard:

image-20230407124145615

2.6 对接口进行流控

image-20230407124658067

  • 资源名: 接口的API
  • 针对来源: 默认是default,当多个微服务都调用这个资源时,可以配置微服务名来对指定的微服务设置阈值
  • 阈值类型: 分为QPS和线程数,假设阈值为1
  • QPS类型: 只得是每秒访问接口的次数>1就进行限流
  • 线程数: 为接受请求该资源分配的线程数>1就进行限流

2.7 测试QPS流控

1秒内访问1次:

image-20230407125234523

1秒内访问2次,会触发流控策略被限流:

image-20230407125257592

微服务和Sentinel Dashboard通信原理

Sentinel控制台与微服务端之间,实现了一套服务发现机制。

1、微服务启动后,会通过 sentinel-transport-simple-http模块,将自己的注册到 Sentinel Dashboard中

2、当用户访问微服务的controller时,sentinel Dashboard会去微服务中获取相应的元数据,值得注意的是 获取的元数据,是不会存储在数据库的,如果微服务一旦重启,数据就会丢失

0

缺点:

  1. 由于数据并不会持久化在Sentinel Dashboard,一旦微服务重启或控制台重启,则数据都会丢失
  2. 如果只是一个节点我们就配置一个策略,那么在生产环境中存在大量的节点就需要配置很多次,明显这不是一个很合理的方案。

通信源码分析

image-20230408121809112

先下载sentinel commandHandler的源码和HttpEventTask源码

image-20230408122823587

下载后,我们可以通过 查找handle使用的方法,去定位到具体的调用位置:

image-20230409155416086

找到调用位置:

image-20230408122930904

image-20230408123000708

控制台发送规则,服务端接收规则后存储到内存中即时生效

image-20230408123844553

image-20230408124141233

Sentinel 集成Open Feign

sentinel可以对api 接口等资源进行流控外,也可以对open feign的接口进行拦截并进行一系列流控。

1、项目结构介绍

image-20230502143044542

这里一共有2个模块,1、sentinel demo 2、xingmall-order ,我们都注册到了nacos中,并且在sentinel demo上通过open feign调用xingmall-order的controller

sentime demo pom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

<!--sentinel持久化 采用 Nacos 作为规则配置数据源-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>


</dependencies>


<repositories>
<repository>
<id>nexus-xu</id>
<name>Nexus Central</name>
<!-- 虚拟的URL形式,指向镜像的URL-->
<url>https://maven.javaxing.com/repository/sentinel/</url>
<layout>default</layout>
<!-- 表示可以从这个仓库下载releases版本的构件-->
<releases>
<enabled>true</enabled>
</releases>
<!-- 表示可以从这个仓库下载snapshot版本的构件 -->
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>

xingmall-order 只是一个很简单的微服务,对外提供了一个controller,所以这里就不贴pom了

2、sentinel demo写feign接口

1
2
3
4
5
6
7
8
9
10
11
12
package com.javaxing.springsentineldemo.api;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

// 这里有一个fallback 失败的回调,这个回调用于在sentinel 流控后的一个降级处理
@FeignClient(value = "xingmall-order",path = "/order",fallback = FallBackOrderFiegnService.class)
public interface OrderFeignService {
@GetMapping("/queryOrder")
public String queryOrder(@RequestParam("orderId") Integer orderId);
}

fallback

1
2
3
4
5
6
7
8
9
10
11
package com.javaxing.springsentineldemo.api;

import org.springframework.stereotype.Component;

@Component
public class FallBackOrderFiegnService implements OrderFeignService{
@Override
public String queryOrder(Integer orderId) {
return "触发异常了,这是一个异常处理";
}
}

3、sentinel demo调用feign接口

1
2
3
4
5
6
7
@Autowired
private OrderFeignService orderFeignService;

@GetMapping("/springBootSentinelSuccess")
public String springBootSentinelSuccess() {
return orderFeignService.queryOrder(111);
}

4、sentinel dashboard设置流控策略

image-20230502144204907image-20230502144313969image-20230502144331606

如果我们要去对openfeign进行流控的话,链路资源名 和正常的controller不太一样,需要注意, 针对正确的资源名称进行流控

5、访问测试

image-20230502144432978image-20230502144453822

如图所示,我们1秒访问一次 接口返回正常查询,1秒访问2次就会触发流控并且进入了fallBack的回调(降级)。